Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b926fa27b | ||
|
|
8999b2b9e8 | ||
|
|
ed7463132a | ||
|
|
d20f10cf8b | ||
|
|
ebbae051bd | ||
|
|
521f39b6fb |
+102
@@ -0,0 +1,102 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.Service;
|
||||
import android.os.IBinder;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import android.os.Build;
|
||||
import im.status.ethereum.module.R;
|
||||
|
||||
public class ForegroundService extends Service {
|
||||
private static final String CHANNEL_ID = "status-service";
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent i, int flags, int startId) {
|
||||
// NOTE: recent versions of Android require the service to display
|
||||
// a sticky notification to inform the user that the service is running
|
||||
Context context = getApplicationContext();
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
|
||||
Intent intent = null;
|
||||
String notificationContentText = null;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
context.getSystemService(NotificationManager.class);
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
context.getResources().getString(R.string.status_service),
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.setShowBadge(false);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
|
||||
// Create intent that takes the user to the notification channel settings so they can hide it
|
||||
|
||||
intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID);
|
||||
|
||||
notificationContentText = context.getResources().getString(R.string.tap_to_hide_notification);
|
||||
|
||||
} else {
|
||||
|
||||
// For older versions of android intent takes the user to the Status app
|
||||
|
||||
Class intentClass;
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
intentClass = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
|
||||
intent = new Intent(context, intentClass);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
notificationContentText = context.getResources().getString(R.string.keep_status_running);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE);
|
||||
Intent stopIntent = new Intent(PushNotificationHelper.ACTION_TAP_STOP);
|
||||
PendingIntent stopPendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setContentTitle(context.getResources().getString(R.string.background_service_opened))
|
||||
.setContentText(notificationContentText)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setNumber(0)
|
||||
.addAction(R.drawable.ic_stat_notify_status,
|
||||
context.getResources().getString(R.string.stop),
|
||||
stopPendingIntent)
|
||||
.build();
|
||||
// the id of the foreground notification MUST NOT be 0
|
||||
startForeground(1, notification);
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.app.Service
|
||||
import android.os.IBinder
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import android.os.Build
|
||||
import im.status.ethereum.module.R
|
||||
|
||||
class ForegroundService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(i: Intent?, flags: Int, startId: Int): Int {
|
||||
// NOTE: recent versions of Android require the service to display
|
||||
// a sticky notification to inform the user that the service is running
|
||||
val context: Context = getApplicationContext()
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
var intent: Intent? = null
|
||||
var notificationContentText: String? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
val channel = NotificationChannel(CHANNEL_ID,
|
||||
context.getResources().getString(R.string.status_service),
|
||||
NotificationManager.IMPORTANCE_HIGH)
|
||||
channel.setShowBadge(false)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
|
||||
// Create intent that takes the user to the notification channel settings so they can hide it
|
||||
intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName())
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID)
|
||||
notificationContentText = context.getResources().getString(R.string.tap_to_hide_notification)
|
||||
} else {
|
||||
// For older versions of android intent takes the user to the Status app
|
||||
val intentClass: java.lang.Class<*>
|
||||
val packageName: String = context.getPackageName()
|
||||
val launchIntent: Intent? = context.getPackageManager().getLaunchIntentForPackage(packageName)
|
||||
val className: String? = launchIntent?.getComponent()?.getClassName()
|
||||
if (className == null) {
|
||||
return 0
|
||||
}
|
||||
intentClass = try {
|
||||
java.lang.Class.forName(className)
|
||||
} catch (e: java.lang.ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
intent = Intent(context, intentClass)
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
intent.setAction(Intent.ACTION_VIEW)
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
notificationContentText = context.getResources().getString(R.string.keep_status_running)
|
||||
}
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
|
||||
val stopIntent = Intent(PushNotificationHelper.ACTION_TAP_STOP)
|
||||
val stopPendingIntent: PendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
val notification: Notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setContentTitle(context.getResources().getString(R.string.background_service_opened))
|
||||
.setContentText(notificationContentText)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setNumber(0)
|
||||
.addAction(R.drawable.ic_stat_notify_status,
|
||||
context.getResources().getString(R.string.stop),
|
||||
stopPendingIntent)
|
||||
.build()
|
||||
// the id of the foreground notification MUST NOT be 0
|
||||
startForeground(1, notification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "status-service"
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import android.util.Log;
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
|
||||
|
||||
public class PushNotification extends ReactContextBaseJavaModule implements ActivityEventListener {
|
||||
public static final String LOG_TAG = "PushNotification";
|
||||
|
||||
private final SecureRandom mRandomNumberGenerator = new SecureRandom();
|
||||
private PushNotificationHelper pushNotificationHelper;
|
||||
private PushNotificationJsDelivery delivery;
|
||||
private ReactApplicationContext reactContext;
|
||||
private boolean started;
|
||||
|
||||
public PushNotification(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
reactContext.addActivityEventListener(this);
|
||||
Application applicationContext = (Application) reactContext.getApplicationContext();
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
pushNotificationHelper = new PushNotificationHelper(applicationContext, intentFilter);
|
||||
|
||||
delivery = new PushNotificationJsDelivery(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PushNotification";
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
// Ignored, required to implement ActivityEventListener for RN 0.33
|
||||
}
|
||||
|
||||
private Bundle getBundleFromIntent(Intent intent) {
|
||||
Bundle bundle = null;
|
||||
if (intent.hasExtra("notification")) {
|
||||
bundle = intent.getBundleExtra("notification");
|
||||
} else if (intent.hasExtra("google.message_id")) {
|
||||
bundle = new Bundle();
|
||||
|
||||
bundle.putBundle("data", intent.getExtras());
|
||||
}
|
||||
|
||||
if(null != bundle && !bundle.getBoolean("foreground", false) && !bundle.containsKey("userInteraction")) {
|
||||
bundle.putBoolean("userInteraction", true);
|
||||
}
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
Bundle bundle = this.getBundleFromIntent(intent);
|
||||
if (bundle != null) {
|
||||
delivery.notifyNotification(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
/**
|
||||
* Creates a channel if it does not already exist. Returns whether the channel was created.
|
||||
*/
|
||||
public void createChannel(ReadableMap channelInfo, Callback callback) {
|
||||
boolean created = pushNotificationHelper.createChannel(channelInfo);
|
||||
|
||||
if(callback != null) {
|
||||
callback.invoke(created);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void presentLocalNotification(ReadableMap details) {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = Arguments.toBundle(details);
|
||||
// If notification ID is not provided by the user, generate one at random
|
||||
if (bundle.getString("id") == null) {
|
||||
bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
|
||||
}
|
||||
|
||||
pushNotificationHelper.sendToNotificationCentre(bundle);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clearMessageNotifications(String conversationId) {
|
||||
if (this.started) {
|
||||
pushNotificationHelper.clearMessageNotifications(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clearAllMessageNotifications() {
|
||||
pushNotificationHelper.clearAllMessageNotifications();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enableNotifications() {
|
||||
this.started = true;
|
||||
this.pushNotificationHelper.start();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disableNotifications() {
|
||||
if (this.started) {
|
||||
this.started = false;
|
||||
this.pushNotificationHelper.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.facebook.react.bridge.ActivityEventListener
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Callback
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import android.util.Log
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
|
||||
|
||||
class PushNotification(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
}
|
||||
|
||||
private val mRandomNumberGenerator: java.security.SecureRandom = java.security.SecureRandom()
|
||||
private val pushNotificationHelper: PushNotificationHelper
|
||||
private val delivery: PushNotificationJsDelivery
|
||||
private val reactContext: ReactApplicationContext
|
||||
private var started = false
|
||||
|
||||
init {
|
||||
this.reactContext = reactContext
|
||||
reactContext.addActivityEventListener(this)
|
||||
val applicationContext: Application = reactContext.getApplicationContext() as Application
|
||||
val intentFilter = IntentFilter()
|
||||
pushNotificationHelper = PushNotificationHelper(applicationContext, intentFilter)
|
||||
delivery = PushNotificationJsDelivery(reactContext)
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return "PushNotification"
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// Ignored, required to implement ActivityEventListener for RN 0.33
|
||||
}
|
||||
|
||||
private fun getBundleFromIntent(intent: Intent): Bundle? {
|
||||
var bundle: Bundle? = null
|
||||
if (intent.hasExtra("notification")) {
|
||||
bundle = intent.getBundleExtra("notification")
|
||||
} else if (intent.hasExtra("google.message_id")) {
|
||||
bundle = Bundle()
|
||||
bundle.putBundle("data", intent.getExtras())
|
||||
}
|
||||
if (null != bundle && !bundle.getBoolean("foreground", false) && !bundle.containsKey("userInteraction")) {
|
||||
bundle.putBoolean("userInteraction", true)
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
val bundle: Bundle? = getBundleFromIntent(intent)
|
||||
if (bundle != null) {
|
||||
delivery.notifyNotification(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a channel if it does not already exist. Returns whether the channel was created.
|
||||
@ReactMethod
|
||||
fun createChannel(channelInfo: ReadableMap?, callback: Callback?) {
|
||||
if (channelInfo == null) {
|
||||
return
|
||||
}
|
||||
val created: Boolean = pushNotificationHelper.createChannel(channelInfo)
|
||||
if (callback != null) {
|
||||
callback.invoke(created)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun presentLocalNotification(details: ReadableMap?) {
|
||||
if (!started) {
|
||||
return
|
||||
}
|
||||
val bundle: Bundle? = Arguments.toBundle(details)
|
||||
if (bundle == null) {
|
||||
return
|
||||
}
|
||||
// If notification ID is not provided by the user, generate one at random
|
||||
if (bundle.getString("id") == null) {
|
||||
bundle.putString("id", mRandomNumberGenerator.nextInt().toString())
|
||||
}
|
||||
pushNotificationHelper.sendToNotificationCentre(bundle)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun clearMessageNotifications(conversationId: String?) {
|
||||
if (started && conversationId != null) {
|
||||
pushNotificationHelper.clearMessageNotifications(conversationId)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun clearAllMessageNotifications() {
|
||||
pushNotificationHelper.clearAllMessageNotifications()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun enableNotifications() {
|
||||
started = true
|
||||
pushNotificationHelper.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun disableNotifications() {
|
||||
if (started) {
|
||||
started = false
|
||||
pushNotificationHelper.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.os.Build;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationActions extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, Intent intent) {
|
||||
String intentActionPrefix = context.getPackageName() + ".ACTION_";
|
||||
|
||||
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications");
|
||||
|
||||
if (null == intent.getAction() || !intent.getAction().startsWith(intentActionPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Bundle bundle = intent.getBundleExtra("notification");
|
||||
|
||||
// Dismiss the notification popup.
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);
|
||||
int notificationID = Integer.parseInt(bundle.getString("id"));
|
||||
|
||||
boolean autoCancel = bundle.getBoolean("autoCancel", true);
|
||||
|
||||
if(autoCancel) {
|
||||
if (bundle.containsKey("tag")) {
|
||||
String tag = bundle.getString("tag");
|
||||
manager.cancel(tag, notificationID);
|
||||
} else {
|
||||
manager.cancel(notificationID);
|
||||
}
|
||||
}
|
||||
|
||||
boolean invokeApp = bundle.getBoolean("invokeApp", true);
|
||||
|
||||
// Notify the action.
|
||||
if(invokeApp) {
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
PushNotificationHelper helper = new PushNotificationHelper((Application) context.getApplicationContext(), intentFilter);
|
||||
|
||||
helper.invokeApp(bundle);
|
||||
} else {
|
||||
|
||||
// We need to run this on the main thread, as the React code assumes that is true.
|
||||
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
// Construct and load our normal React JS code bundle
|
||||
final ReactInstanceManager mReactInstanceManager = ((ReactApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager();
|
||||
ReactContext context = mReactInstanceManager.getCurrentReactContext();
|
||||
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
|
||||
// If it's constructed, send a notification
|
||||
if (context != null) {
|
||||
delivery.notifyNotificationAction(bundle);
|
||||
} else {
|
||||
// Otherwise wait for construction, then send the notification
|
||||
mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
public void onReactContextInitialized(ReactContext context) {
|
||||
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
|
||||
delivery.notifyNotificationAction(bundle);
|
||||
mReactInstanceManager.removeReactInstanceEventListener(this);
|
||||
}
|
||||
});
|
||||
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
|
||||
// Construct it in the background
|
||||
mReactInstanceManager.createReactContextInBackground();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.os.Build
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactInstanceManager
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
|
||||
|
||||
class PushNotificationActions : BroadcastReceiver() {
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intentActionPrefix: String = context.getPackageName() + ".ACTION_"
|
||||
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications")
|
||||
var intentAction: String? = intent.getAction()
|
||||
if (intentAction == null || !intentAction.startsWith(intentActionPrefix)) {
|
||||
return
|
||||
}
|
||||
val bundle: Bundle? = intent.getBundleExtra("notification")
|
||||
if (bundle == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dismiss the notification popup.
|
||||
val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notificationID: Int? = bundle.getString("id")?.toInt()
|
||||
val autoCancel: Boolean = bundle.getBoolean("autoCancel", true)
|
||||
if (notificationID != null && autoCancel) {
|
||||
if (bundle.containsKey("tag")) {
|
||||
val tag: String? = bundle.getString("tag")
|
||||
if (tag != null) {
|
||||
manager.cancel(tag, notificationID)
|
||||
}
|
||||
} else {
|
||||
manager.cancel(notificationID)
|
||||
}
|
||||
}
|
||||
val invokeApp: Boolean = bundle.getBoolean("invokeApp", true)
|
||||
|
||||
// Notify the action.
|
||||
if (invokeApp) {
|
||||
val intentFilter = IntentFilter()
|
||||
val helper = PushNotificationHelper(context.getApplicationContext() as Application, intentFilter)
|
||||
helper.invokeApp(bundle)
|
||||
} else {
|
||||
// We need to run this on the main thread, as the React code assumes that is true.
|
||||
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
handler.post(object : java.lang.Runnable {
|
||||
override fun run() {
|
||||
// Construct and load our normal React JS code bundle
|
||||
val mReactInstanceManager: ReactInstanceManager = (context.getApplicationContext() as ReactApplication).getReactNativeHost().getReactInstanceManager()
|
||||
val context: ReactContext? = mReactInstanceManager.getCurrentReactContext()
|
||||
// If it's constructed, send a notification
|
||||
if (context != null) {
|
||||
val delivery = PushNotificationJsDelivery(context)
|
||||
delivery.notifyNotificationAction(bundle)
|
||||
} else {
|
||||
// Otherwise wait for construction, then send the notification
|
||||
mReactInstanceManager.addReactInstanceEventListener(object : ReactInstanceManager.ReactInstanceEventListener {
|
||||
override fun onReactContextInitialized(context: ReactContext) {
|
||||
val delivery = PushNotificationJsDelivery(context)
|
||||
delivery.notifyNotificationAction(bundle)
|
||||
mReactInstanceManager.removeReactInstanceEventListener(this)
|
||||
}
|
||||
})
|
||||
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
|
||||
// Construct it in the background
|
||||
mReactInstanceManager.createReactContextInBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+886
@@ -0,0 +1,886 @@
|
||||
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Application;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import im.status.ethereum.module.R;
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationHelper {
|
||||
|
||||
private Context context;
|
||||
|
||||
private static final long DEFAULT_VIBRATION = 300L;
|
||||
private static final String CHANNEL_ID = "status-im-notifications";
|
||||
public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION";
|
||||
public static final String ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP";
|
||||
final int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_CANCEL_CURRENT;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
|
||||
private HashMap<String, Person> persons;
|
||||
private HashMap<String, StatusMessageGroup> messageGroups;
|
||||
|
||||
private IntentFilter intentFilter;
|
||||
|
||||
public PushNotificationHelper(Application context, IntentFilter intentFilter) {
|
||||
this.context = context;
|
||||
this.intentFilter = intentFilter;
|
||||
this.persons = new HashMap<String, Person>();
|
||||
this.messageGroups = new HashMap<String, StatusMessageGroup>();
|
||||
this.notificationManager = context.getSystemService(NotificationManager.class);
|
||||
this.registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public Intent getOpenAppIntent(String deepLink) {
|
||||
Class intentClass;
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
intentClass = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
Intent intent = new Intent(context, intentClass);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
//NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well,
|
||||
//the answer is a simple as it can get in the Android native development world. I noticed
|
||||
//that my initial setup was opening the app but wasn't triggering any events on the js side, like
|
||||
//the links do from the browser. So I compared both intents and noticed that the link from
|
||||
//the browser produces an intent with the flag 0x14000000. I found out that it was the following
|
||||
//flags in this link:
|
||||
//https://stackoverflow.com/questions/52390129/android-intent-setflags-issue
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
if (deepLink != null) {
|
||||
intent.setData(Uri.parse(deepLink));
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
//NOTE: we use a dynamically created BroadcastReceiver here so that we can capture
|
||||
//intents from notifications and act on them. For instance when tapping/dismissing
|
||||
//a chat notification we want to clear the chat so that next messages don't show
|
||||
//the messages that we have seen again
|
||||
private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction() == ACTION_DELETE_NOTIFICATION) {
|
||||
String groupId = intent.getExtras().getString("im.status.ethereum.groupId");
|
||||
if (groupId != null) {
|
||||
cleanGroup(groupId);
|
||||
}
|
||||
}
|
||||
if (intent.getAction() == ACTION_TAP_STOP) {
|
||||
stop();
|
||||
System.exit(0);
|
||||
}
|
||||
Log.e(LOG_TAG, "intent received: " + intent.getAction());
|
||||
}
|
||||
};
|
||||
|
||||
public void registerBroadcastReceiver() {
|
||||
this.intentFilter.addAction(ACTION_DELETE_NOTIFICATION);
|
||||
this.intentFilter.addAction(ACTION_TAP_STOP);
|
||||
context.registerReceiver(notificationActionReceiver, this.intentFilter);
|
||||
Log.e(LOG_TAG, "Broadcast Receiver registered");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private NotificationManager notificationManager() {
|
||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
public void invokeApp(Bundle bundle) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
|
||||
try {
|
||||
Class<?> activityClass = Class.forName(className);
|
||||
Intent activityIntent = new Intent(context, activityClass);
|
||||
|
||||
if(bundle != null) {
|
||||
activityIntent.putExtra("notification", bundle);
|
||||
}
|
||||
|
||||
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
context.startActivity(activityIntent);
|
||||
} catch(Exception e) {
|
||||
Log.e(LOG_TAG, "Class not found", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendToNotificationCentre(final Bundle bundle) {
|
||||
PushNotificationPicturesAggregator aggregator = new PushNotificationPicturesAggregator(new PushNotificationPicturesAggregator.Callback() {
|
||||
public void call(Bitmap largeIconImage, Bitmap bigPictureImage) {
|
||||
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage);
|
||||
}
|
||||
});
|
||||
|
||||
aggregator.setLargeIconUrl(context, bundle.getString("largeIconUrl"));
|
||||
aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"));
|
||||
}
|
||||
|
||||
public void handleConversation(final Bundle bundle) {
|
||||
if (bundle.getBoolean("deleted")){
|
||||
this.removeStatusMessage(bundle);
|
||||
} else {
|
||||
this.addStatusMessage(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearMessageNotifications(String conversationId) {
|
||||
notificationManager.cancel(conversationId.hashCode());
|
||||
cleanGroup(conversationId);
|
||||
}
|
||||
|
||||
public void clearAllMessageNotifications() {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
|
||||
public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) {
|
||||
|
||||
try {
|
||||
Class intentClass = getMainActivityClass();
|
||||
if (intentClass == null) {
|
||||
Log.e(LOG_TAG, "No activity class found for the notification");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle.getBoolean("isConversation")) {
|
||||
this.handleConversation(bundle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle.getString("message") == null) {
|
||||
// this happens when a 'data' notification is received - we do not synthesize a local notification in this case
|
||||
Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. Cannot send to notification centre because there is no 'message' field in: " + bundle);
|
||||
return;
|
||||
}
|
||||
|
||||
String notificationIdString = bundle.getString("id");
|
||||
if (notificationIdString == null) {
|
||||
Log.e(LOG_TAG, "No notification ID specified for the notification");
|
||||
return;
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
String packageName = context.getPackageName();
|
||||
|
||||
String title = bundle.getString("title");
|
||||
if (title == null) {
|
||||
ApplicationInfo appInfo = context.getApplicationInfo();
|
||||
title = context.getPackageManager().getApplicationLabel(appInfo).toString();
|
||||
}
|
||||
|
||||
int priority = NotificationCompat.PRIORITY_HIGH;
|
||||
final String priorityString = bundle.getString("priority");
|
||||
|
||||
if (priorityString != null) {
|
||||
switch (priorityString.toLowerCase()) {
|
||||
case "max":
|
||||
priority = NotificationCompat.PRIORITY_MAX;
|
||||
break;
|
||||
case "high":
|
||||
priority = NotificationCompat.PRIORITY_HIGH;
|
||||
break;
|
||||
case "low":
|
||||
priority = NotificationCompat.PRIORITY_LOW;
|
||||
break;
|
||||
case "min":
|
||||
priority = NotificationCompat.PRIORITY_MIN;
|
||||
break;
|
||||
case "default":
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT;
|
||||
break;
|
||||
default:
|
||||
priority = NotificationCompat.PRIORITY_HIGH;
|
||||
}
|
||||
}
|
||||
|
||||
int visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
final String visibilityString = bundle.getString("visibility");
|
||||
|
||||
if (visibilityString != null) {
|
||||
switch (visibilityString.toLowerCase()) {
|
||||
case "private":
|
||||
visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
break;
|
||||
case "public":
|
||||
visibility = NotificationCompat.VISIBILITY_PUBLIC;
|
||||
break;
|
||||
case "secret":
|
||||
visibility = NotificationCompat.VISIBILITY_SECRET;
|
||||
break;
|
||||
default:
|
||||
visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
}
|
||||
}
|
||||
|
||||
String channel_id = bundle.getString("channelId");
|
||||
|
||||
if(channel_id == null) {
|
||||
channel_id = this.getNotificationDefaultChannelId();
|
||||
}
|
||||
|
||||
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, channel_id)
|
||||
.setContentTitle(title)
|
||||
.setTicker(bundle.getString("ticker"))
|
||||
.setVisibility(visibility)
|
||||
.setPriority(priority)
|
||||
.setAutoCancel(bundle.getBoolean("autoCancel", true))
|
||||
.setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // API 24 and higher
|
||||
// Restore showing timestamp on Android 7+
|
||||
// Source: https://developer.android.com/reference/android/app/Notification.Builder.html#setShowWhen(boolean)
|
||||
boolean showWhen = bundle.getBoolean("showWhen", true);
|
||||
|
||||
notification.setShowWhen(showWhen);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26 and higher
|
||||
// Changing Default mode of notification
|
||||
notification.setDefaults(Notification.DEFAULT_LIGHTS);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { // API 20 and higher
|
||||
String group = bundle.getString("group");
|
||||
|
||||
if (group != null) {
|
||||
notification.setGroup(group);
|
||||
}
|
||||
|
||||
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
|
||||
notification.setGroupSummary(bundle.getBoolean("groupSummary"));
|
||||
}
|
||||
}
|
||||
|
||||
String numberString = bundle.getString("number");
|
||||
|
||||
if (numberString != null) {
|
||||
notification.setNumber(Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
// Small icon
|
||||
int smallIconResId = 0;
|
||||
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
|
||||
if (smallIcon != null && !smallIcon.isEmpty()) {
|
||||
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
|
||||
} else if(smallIcon == null) {
|
||||
smallIconResId = res.getIdentifier("ic_stat_notify_status", "drawable", packageName);
|
||||
}
|
||||
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = android.R.drawable.ic_dialog_info;
|
||||
}
|
||||
}
|
||||
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
// Large icon
|
||||
if(largeIconBitmap == null) {
|
||||
int largeIconResId = 0;
|
||||
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
|
||||
if (largeIcon != null && !largeIcon.isEmpty()) {
|
||||
largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
|
||||
} else if(largeIcon == null) {
|
||||
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
}
|
||||
|
||||
// Before Lolipop there was no large icon for notifications.
|
||||
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
}
|
||||
}
|
||||
|
||||
Bundle author = bundle.getBundle("notificationAuthor");
|
||||
|
||||
if (largeIconBitmap == null && author != null) {
|
||||
String base64Image = author.getString("icon").split(",")[1];
|
||||
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
|
||||
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
||||
notification.setLargeIcon(getCircleBitmap(decodedByte));
|
||||
} else if (largeIconBitmap != null){
|
||||
notification.setLargeIcon(largeIconBitmap);
|
||||
}
|
||||
|
||||
String message = bundle.getString("message");
|
||||
|
||||
notification.setContentText(message);
|
||||
|
||||
String subText = bundle.getString("subText");
|
||||
|
||||
if (subText != null) {
|
||||
notification.setSubText(subText);
|
||||
}
|
||||
|
||||
String bigText = bundle.getString("bigText");
|
||||
|
||||
if (bigText == null) {
|
||||
bigText = message;
|
||||
}
|
||||
|
||||
NotificationCompat.Style style;
|
||||
|
||||
if(bigPictureBitmap != null) {
|
||||
style = new NotificationCompat.BigPictureStyle()
|
||||
.bigPicture(bigPictureBitmap)
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(message);
|
||||
} else {
|
||||
style = new NotificationCompat.BigTextStyle().bigText(bigText);
|
||||
}
|
||||
|
||||
notification.setStyle(style);
|
||||
|
||||
Intent intent = new Intent(context, intentClass);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
bundle.putBoolean("foreground", this.isApplicationInForeground());
|
||||
bundle.putBoolean("userInteraction", true);
|
||||
intent.putExtra("notification", bundle);
|
||||
|
||||
Uri soundUri = null;
|
||||
|
||||
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
|
||||
String soundName = bundle.getString("soundName");
|
||||
|
||||
if (soundName == null) {
|
||||
soundName = "default";
|
||||
}
|
||||
|
||||
soundUri = getSoundUri(soundName);
|
||||
|
||||
notification.setSound(soundUri);
|
||||
}
|
||||
|
||||
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setSound(null);
|
||||
}
|
||||
|
||||
if (bundle.containsKey("ongoing") || bundle.getBoolean("ongoing")) {
|
||||
notification.setOngoing(bundle.getBoolean("ongoing"));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
notification.setCategory(NotificationCompat.CATEGORY_CALL);
|
||||
|
||||
String color = bundle.getString("color");
|
||||
int defaultColor = -1;
|
||||
if (color != null) {
|
||||
notification.setColor(Color.parseColor(color));
|
||||
} else if (defaultColor != -1) {
|
||||
notification.setColor(defaultColor);
|
||||
}
|
||||
}
|
||||
|
||||
int notificationID = notificationIdString.hashCode();
|
||||
|
||||
notification.setContentIntent(createOnTapIntent(context, notificationID, bundle.getString("deepLink")))
|
||||
.setDeleteIntent(createOnDismissedIntent(context, notificationID, bundle.getString("deepLink")));
|
||||
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
long[] vibratePattern = new long[]{0};
|
||||
|
||||
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
|
||||
long vibration = bundle.containsKey("vibration") ? (long) bundle.getDouble("vibration") : DEFAULT_VIBRATION;
|
||||
if (vibration == 0)
|
||||
vibration = DEFAULT_VIBRATION;
|
||||
|
||||
vibratePattern = new long[]{0, vibration};
|
||||
|
||||
notification.setVibrate(vibratePattern);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Define the shortcutId
|
||||
String shortcutId = bundle.getString("shortcutId");
|
||||
|
||||
if (shortcutId != null) {
|
||||
notification.setShortcutId(shortcutId);
|
||||
}
|
||||
|
||||
Long timeoutAfter = (long) bundle.getDouble("timeoutAfter");
|
||||
|
||||
if (timeoutAfter != null && timeoutAfter >= 0) {
|
||||
notification.setTimeoutAfter(timeoutAfter);
|
||||
}
|
||||
}
|
||||
|
||||
Long when = (long) bundle.getDouble("when");
|
||||
|
||||
if (when != null && when >= 0) {
|
||||
notification.setWhen(when);
|
||||
}
|
||||
|
||||
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
|
||||
notification.setChannelId(channel_id);
|
||||
|
||||
JSONArray actionsArray = null;
|
||||
try {
|
||||
actionsArray = bundle.getString("actions") != null ? new JSONArray(bundle.getString("actions")) : null;
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Exception while converting actions to JSON object.", e);
|
||||
}
|
||||
|
||||
if (actionsArray != null) {
|
||||
// No icon for now. The icon value of 0 shows no icon.
|
||||
int icon = 0;
|
||||
|
||||
// Add button for each actions.
|
||||
for (int i = 0; i < actionsArray.length(); i++) {
|
||||
String action;
|
||||
try {
|
||||
action = actionsArray.getString(i);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Exception while getting action from actionsArray.", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Intent actionIntent = new Intent(context, PushNotificationActions.class);
|
||||
actionIntent.setAction(packageName + ".ACTION_" + i);
|
||||
|
||||
actionIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
// Add "action" for later identifying which button gets pressed.
|
||||
bundle.putString("action", action);
|
||||
actionIntent.putExtra("notification", bundle);
|
||||
actionIntent.setPackage(packageName);
|
||||
|
||||
PendingIntent pendingActionIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notification.addAction(new NotificationCompat.Action.Builder(icon, action, pendingActionIntent).build());
|
||||
} else {
|
||||
notification.addAction(icon, action, pendingActionIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!(this.isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
|
||||
Notification info = notification.build();
|
||||
info.defaults |= Notification.DEFAULT_LIGHTS;
|
||||
|
||||
if (bundle.containsKey("tag")) {
|
||||
String tag = bundle.getString("tag");
|
||||
notificationManager.notify(tag, notificationID, info);
|
||||
} else {
|
||||
notificationManager.notify(notificationID, info);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "failed to send push notification", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkOrCreateChannel(NotificationManager manager, String channel_id, String channel_name, String channel_description, Uri soundUri, int importance, long[] vibratePattern, boolean showBadge) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return false;
|
||||
if (manager == null)
|
||||
return false;
|
||||
|
||||
NotificationChannel channel = manager.getNotificationChannel(channel_id);
|
||||
|
||||
if (
|
||||
channel == null && channel_name != null && channel_description != null ||
|
||||
channel != null &&
|
||||
(
|
||||
channel_name != null && !channel.getName().equals(channel_name) ||
|
||||
channel_description != null && !channel.getDescription().equals(channel_description)
|
||||
)
|
||||
) {
|
||||
// If channel doesn't exist create a new one.
|
||||
// If channel name or description is updated then update the existing channel.
|
||||
channel = new NotificationChannel(channel_id, channel_name, importance);
|
||||
|
||||
channel.setDescription(channel_description);
|
||||
channel.enableLights(true);
|
||||
channel.enableVibration(vibratePattern != null);
|
||||
channel.setVibrationPattern(vibratePattern);
|
||||
channel.setShowBadge(showBadge);
|
||||
|
||||
if (soundUri != null) {
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build();
|
||||
|
||||
channel.setSound(soundUri, audioAttributes);
|
||||
} else {
|
||||
channel.setSound(null, null);
|
||||
}
|
||||
|
||||
manager.createNotificationChannel(channel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean createChannel(ReadableMap channelInfo) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return false;
|
||||
|
||||
String channelId = channelInfo.getString("channelId");
|
||||
String channelName = channelInfo.getString("channelName");
|
||||
String channelDescription = channelInfo.hasKey("channelDescription") ? channelInfo.getString("channelDescription") : "";
|
||||
String soundName = channelInfo.hasKey("soundName") ? channelInfo.getString("soundName") : "default";
|
||||
int importance = channelInfo.hasKey("importance") ? channelInfo.getInt("importance") : 4;
|
||||
boolean vibrate = channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate");
|
||||
long[] vibratePattern = vibrate ? new long[] { DEFAULT_VIBRATION } : null;
|
||||
boolean showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge");
|
||||
|
||||
NotificationManager manager = notificationManager();
|
||||
|
||||
Uri soundUri = getSoundUri(soundName);
|
||||
|
||||
return checkOrCreateChannel(manager, channelId, channelName, channelDescription, soundUri, importance, vibratePattern, showBadge);
|
||||
}
|
||||
|
||||
public String getNotificationDefaultChannelId() {
|
||||
return this.CHANNEL_ID;
|
||||
}
|
||||
|
||||
private Uri getSoundUri(String soundName) {
|
||||
if (soundName == null || "default".equalsIgnoreCase(soundName)) {
|
||||
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
} else {
|
||||
|
||||
// sound name can be full filename, or just the resource name.
|
||||
// So the strings 'my_sound.mp3' AND 'my_sound' are accepted
|
||||
// The reason is to make the iOS and android javascript interfaces compatible
|
||||
|
||||
int resId;
|
||||
if (context.getResources().getIdentifier(soundName, "raw", context.getPackageName()) != 0) {
|
||||
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
|
||||
} else {
|
||||
soundName = soundName.substring(0, soundName.lastIndexOf('.'));
|
||||
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
|
||||
}
|
||||
|
||||
return Uri.parse("android.resource://" + context.getPackageName() + "/" + resId);
|
||||
}
|
||||
}
|
||||
|
||||
public Class getMainActivityClass() {
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isApplicationInForeground() {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
|
||||
if (processInfos != null) {
|
||||
for (RunningAppProcessInfo processInfo : processInfos) {
|
||||
if (processInfo.processName.equals(context.getPackageName())
|
||||
&& processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
&& processInfo.pkgList.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Bitmap getCircleBitmap(Bitmap bitmap) {
|
||||
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
|
||||
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(output);
|
||||
|
||||
final int color = Color.RED;
|
||||
final Paint paint = new Paint();
|
||||
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
final RectF rectF = new RectF(rect);
|
||||
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawARGB(0, 0, 0, 0);
|
||||
paint.setColor(color);
|
||||
canvas.drawOval(rectF, paint);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint);
|
||||
|
||||
bitmap.recycle();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private Person getPerson(Bundle bundle) {
|
||||
String name = bundle.getString("name");
|
||||
|
||||
return new Person.Builder().setName(name).build();
|
||||
}
|
||||
|
||||
private StatusMessage createMessage(Bundle data) {
|
||||
Person author = getPerson(data.getBundle("notificationAuthor"));
|
||||
long timeStampLongValue = (long) data.getDouble("timestamp");
|
||||
return new StatusMessage(data.getString("id"), author, timeStampLongValue, data.getString("message"));
|
||||
}
|
||||
|
||||
private PendingIntent createGroupOnDismissedIntent(Context context, int notificationId, String groupId, String deepLink) {
|
||||
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink);
|
||||
intent.putExtra("im.status.ethereum.groupId", groupId);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createGroupOnTapIntent(Context context, int notificationId, String groupId, String deepLink) {
|
||||
Intent intent = getOpenAppIntent(deepLink);
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createOnTapIntent(Context context, int notificationId, String deepLink) {
|
||||
Intent intent = getOpenAppIntent(deepLink);
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createOnDismissedIntent(Context context, int notificationId, String deepLink) {
|
||||
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
public void removeStatusMessage(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
if (group == null) {
|
||||
group = new StatusMessageGroup(conversationId);
|
||||
}
|
||||
|
||||
this.messageGroups.put(conversationId, group);
|
||||
|
||||
String id = bundle.getString("id");
|
||||
group.removeMessage(id);
|
||||
|
||||
this.showMessages(bundle);
|
||||
}
|
||||
|
||||
public StatusMessageGroup getMessageGroup(String conversationId) {
|
||||
return this.messageGroups.get(conversationId);
|
||||
}
|
||||
|
||||
public void addStatusMessage(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
if (group == null) {
|
||||
group = new StatusMessageGroup(conversationId);
|
||||
}
|
||||
|
||||
this.messageGroups.put(conversationId, group);
|
||||
|
||||
group.addMessage(createMessage(bundle));
|
||||
|
||||
this.showMessages(bundle);
|
||||
}
|
||||
|
||||
public void showMessages(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
ArrayList<StatusMessage> messages = group.getMessages();
|
||||
|
||||
if (messages.size() == 0) {
|
||||
notificationManager.cancel(conversationId.hashCode());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < messages.size(); i++) {
|
||||
StatusMessage message = messages.get(i);
|
||||
messagingStyle.addMessage(message.getText(),
|
||||
message.getTimestamp(),
|
||||
message.getAuthor());
|
||||
}
|
||||
|
||||
if (bundle.getString("title") != null) {
|
||||
messagingStyle.setConversationTitle(bundle.getString("title"));
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(messagingStyle)
|
||||
.setGroup(conversationId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setNumber(messages.size() + 1)
|
||||
.setAutoCancel(true);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
builder.setVibrate(new long[0]);
|
||||
}
|
||||
notificationManager.notify(conversationId.hashCode(), builder.build());
|
||||
}
|
||||
|
||||
class StatusMessageGroup {
|
||||
private ArrayList<StatusMessage> messages;
|
||||
private String id;
|
||||
|
||||
StatusMessageGroup(String id) {
|
||||
this.id = id;
|
||||
this.messages = new ArrayList<StatusMessage>();
|
||||
}
|
||||
public ArrayList<StatusMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void addMessage(StatusMessage message) {
|
||||
this.messages.add(message);
|
||||
}
|
||||
|
||||
public void removeMessage(String id) {
|
||||
ArrayList<StatusMessage> newMessages = new ArrayList<StatusMessage>();
|
||||
for(StatusMessage message: this.messages) {
|
||||
if(!message.id.equals(id)) {
|
||||
newMessages.add(message);
|
||||
}
|
||||
}
|
||||
this.messages = newMessages;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
class StatusMessage {
|
||||
public Person getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
private String id;
|
||||
private Person author;
|
||||
private long timestamp;
|
||||
private String text;
|
||||
|
||||
StatusMessage(String id, Person author, long timestamp, String text) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.timestamp = timestamp;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeGroup(String groupId) {
|
||||
this.messageGroups.remove(groupId);
|
||||
}
|
||||
|
||||
private void cleanGroup(String groupId) {
|
||||
removeGroup(groupId);
|
||||
if (messageGroups.size() == 0) {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Log.e(LOG_TAG, "Starting Foreground Service");
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.startService(serviceIntent);
|
||||
this.registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Log.e(LOG_TAG, "Stopping Foreground Service");
|
||||
//NOTE: we cancel all the current notifications, because the intents can't be used anymore
|
||||
//since the broadcast receiver will be killed as well and won't be able to handle any intent
|
||||
notificationManager.cancelAll();
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.stopService(serviceIntent);
|
||||
context.unregisterReceiver(notificationActionReceiver);
|
||||
}
|
||||
}
|
||||
-771
@@ -1,771 +0,0 @@
|
||||
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.media.AudioAttributes
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import android.util.Base64
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import im.status.ethereum.module.R
|
||||
|
||||
class PushNotificationHelper(context: Application, intentFilter: IntentFilter) {
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
private const val DEFAULT_VIBRATION: Long = 300L
|
||||
private const val CHANNEL_ID = "status-im-notifications"
|
||||
const val ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION"
|
||||
const val ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP"
|
||||
}
|
||||
|
||||
private val context: Context
|
||||
val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_CANCEL_CURRENT
|
||||
private val notificationManager: NotificationManager
|
||||
private val persons: HashMap<String, Person>
|
||||
private val messageGroups: HashMap<String, StatusMessageGroup>
|
||||
private val intentFilter: IntentFilter
|
||||
fun getOpenAppIntent(deepLink: String?): Intent? {
|
||||
val intentClass: java.lang.Class<*>
|
||||
val packageName: String = context.getPackageName()
|
||||
val launchIntent: Intent? = context.getPackageManager().getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent == null) {
|
||||
return null
|
||||
}
|
||||
val className: String? = launchIntent.getComponent()?.getClassName()
|
||||
if (className == null) {
|
||||
return null
|
||||
}
|
||||
intentClass = try {
|
||||
java.lang.Class.forName(className)
|
||||
} catch (e: java.lang.ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
val intent = Intent(context, intentClass)
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
intent.setAction(Intent.ACTION_VIEW)
|
||||
//NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well,
|
||||
//the answer is a simple as it can get in the Android native development world. I noticed
|
||||
//that my initial setup was opening the app but wasn't triggering any events on the js side, like
|
||||
//the links do from the browser. So I compared both intents and noticed that the link from
|
||||
//the browser produces an intent with the flag 0x14000000. I found out that it was the following
|
||||
//flags in this link:
|
||||
//https://stackoverflow.com/questions/52390129/android-intent-setflags-issue
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
if (deepLink != null) {
|
||||
intent.setData(Uri.parse(deepLink))
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
//NOTE: we use a dynamically created BroadcastReceiver here so that we can capture
|
||||
//intents from notifications and act on them. For instance when tapping/dismissing
|
||||
//a chat notification we want to clear the chat so that next messages don't show
|
||||
//the messages that we have seen again
|
||||
private val notificationActionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent) {
|
||||
if (intent.getAction() === ACTION_DELETE_NOTIFICATION) {
|
||||
val groupId: String? = intent.getExtras()?.getString("im.status.ethereum.groupId")
|
||||
if (groupId != null) {
|
||||
cleanGroup(groupId)
|
||||
}
|
||||
}
|
||||
if (intent.getAction() === ACTION_TAP_STOP) {
|
||||
stop()
|
||||
java.lang.System.exit(0)
|
||||
}
|
||||
Log.e(LOG_TAG, "intent received: " + intent.getAction())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
this.context = context
|
||||
this.intentFilter = intentFilter
|
||||
persons = HashMap<String, Person>()
|
||||
messageGroups = HashMap<String, StatusMessageGroup>()
|
||||
notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
registerBroadcastReceiver()
|
||||
}
|
||||
|
||||
fun registerBroadcastReceiver() {
|
||||
intentFilter.addAction(ACTION_DELETE_NOTIFICATION)
|
||||
intentFilter.addAction(ACTION_TAP_STOP)
|
||||
context.registerReceiver(notificationActionReceiver, intentFilter)
|
||||
Log.e(LOG_TAG, "Broadcast Receiver registered")
|
||||
}
|
||||
|
||||
private fun notificationManager(): NotificationManager {
|
||||
return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
fun invokeApp(bundle: Bundle?) {
|
||||
val packageName: String = context.getPackageName()
|
||||
val launchIntent: Intent? = context.getPackageManager().getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent == null) {
|
||||
return
|
||||
}
|
||||
val className: String? = launchIntent.getComponent()?.getClassName()
|
||||
if (className == null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val activityClass: java.lang.Class<*> = java.lang.Class.forName(className)
|
||||
val activityIntent = Intent(context, activityClass)
|
||||
if (bundle != null) {
|
||||
activityIntent.putExtra("notification", bundle)
|
||||
}
|
||||
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(activityIntent)
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(LOG_TAG, "Class not found", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun sendToNotificationCentre(bundle: Bundle) {
|
||||
val aggregator = PushNotificationPicturesAggregator(object : PushNotificationPicturesAggregator.Callback {
|
||||
override fun call(largeIconImage: Bitmap?, bigPictureImage: Bitmap?) {
|
||||
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage)
|
||||
}
|
||||
})
|
||||
aggregator.setLargeIconUrl(context, bundle.getString("largeIconUrl"))
|
||||
aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"))
|
||||
}
|
||||
|
||||
fun handleConversation(bundle: Bundle) {
|
||||
if (bundle.getBoolean("deleted")) {
|
||||
removeStatusMessage(bundle)
|
||||
} else {
|
||||
addStatusMessage(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMessageNotifications(conversationId: String) {
|
||||
notificationManager.cancel(conversationId.hashCode())
|
||||
cleanGroup(conversationId)
|
||||
}
|
||||
|
||||
fun clearAllMessageNotifications() {
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
|
||||
fun sendToNotificationCentreWithPicture(bundle: Bundle, largeIconBitmap: Bitmap?, bigPictureBitmap: Bitmap?) {
|
||||
var largeIconBitmap: Bitmap? = largeIconBitmap
|
||||
try {
|
||||
val intentClass: java.lang.Class<*>? = mainActivityClass
|
||||
if (intentClass == null) {
|
||||
Log.e(LOG_TAG, "No activity class found for the notification")
|
||||
return
|
||||
}
|
||||
if (bundle.getBoolean("isConversation")) {
|
||||
handleConversation(bundle)
|
||||
return
|
||||
}
|
||||
if (bundle.getString("message") == null) {
|
||||
// this happens when a 'data' notification is received - we do not synthesize a local notification in this case
|
||||
Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. Cannot send to notification centre because there is no 'message' field in: $bundle")
|
||||
return
|
||||
}
|
||||
val notificationIdString: String? = bundle.getString("id")
|
||||
if (notificationIdString == null) {
|
||||
Log.e(LOG_TAG, "No notification ID specified for the notification")
|
||||
return
|
||||
}
|
||||
val res: Resources = context.getResources()
|
||||
val packageName: String = context.getPackageName()
|
||||
var title: String? = bundle.getString("title")
|
||||
if (title == null) {
|
||||
val appInfo: ApplicationInfo = context.getApplicationInfo()
|
||||
title = context.getPackageManager().getApplicationLabel(appInfo).toString()
|
||||
}
|
||||
var priority: Int = NotificationCompat.PRIORITY_HIGH
|
||||
val priorityString: String? = bundle.getString("priority")
|
||||
if (priorityString != null) {
|
||||
priority = when (priorityString.lowercase()) {
|
||||
"max" -> NotificationCompat.PRIORITY_MAX
|
||||
"high" -> NotificationCompat.PRIORITY_HIGH
|
||||
"low" -> NotificationCompat.PRIORITY_LOW
|
||||
"min" -> NotificationCompat.PRIORITY_MIN
|
||||
"default" -> NotificationCompat.PRIORITY_DEFAULT
|
||||
else -> NotificationCompat.PRIORITY_HIGH
|
||||
}
|
||||
}
|
||||
var visibility: Int = NotificationCompat.VISIBILITY_PRIVATE
|
||||
val visibilityString: String? = bundle.getString("visibility")
|
||||
if (visibilityString != null) {
|
||||
visibility = when (visibilityString.lowercase()) {
|
||||
"private" -> NotificationCompat.VISIBILITY_PRIVATE
|
||||
"public" -> NotificationCompat.VISIBILITY_PUBLIC
|
||||
"secret" -> NotificationCompat.VISIBILITY_SECRET
|
||||
else -> NotificationCompat.VISIBILITY_PRIVATE
|
||||
}
|
||||
}
|
||||
var channel_id: String? = bundle.getString("channelId")
|
||||
if (channel_id == null) {
|
||||
channel_id = notificationDefaultChannelId
|
||||
}
|
||||
val notification: NotificationCompat.Builder = NotificationCompat.Builder(context, channel_id)
|
||||
.setContentTitle(title)
|
||||
.setTicker(bundle.getString("ticker"))
|
||||
.setVisibility(visibility)
|
||||
.setPriority(priority)
|
||||
.setAutoCancel(bundle.getBoolean("autoCancel", true))
|
||||
.setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // API 24 and higher
|
||||
// Restore showing timestamp on Android 7+
|
||||
// Source: https://developer.android.com/reference/android/app/Notification.Builder.html#setShowWhen(boolean)
|
||||
val showWhen: Boolean = bundle.getBoolean("showWhen", true)
|
||||
notification.setShowWhen(showWhen)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26 and higher
|
||||
// Changing Default mode of notification
|
||||
notification.setDefaults(Notification.DEFAULT_LIGHTS)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { // API 20 and higher
|
||||
val group: String? = bundle.getString("group")
|
||||
if (group != null) {
|
||||
notification.setGroup(group)
|
||||
}
|
||||
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
|
||||
notification.setGroupSummary(bundle.getBoolean("groupSummary"))
|
||||
}
|
||||
}
|
||||
val numberString: String? = bundle.getString("number")
|
||||
if (numberString != null) {
|
||||
notification.setNumber(numberString.toInt())
|
||||
}
|
||||
|
||||
// Small icon
|
||||
var smallIconResId = 0
|
||||
val smallIcon: String? = bundle.getString("smallIcon")
|
||||
if (smallIcon != null && !smallIcon.isEmpty()) {
|
||||
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName)
|
||||
} else if (smallIcon == null) {
|
||||
smallIconResId = res.getIdentifier("ic_stat_notify_status", "drawable", packageName)
|
||||
}
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName)
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = android.R.drawable.ic_dialog_info
|
||||
}
|
||||
}
|
||||
notification.setSmallIcon(smallIconResId)
|
||||
|
||||
// Large icon
|
||||
if (largeIconBitmap == null) {
|
||||
var largeIconResId = 0
|
||||
val largeIcon: String? = bundle.getString("largeIcon")
|
||||
if (largeIcon != null && !largeIcon.isEmpty()) {
|
||||
largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName)
|
||||
} else if (largeIcon == null) {
|
||||
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName)
|
||||
}
|
||||
|
||||
// Before Lolipop there was no large icon for notifications.
|
||||
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId)
|
||||
}
|
||||
}
|
||||
val author: Bundle? = bundle.getBundle("notificationAuthor")
|
||||
if (largeIconBitmap == null && author != null) {
|
||||
val base64Image: String? = author.getString("icon")?.split(",")?.get(1)
|
||||
if (base64Image != null) {
|
||||
val decodedString: ByteArray = Base64.decode(base64Image, Base64.DEFAULT)
|
||||
val decodedByte: Bitmap = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
|
||||
notification.setLargeIcon(getCircleBitmap(decodedByte))
|
||||
}
|
||||
} else if (largeIconBitmap != null) {
|
||||
notification.setLargeIcon(largeIconBitmap)
|
||||
}
|
||||
val message: String? = bundle.getString("message")
|
||||
if (message != null) {
|
||||
notification.setContentText(message)
|
||||
}
|
||||
val subText: String? = bundle.getString("subText")
|
||||
if (subText != null) {
|
||||
notification.setSubText(subText)
|
||||
}
|
||||
var bigText: String? = bundle.getString("bigText")
|
||||
if (bigText == null) {
|
||||
bigText = message
|
||||
}
|
||||
val style: NotificationCompat.Style
|
||||
style = if (bigPictureBitmap != null) {
|
||||
NotificationCompat.BigPictureStyle()
|
||||
.bigPicture(bigPictureBitmap)
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(message)
|
||||
} else {
|
||||
NotificationCompat.BigTextStyle().bigText(bigText)
|
||||
}
|
||||
notification.setStyle(style)
|
||||
val intent = Intent(context, intentClass)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
bundle.putBoolean("foreground", isApplicationInForeground)
|
||||
bundle.putBoolean("userInteraction", true)
|
||||
intent.putExtra("notification", bundle)
|
||||
var soundUri: Uri? = null
|
||||
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
|
||||
var soundName: String? = bundle.getString("soundName")
|
||||
if (soundName == null) {
|
||||
soundName = "default"
|
||||
}
|
||||
soundUri = getSoundUri(soundName)
|
||||
notification.setSound(soundUri)
|
||||
}
|
||||
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setSound(null)
|
||||
}
|
||||
if (bundle.containsKey("ongoing") || bundle.getBoolean("ongoing")) {
|
||||
notification.setOngoing(bundle.getBoolean("ongoing"))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
notification.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
val color: String? = bundle.getString("color")
|
||||
val defaultColor = -1
|
||||
if (color != null) {
|
||||
notification.setColor(Color.parseColor(color))
|
||||
} else if (defaultColor != -1) {
|
||||
notification.setColor(defaultColor)
|
||||
}
|
||||
}
|
||||
val notificationID = notificationIdString.hashCode()
|
||||
var deepLink: String? = bundle.getString("deepLink")
|
||||
if (deepLink != null) {
|
||||
notification
|
||||
.setContentIntent(createOnTapIntent(context, notificationID, deepLink))
|
||||
.setDeleteIntent(createOnDismissedIntent(context, notificationID, deepLink))
|
||||
}
|
||||
val notificationManager: NotificationManager = notificationManager()
|
||||
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
|
||||
var vibration = if (bundle.containsKey("vibration")) bundle.getDouble("vibration") as Long else DEFAULT_VIBRATION
|
||||
if (vibration == 0L) {
|
||||
vibration = DEFAULT_VIBRATION
|
||||
}
|
||||
val vibrationPattern: LongArray = longArrayOf(0L, vibration)
|
||||
notification.setVibrate(vibrationPattern)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Define the shortcutId
|
||||
val shortcutId: String? = bundle.getString("shortcutId")
|
||||
if (shortcutId != null) {
|
||||
notification.setShortcutId(shortcutId)
|
||||
}
|
||||
val timeoutAfter = bundle.getDouble("timeoutAfter") as Long
|
||||
if (timeoutAfter != null && timeoutAfter >= 0) {
|
||||
notification.setTimeoutAfter(timeoutAfter)
|
||||
}
|
||||
}
|
||||
val `when` = bundle.getDouble("when") as Long
|
||||
if (`when` != null && `when` >= 0) {
|
||||
notification.setWhen(`when`)
|
||||
}
|
||||
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false))
|
||||
notification.setChannelId(channel_id)
|
||||
var actionsArray: JSONArray? = null
|
||||
try {
|
||||
actionsArray = if (bundle.getString("actions") != null) JSONArray(bundle.getString("actions")) else null
|
||||
} catch (e: JSONException) {
|
||||
Log.e(LOG_TAG, "Exception while converting actions to JSON object.", e)
|
||||
}
|
||||
if (actionsArray != null) {
|
||||
// No icon for now. The icon value of 0 shows no icon.
|
||||
val icon = 0
|
||||
|
||||
// Add button for each actions.
|
||||
for (i in 0 until actionsArray.length()) {
|
||||
var action: String
|
||||
action = try {
|
||||
actionsArray.getString(i)
|
||||
} catch (e: JSONException) {
|
||||
Log.e(LOG_TAG, "Exception while getting action from actionsArray.", e)
|
||||
continue
|
||||
}
|
||||
val actionIntent = Intent(context, PushNotificationActions::class.java)
|
||||
actionIntent.setAction("$packageName.ACTION_$i")
|
||||
actionIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
|
||||
// Add "action" for later identifying which button gets pressed.
|
||||
bundle.putString("action", action)
|
||||
actionIntent.putExtra("notification", bundle)
|
||||
actionIntent.setPackage(packageName)
|
||||
val pendingActionIntent: PendingIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notification.addAction(NotificationCompat.Action.Builder(icon, action, pendingActionIntent).build())
|
||||
} else {
|
||||
notification.addAction(icon, action, pendingActionIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!(isApplicationInForeground && bundle.getBoolean("ignoreInForeground"))) {
|
||||
val info: Notification = notification.build()
|
||||
info.defaults = info.defaults or Notification.DEFAULT_LIGHTS
|
||||
if (bundle.containsKey("tag")) {
|
||||
val tag: String? = bundle.getString("tag")
|
||||
if (tag != null) {
|
||||
notificationManager.notify(tag, notificationID, info)
|
||||
}
|
||||
} else {
|
||||
notificationManager.notify(notificationID, info)
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(LOG_TAG, "failed to send push notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOrCreateChannel(manager: NotificationManager?, channel_id: String, channel_name: String?, channel_description: String?, soundUri: Uri?, importance: Int, vibratePattern: LongArray?, showBadge: Boolean): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
|
||||
if (manager == null) return false
|
||||
var channel: NotificationChannel? = manager.getNotificationChannel(channel_id)
|
||||
if (channel == null && channel_name != null && channel_description != null ||
|
||||
channel != null &&
|
||||
(channel_name != null && !channel.getName().equals(channel_name) ||
|
||||
channel_description != null && !channel.getDescription().equals(channel_description))) {
|
||||
// If channel doesn't exist create a new one.
|
||||
// If channel name or description is updated then update the existing channel.
|
||||
channel = NotificationChannel(channel_id, channel_name, importance)
|
||||
channel.setDescription(channel_description)
|
||||
channel.enableLights(true)
|
||||
channel.enableVibration(vibratePattern != null)
|
||||
channel.setVibrationPattern(vibratePattern)
|
||||
channel.setShowBadge(showBadge)
|
||||
if (soundUri != null) {
|
||||
val audioAttributes: AudioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
channel.setSound(soundUri, audioAttributes)
|
||||
} else {
|
||||
channel.setSound(null, null)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun createChannel(channelInfo: ReadableMap): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return false
|
||||
}
|
||||
val channelId: String? = channelInfo.getString("channelId")
|
||||
val channelName: String? = channelInfo.getString("channelName")
|
||||
val channelDescription = if (channelInfo.hasKey("channelDescription")) channelInfo.getString("channelDescription") else ""
|
||||
val soundName = if (channelInfo.hasKey("soundName")) channelInfo.getString("soundName") else "default"
|
||||
val importance = if (channelInfo.hasKey("importance")) channelInfo.getInt("importance") else 4
|
||||
val vibrate = channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate")
|
||||
val vibratePattern = if (vibrate) longArrayOf(DEFAULT_VIBRATION) else null
|
||||
val showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge")
|
||||
val manager: NotificationManager = notificationManager()
|
||||
val soundUri: Uri? = if (soundName != null) getSoundUri(soundName) else null
|
||||
if (channelId != null) {
|
||||
return checkOrCreateChannel(manager, channelId, channelName, channelDescription, soundUri, importance, vibratePattern, showBadge)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val notificationDefaultChannelId: String
|
||||
get() = CHANNEL_ID
|
||||
|
||||
private fun getSoundUri(soundName: String): Uri {
|
||||
var soundName: String? = soundName
|
||||
return if (soundName == null || "default".equals(soundName, ignoreCase = true)) {
|
||||
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||
} else {
|
||||
|
||||
// sound name can be full filename, or just the resource name.
|
||||
// So the strings 'my_sound.mp3' AND 'my_sound' are accepted
|
||||
// The reason is to make the iOS and android javascript interfaces compatible
|
||||
val resId: Int
|
||||
if (context.getResources().getIdentifier(soundName, "raw", context.getPackageName()) !== 0) {
|
||||
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName())
|
||||
} else {
|
||||
soundName = soundName.substring(0, soundName.lastIndexOf('.'))
|
||||
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName())
|
||||
}
|
||||
Uri.parse("android.resource://" + context.getPackageName() + "/" + resId)
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityClass: java.lang.Class<*>?
|
||||
get() {
|
||||
val packageName: String = context.getPackageName()
|
||||
val launchIntent: Intent? = context.getPackageManager().getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent == null) {
|
||||
return null
|
||||
}
|
||||
val className: String? = launchIntent.getComponent()?.getClassName()
|
||||
if (className == null) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
java.lang.Class.forName(className)
|
||||
} catch (e: java.lang.ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
val isApplicationInForeground: Boolean
|
||||
get() {
|
||||
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val processInfos: List<RunningAppProcessInfo> = activityManager.getRunningAppProcesses()
|
||||
if (processInfos != null) {
|
||||
for (processInfo in processInfos) {
|
||||
if (processInfo.processName.equals(context.getPackageName()) && processInfo.importance === RunningAppProcessInfo.IMPORTANCE_FOREGROUND && processInfo.pkgList.size > 0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getCircleBitmap(bitmap: Bitmap): Bitmap {
|
||||
val output: Bitmap = Bitmap.createBitmap(bitmap.getWidth(),
|
||||
bitmap.getHeight(), Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
val color: Int = Color.RED
|
||||
val paint = Paint()
|
||||
val rect = Rect(0, 0, bitmap.getWidth(), bitmap.getHeight())
|
||||
val rectF = RectF(rect)
|
||||
paint.setAntiAlias(true)
|
||||
canvas.drawARGB(0, 0, 0, 0)
|
||||
paint.setColor(color)
|
||||
canvas.drawOval(rectF, paint)
|
||||
paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint)
|
||||
bitmap.recycle()
|
||||
return output
|
||||
}
|
||||
|
||||
private fun getPerson(bundle: Bundle?): Person {
|
||||
val builder = Person.Builder()
|
||||
val name: String? = bundle?.getString("name")
|
||||
if (name != null) {
|
||||
builder.setName(name)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createMessage(data: Bundle): StatusMessage {
|
||||
val notificationAuthor: Bundle? = data.getBundle("notificationAuthor")
|
||||
val author: Person = getPerson(notificationAuthor)
|
||||
val timeStampLongValue = data.getDouble("timestamp") as Long
|
||||
val id: String? = data.getString("id")
|
||||
val message: String? = data.getString("message")
|
||||
return StatusMessage(id ?: "", author, timeStampLongValue, message ?: "")
|
||||
}
|
||||
|
||||
private fun createGroupOnDismissedIntent(context: Context, notificationId: Int, groupId: String, deepLink: String?): PendingIntent {
|
||||
val intent = Intent(ACTION_DELETE_NOTIFICATION)
|
||||
if (deepLink != null) {
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink)
|
||||
}
|
||||
intent.putExtra("im.status.ethereum.groupId", groupId)
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag)
|
||||
}
|
||||
|
||||
private fun createGroupOnTapIntent(context: Context, notificationId: Int, groupId: String, deepLink: String?): PendingIntent {
|
||||
val intent: Intent? = getOpenAppIntent(deepLink)
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag)
|
||||
}
|
||||
|
||||
private fun createOnTapIntent(context: Context, notificationId: Int, deepLink: String?): PendingIntent {
|
||||
val intent: Intent? = getOpenAppIntent(deepLink)
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag)
|
||||
}
|
||||
|
||||
private fun createOnDismissedIntent(context: Context, notificationId: Int, deepLink: String): PendingIntent {
|
||||
val intent = Intent(ACTION_DELETE_NOTIFICATION)
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink)
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag)
|
||||
}
|
||||
|
||||
fun removeStatusMessage(bundle: Bundle) {
|
||||
val conversationId: String? = bundle.getString("conversationId")
|
||||
if (conversationId == null) {
|
||||
return
|
||||
}
|
||||
var group: StatusMessageGroup? = if (conversationId != null) messageGroups.get(conversationId) else null
|
||||
val notificationManager: NotificationManager = notificationManager()
|
||||
|
||||
if (group == null) {
|
||||
group = StatusMessageGroup(conversationId)
|
||||
}
|
||||
messageGroups.put(conversationId, group)
|
||||
|
||||
val id: String? = bundle.getString("id")
|
||||
if (id != null) {
|
||||
group.removeMessage(id)
|
||||
}
|
||||
showMessages(bundle)
|
||||
}
|
||||
|
||||
fun getMessageGroup(conversationId: String?): StatusMessageGroup? {
|
||||
return messageGroups.get(conversationId)
|
||||
}
|
||||
|
||||
fun addStatusMessage(bundle: Bundle) {
|
||||
val conversationId: String? = bundle.getString("conversationId")
|
||||
if (conversationId == null) {
|
||||
return
|
||||
}
|
||||
var group: StatusMessageGroup? = if (conversationId != null) messageGroups.get(conversationId) else null
|
||||
val notificationManager: NotificationManager = notificationManager()
|
||||
|
||||
if (group == null) {
|
||||
group = StatusMessageGroup(conversationId)
|
||||
}
|
||||
messageGroups.put(conversationId, group)
|
||||
|
||||
group.addMessage(createMessage(bundle))
|
||||
showMessages(bundle)
|
||||
}
|
||||
|
||||
fun showMessages(bundle: Bundle) {
|
||||
val conversationId: String? = bundle.getString("conversationId")
|
||||
if (conversationId == null) {
|
||||
return
|
||||
}
|
||||
val group: StatusMessageGroup? = if (conversationId != null) messageGroups.get(conversationId) else null
|
||||
val notificationManager: NotificationManager = notificationManager()
|
||||
val messagingStyle: NotificationCompat.MessagingStyle = NotificationCompat.MessagingStyle("Me")
|
||||
var messages = if (group != null) group.getMessages() else null
|
||||
if (messages == null || messages.size == 0) {
|
||||
notificationManager.cancel(conversationId.hashCode())
|
||||
return
|
||||
}
|
||||
for (i in messages.indices) {
|
||||
val message: StatusMessage = messages.get(i)
|
||||
messagingStyle.addMessage(message.text, message.timestamp, message.getAuthor())
|
||||
}
|
||||
val title: String? = bundle.getString("title")
|
||||
if (title != null) {
|
||||
messagingStyle.setConversationTitle(title)
|
||||
}
|
||||
val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(messagingStyle)
|
||||
.setGroup(conversationId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setNumber(messages.size + 1)
|
||||
.setAutoCancel(true)
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
builder.setVibrate(LongArray(0))
|
||||
}
|
||||
notificationManager.notify(conversationId.hashCode(), builder.build())
|
||||
}
|
||||
|
||||
inner class StatusMessageGroup(val id: String) {
|
||||
private var messages: java.util.ArrayList<StatusMessage>
|
||||
|
||||
init {
|
||||
messages = java.util.ArrayList<StatusMessage>()
|
||||
}
|
||||
|
||||
fun getMessages(): java.util.ArrayList<StatusMessage> {
|
||||
return messages
|
||||
}
|
||||
|
||||
fun addMessage(message: StatusMessage) {
|
||||
messages.add(message)
|
||||
}
|
||||
|
||||
fun removeMessage(id: String) {
|
||||
val newMessages: java.util.ArrayList<StatusMessage> = java.util.ArrayList<StatusMessage>()
|
||||
for (message in messages) {
|
||||
if (message.id != id) {
|
||||
newMessages.add(message)
|
||||
}
|
||||
}
|
||||
messages = newMessages
|
||||
}
|
||||
}
|
||||
|
||||
inner class StatusMessage(val id: String, author: Person, timestamp: Long, text: String) {
|
||||
fun getAuthor(): Person {
|
||||
return author
|
||||
}
|
||||
|
||||
private val author: Person
|
||||
val timestamp: Long
|
||||
val text: String
|
||||
|
||||
init {
|
||||
this.author = author
|
||||
this.timestamp = timestamp
|
||||
this.text = text
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeGroup(groupId: String) {
|
||||
messageGroups.remove(groupId)
|
||||
}
|
||||
|
||||
private fun cleanGroup(groupId: String) {
|
||||
removeGroup(groupId)
|
||||
if (messageGroups.size == 0) {
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
Log.e(LOG_TAG, "Starting Foreground Service")
|
||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||
context.startService(serviceIntent)
|
||||
registerBroadcastReceiver()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Log.e(LOG_TAG, "Stopping Foreground Service")
|
||||
//NOTE: we cancel all the current notifications, because the intents can't be used anymore
|
||||
//since the broadcast receiver will be killed as well and won't be able to handle any intent
|
||||
notificationManager.cancelAll()
|
||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||
context.stopService(serviceIntent)
|
||||
context.unregisterReceiver(notificationActionReceiver)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.os.Build;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
class PushNotificationJsDelivery {
|
||||
|
||||
private ReactContext reactContext;
|
||||
|
||||
PushNotificationJsDelivery(ReactContext context){
|
||||
reactContext = context;
|
||||
}
|
||||
|
||||
String convertJSON(Bundle bundle) {
|
||||
try {
|
||||
JSONObject json = convertJSONObject(bundle);
|
||||
return json.toString();
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// a Bundle is not a map, so we have to convert it explicitly
|
||||
private JSONObject convertJSONObject(Bundle bundle) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
Set<String> keys = bundle.keySet();
|
||||
for (String key : keys) {
|
||||
Object value = bundle.get(key);
|
||||
if (value instanceof Bundle) {
|
||||
json.put(key, convertJSONObject((Bundle)value));
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
json.put(key, JSONObject.wrap(value));
|
||||
} else {
|
||||
json.put(key, value);
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
void notifyNotification(Bundle bundle) {
|
||||
String bundleString = convertJSON(bundle);
|
||||
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("dataJSON", bundleString);
|
||||
|
||||
sendEvent("remoteNotificationReceived", params);
|
||||
}
|
||||
|
||||
void notifyNotificationAction(Bundle bundle) {
|
||||
String bundleString = convertJSON(bundle);
|
||||
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("dataJSON", bundleString);
|
||||
|
||||
sendEvent("notificationActionReceived", params);
|
||||
}
|
||||
|
||||
void sendEvent(String eventName, Object params) {
|
||||
if (reactContext.hasActiveCatalystInstance()) {
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.os.Build
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
internal class PushNotificationJsDelivery(context: ReactContext) {
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
}
|
||||
|
||||
private val reactContext: ReactContext
|
||||
|
||||
init {
|
||||
reactContext = context
|
||||
}
|
||||
|
||||
fun convertJSON(bundle: Bundle): String? {
|
||||
return try {
|
||||
val json: JSONObject = convertJSONObject(bundle)
|
||||
json.toString()
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// a Bundle is not a map, so we have to convert it explicitly
|
||||
@kotlin.Throws(JSONException::class)
|
||||
private fun convertJSONObject(bundle: Bundle): JSONObject {
|
||||
val json = JSONObject()
|
||||
val keys: Set<String> = bundle.keySet()
|
||||
for (key in keys) {
|
||||
val value: Any? = bundle.get(key)
|
||||
if (value == null) {
|
||||
continue
|
||||
} else if (value is Bundle) {
|
||||
json.put(key, convertJSONObject(value as Bundle))
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
json.put(key, JSONObject.wrap(value))
|
||||
} else {
|
||||
json.put(key, value)
|
||||
}
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
fun notifyNotification(bundle: Bundle) {
|
||||
val bundleString = convertJSON(bundle)
|
||||
val params: WritableMap = Arguments.createMap()
|
||||
params.putString("dataJSON", bundleString)
|
||||
sendEvent("remoteNotificationReceived", params)
|
||||
}
|
||||
|
||||
fun notifyNotificationAction(bundle: Bundle) {
|
||||
val bundleString = convertJSON(bundle)
|
||||
val params: WritableMap = Arguments.createMap()
|
||||
params.putString("dataJSON", bundleString)
|
||||
sendEvent("notificationActionReceived", params)
|
||||
}
|
||||
|
||||
fun sendEvent(eventName: String, params: Any?) {
|
||||
if (reactContext.hasActiveCatalystInstance()) {
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotification;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PushNotificationPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.<NativeModule>singletonList(new PushNotification(reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotification
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.JavaScriptModule
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
class PushNotificationPackage : ReactPackage {
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||
return listOf<NativeModule>(PushNotification(reactContext))
|
||||
}
|
||||
|
||||
fun createJSModules(): List<java.lang.Class<out JavaScriptModule?>> {
|
||||
return emptyList<java.lang.Class<out JavaScriptModule?>>()
|
||||
}
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.datasource.DataSource;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.common.Priority;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
|
||||
import android.util.Log;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationPicturesAggregator {
|
||||
interface Callback {
|
||||
public void call(Bitmap largeIconImage, Bitmap bigPictureImage);
|
||||
}
|
||||
|
||||
private AtomicInteger count = new AtomicInteger(0);
|
||||
|
||||
private Bitmap largeIconImage;
|
||||
private Bitmap bigPictureImage;
|
||||
|
||||
private Callback callback;
|
||||
|
||||
public PushNotificationPicturesAggregator(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void setBigPicture(Bitmap bitmap) {
|
||||
this.bigPictureImage = bitmap;
|
||||
this.finished();
|
||||
}
|
||||
|
||||
public void setBigPictureUrl(Context context, String url) {
|
||||
if(null == url) {
|
||||
this.setBigPicture(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = null;
|
||||
|
||||
try {
|
||||
uri = Uri.parse(url);
|
||||
} catch(Exception ex) {
|
||||
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex);
|
||||
this.setBigPicture(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final PushNotificationPicturesAggregator aggregator = this;
|
||||
|
||||
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
aggregator.setBigPicture(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
aggregator.setBigPicture(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setLargeIcon(Bitmap bitmap) {
|
||||
this.largeIconImage = bitmap;
|
||||
this.finished();
|
||||
}
|
||||
|
||||
public void setLargeIconUrl(Context context, String url) {
|
||||
if(null == url) {
|
||||
this.setLargeIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = null;
|
||||
|
||||
try {
|
||||
uri = Uri.parse(url);
|
||||
} catch(Exception ex) {
|
||||
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex);
|
||||
this.setLargeIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final PushNotificationPicturesAggregator aggregator = this;
|
||||
|
||||
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
aggregator.setLargeIcon(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
aggregator.setLargeIcon(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void downloadRequest(Context context, Uri uri, BaseBitmapDataSubscriber subscriber) {
|
||||
ImageRequest imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(uri)
|
||||
.setRequestPriority(Priority.HIGH)
|
||||
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
|
||||
.build();
|
||||
|
||||
if(!Fresco.hasBeenInitialized()) {
|
||||
Fresco.initialize(context);
|
||||
}
|
||||
|
||||
DataSource<CloseableReference<CloseableImage>> dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context);
|
||||
|
||||
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance());
|
||||
}
|
||||
|
||||
private void finished() {
|
||||
synchronized(this.count) {
|
||||
int val = this.count.incrementAndGet();
|
||||
|
||||
if(val >= 2 && this.callback != null) {
|
||||
this.callback.call(this.largeIconImage, this.bigPictureImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import com.facebook.common.executors.CallerThreadExecutor
|
||||
import com.facebook.common.references.CloseableReference
|
||||
import com.facebook.datasource.DataSource
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.imagepipeline.common.Priority
|
||||
import com.facebook.imagepipeline.core.ImagePipeline
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
|
||||
import com.facebook.imagepipeline.image.CloseableImage
|
||||
import com.facebook.imagepipeline.request.ImageRequest
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||
import android.util.Log
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class PushNotificationPicturesAggregator(private val callback: Callback?) {
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun call(largeIconImage: Bitmap?, bigPictureImage: Bitmap?)
|
||||
}
|
||||
|
||||
private val count: AtomicInteger = AtomicInteger(0)
|
||||
private var largeIconImage: Bitmap? = null
|
||||
private var bigPictureImage: Bitmap? = null
|
||||
fun setBigPicture(bitmap: Bitmap?) {
|
||||
bigPictureImage = bitmap
|
||||
finished()
|
||||
}
|
||||
|
||||
fun setBigPictureUrl(context: Context, url: String?) {
|
||||
if (null == url) {
|
||||
setBigPicture(null)
|
||||
return
|
||||
}
|
||||
var uri: Uri? = null
|
||||
uri = try {
|
||||
Uri.parse(url)
|
||||
} catch (ex: java.lang.Exception) {
|
||||
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex)
|
||||
setBigPicture(null)
|
||||
return
|
||||
}
|
||||
val aggregator = this
|
||||
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
|
||||
override fun onNewResultImpl(bitmap: Bitmap?) {
|
||||
aggregator.setBigPicture(bitmap)
|
||||
}
|
||||
|
||||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
|
||||
aggregator.setBigPicture(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setLargeIcon(bitmap: Bitmap?) {
|
||||
largeIconImage = bitmap
|
||||
finished()
|
||||
}
|
||||
|
||||
fun setLargeIconUrl(context: Context, url: String?) {
|
||||
if (null == url) {
|
||||
setLargeIcon(null)
|
||||
return
|
||||
}
|
||||
var uri: Uri? = null
|
||||
uri = try {
|
||||
Uri.parse(url)
|
||||
} catch (ex: java.lang.Exception) {
|
||||
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex)
|
||||
setLargeIcon(null)
|
||||
return
|
||||
}
|
||||
val aggregator = this
|
||||
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
|
||||
override fun onNewResultImpl(bitmap: Bitmap?) {
|
||||
aggregator.setLargeIcon(bitmap)
|
||||
}
|
||||
|
||||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
|
||||
aggregator.setLargeIcon(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun downloadRequest(context: Context, uri: Uri?, subscriber: BaseBitmapDataSubscriber) {
|
||||
val imageRequest: ImageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(uri)
|
||||
.setRequestPriority(Priority.HIGH)
|
||||
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
|
||||
.build()
|
||||
if (!Fresco.hasBeenInitialized()) {
|
||||
Fresco.initialize(context)
|
||||
}
|
||||
val dataSource: DataSource<CloseableReference<CloseableImage>> = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
|
||||
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance())
|
||||
}
|
||||
|
||||
private fun finished() {
|
||||
synchronized(count) {
|
||||
val `val`: Int = count.incrementAndGet()
|
||||
if (`val` >= 2 && callback != null) {
|
||||
callback.call(largeIconImage, bigPictureImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,14 @@ SECRETS_ENV_VARS=(
|
||||
'ALCHEMY_ARBITRUM_MAINNET_TOKEN'
|
||||
'ALCHEMY_OPTIMISM_GOERLI_TOKEN'
|
||||
'ALCHEMY_OPTIMISM_MAINNET_TOKEN'
|
||||
'ALCHEMY_ETHEREUM_SEPOLIA_TOKEN'
|
||||
'ALCHEMY_ARBITRUM_SEPOLIA_TOKEN'
|
||||
'ALCHEMY_OPTIMISM_SEPOLIA_TOKEN'
|
||||
'INFURA_TOKEN'
|
||||
'INFURA_TOKEN_SECRET'
|
||||
'OPENSEA_API_KEY'
|
||||
'RARIBLE_MAINNET_API_KEY'
|
||||
'RARIBLE_TESTNET_API_KEY'
|
||||
'POKT_TOKEN'
|
||||
)
|
||||
|
||||
|
||||
+18
-3
@@ -63,10 +63,15 @@
|
||||
{status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN"
|
||||
status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY"
|
||||
status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY"
|
||||
status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY"
|
||||
status-im.config/ALCHEMY_ARBITRUM_GOERLI_TOKEN #shadow/env "ALCHEMY_ARBITRUM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_GOERLI_TOKEN #shadow/env "ALCHEMY_OPTIMISM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"}
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"}
|
||||
:compiler-options {:output-feature-set :es5
|
||||
;; We disable `:fn-deprecated` warnings because we
|
||||
;; are managing deprecation via clj-kondo and we
|
||||
@@ -90,10 +95,15 @@
|
||||
{status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN"
|
||||
status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY"
|
||||
status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY"
|
||||
status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY"
|
||||
status-im.config/ALCHEMY_ARBITRUM_GOERLI_TOKEN #shadow/env "ALCHEMY_ARBITRUM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_GOERLI_TOKEN #shadow/env "ALCHEMY_OPTIMISM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"}
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"}
|
||||
:compiler-options {:output-feature-set :es6
|
||||
;;disable for android build as there
|
||||
;;is an intermittent warning with deftype
|
||||
@@ -123,10 +133,15 @@
|
||||
status-im.config/POKT_TOKEN #shadow/env "POKT_TOKEN"
|
||||
status-im.config/INFURA_TOKEN #shadow/env "INFURA_TOKEN"
|
||||
status-im.config/OPENSEA_API_KEY #shadow/env "OPENSEA_API_KEY"
|
||||
status-im.config/RARIBLE_MAINNET_API_KEY #shadow/env "RARIBLE_MAINNET_API_KEY"
|
||||
status-im.config/RARIBLE_TESTNET_API_KEY #shadow/env "RARIBLE_TESTNET_API_KEY"
|
||||
status-im.config/ALCHEMY_ARBITRUM_GOERLI_TOKEN #shadow/env "ALCHEMY_ARBITRUM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_GOERLI_TOKEN #shadow/env "ALCHEMY_OPTIMISM_GOERLI_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"}
|
||||
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"
|
||||
status-im.config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ETHEREUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
|
||||
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"}
|
||||
:compiler-options
|
||||
{;; needed because we override require and it
|
||||
;; messes with source-map which reports callstack
|
||||
|
||||
@@ -49,8 +49,12 @@
|
||||
:height "100%")))
|
||||
|
||||
(defn text-input
|
||||
[theme]
|
||||
(assoc text-input-dimensions :color (colors/theme-colors colors/neutral-100 colors/white theme)))
|
||||
[theme error?]
|
||||
(assoc text-input-dimensions
|
||||
:color
|
||||
(if error?
|
||||
(colors/resolve-color :danger theme)
|
||||
(colors/theme-colors colors/neutral-100 colors/white theme))))
|
||||
|
||||
(defn placeholder-text
|
||||
[theme]
|
||||
|
||||
@@ -32,9 +32,12 @@
|
||||
(crypto-format num-value conversion crypto-decimals token))))
|
||||
|
||||
(defn- data-info
|
||||
[{:keys [theme token crypto-decimals conversion networks title crypto? currency amount]}]
|
||||
[{:keys [theme token crypto-decimals conversion networks title crypto? currency amount error?]}]
|
||||
[rn/view {:style style/data-container}
|
||||
[network-tag/view {:networks networks :title title}]
|
||||
[network-tag/view
|
||||
{:networks networks
|
||||
:title title
|
||||
:status (when error? :error)}]
|
||||
[text/text
|
||||
{:size :paragraph-2
|
||||
:weight :medium
|
||||
@@ -80,7 +83,7 @@
|
||||
(reset! value-atom v))
|
||||
(when on-change-text
|
||||
(on-change-text v)))]
|
||||
(fn [{:keys [theme token customization-color show-keyboard? crypto? currency value]
|
||||
(fn [{:keys [theme token customization-color show-keyboard? crypto? currency value error?]
|
||||
:or {show-keyboard? true}}]
|
||||
[rn/pressable
|
||||
{:on-press focus-input
|
||||
@@ -90,7 +93,7 @@
|
||||
:size :size-32}]
|
||||
[rn/view {:style style/text-input-container}
|
||||
[rn/text-input
|
||||
(cond-> {:style (style/text-input theme)
|
||||
(cond-> {:style (style/text-input theme error?)
|
||||
:placeholder-text-color (style/placeholder-text theme)
|
||||
:auto-focus true
|
||||
:ref set-ref
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
:remove-listeners remove-listeners}))
|
||||
|
||||
(defn view
|
||||
[{:keys [header footer customization-color gradient-cover?]} &
|
||||
[{:keys [header footer customization-color footer-container-padding gradient-cover?]
|
||||
:or {footer-container-padding (safe-area/get-top)}} &
|
||||
children]
|
||||
(reagent/with-let [window-height (:height (rn/get-window))
|
||||
footer-container-height (reagent/atom 0)
|
||||
@@ -94,7 +95,7 @@
|
||||
children)]
|
||||
[rn/keyboard-avoiding-view
|
||||
{:style style/keyboard-avoiding-view
|
||||
:keyboard-vertical-offset (if platform/ios? (safe-area/get-top) 0)
|
||||
:keyboard-vertical-offset (if platform/ios? footer-container-padding 0)
|
||||
:pointer-events :box-none}
|
||||
[floating-container/view
|
||||
{:on-layout set-footer-container-height
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.animations
|
||||
(ns status-im.common.lightbox.animations
|
||||
(:require
|
||||
[react-native.reanimated :as reanimated]))
|
||||
|
||||
+23
-17
@@ -1,25 +1,25 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.bottom-view
|
||||
(ns status-im.common.lightbox.bottom-view
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as c]
|
||||
[status-im.contexts.chat.messenger.lightbox.style :as style]
|
||||
[status-im.contexts.chat.messenger.lightbox.text-sheet.view :as text-sheet]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.style :as style]
|
||||
[status-im.common.lightbox.text-sheet.view :as text-sheet]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn get-small-item-layout
|
||||
[_ index]
|
||||
#js
|
||||
{:length c/small-image-size
|
||||
:offset (* (+ c/small-image-size 8) index)
|
||||
{:length constants/small-image-size
|
||||
:offset (* (+ constants/small-image-size 8) index)
|
||||
:index index})
|
||||
|
||||
(defn- f-small-image
|
||||
[item index _ {:keys [scroll-index props]}]
|
||||
(let [size (if (= @scroll-index index) c/focused-image-size c/small-image-size)
|
||||
(let [size (if (= @scroll-index index) constants/focused-image-size constants/small-image-size)
|
||||
size-value (anim/use-val size)
|
||||
{:keys [scroll-index-lock? small-list-ref flat-list-ref]}
|
||||
props]
|
||||
@@ -27,7 +27,7 @@
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:chat.ui/zoom-out-signal @scroll-index])
|
||||
(rf/dispatch [:lightbox/zoom-out-signal @scroll-index])
|
||||
(reset! scroll-index-lock? true)
|
||||
(js/setTimeout #(reset! scroll-index-lock? false) 500)
|
||||
(js/setTimeout
|
||||
@@ -38,9 +38,9 @@
|
||||
(.scrollToIndex ^js @flat-list-ref
|
||||
#js {:animated true :index index}))
|
||||
(if platform/ios? 50 150))
|
||||
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id item)]))}
|
||||
(rf/dispatch [:lightbox/update-animation-shared-element-id (:id item)]))}
|
||||
[reanimated/fast-image
|
||||
{:source {:uri (:image (:content item))}
|
||||
{:source {:uri (:image item)}
|
||||
:style (reanimated/apply-animations-to-style {:width size-value
|
||||
:height size-value}
|
||||
{:border-radius 10})}]]))
|
||||
@@ -50,20 +50,26 @@
|
||||
[:f> f-small-image item index _ render-data])
|
||||
|
||||
(defn bottom-view
|
||||
[messages index scroll-index insets animations derived item-width props state transparent?]
|
||||
(let [padding-horizontal (- (/ item-width 2) (/ c/focused-image-size 2))]
|
||||
[{:keys [images index scroll-index insets animations derived item-width props state transparent?
|
||||
bottom-text-component]}]
|
||||
(let [padding-horizontal (- (/ item-width 2) (/ constants/focused-image-size 2))]
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-100 colors/neutral-100-opa-80 colors/neutral-100-opa-0]
|
||||
:location [0.2 0.9]
|
||||
:start {:x 0 :y 1}
|
||||
:end {:x 0 :y 0}
|
||||
:style (style/gradient-container insets animations derived transparent?)}
|
||||
[text-sheet/view messages animations state props]
|
||||
(when bottom-text-component
|
||||
[text-sheet/view
|
||||
{:overlay-opacity (:overlay-opacity animations)
|
||||
:overlay-z-index (:overlay-z-index state)
|
||||
:text-sheet-lock? (:text-sheet-lock? props)
|
||||
:text-component bottom-text-component}])
|
||||
[rn/flat-list
|
||||
{:ref #(reset! (:small-list-ref props) %)
|
||||
:key-fn :message-id
|
||||
:style {:height c/small-list-height}
|
||||
:data messages
|
||||
:key-fn :id
|
||||
:style {:height constants/small-list-height}
|
||||
:data images
|
||||
:render-fn small-image
|
||||
:render-data {:scroll-index scroll-index
|
||||
:props props}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.constants)
|
||||
(ns status-im.common.lightbox.constants)
|
||||
|
||||
(def ^:const small-image-size 40)
|
||||
(def ^:const focused-extra-size 16)
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.effects
|
||||
(ns status-im.common.lightbox.effects
|
||||
(:require [react-native.blob :as blob]
|
||||
[react-native.cameraroll :as cameraroll]
|
||||
[react-native.fs :as fs]
|
||||
@@ -10,7 +10,7 @@
|
||||
{:trusty platform/ios?
|
||||
:path (str (fs/cache-dir) "/StatusIm_Image.jpeg")})
|
||||
|
||||
(rf/reg-fx :effects.chat/share-image
|
||||
(rf/reg-fx :effects.lightbox/share-image
|
||||
(fn [uri]
|
||||
(blob/fetch uri
|
||||
config
|
||||
@@ -20,7 +20,7 @@
|
||||
#(fs/unlink downloaded-url)
|
||||
#(fs/unlink downloaded-url))))))
|
||||
|
||||
(rf/reg-fx :effects.chat/save-image-to-gallery
|
||||
(rf/reg-fx :effects.lightbox/save-image-to-gallery
|
||||
(fn [[uri on-success]]
|
||||
(blob/fetch uri
|
||||
config
|
||||
@@ -0,0 +1,37 @@
|
||||
(ns status-im.common.lightbox.events
|
||||
(:require [reagent.core :as reagent]
|
||||
status-im.common.lightbox.effects
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(rf/reg-event-fx :lightbox/navigate-to-lightbox
|
||||
(fn [{:keys [db]} [animation-shared-element-id screen-params]]
|
||||
(reagent/next-tick #(rf/dispatch [:navigate-to :lightbox screen-params]))
|
||||
{:db (assoc db :animation-shared-element-id animation-shared-element-id)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/update-animation-shared-element-id
|
||||
(fn [{:keys [db]} [animation-shared-element-id]]
|
||||
{:db (assoc db :animation-shared-element-id animation-shared-element-id)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/exit-lightbox-signal
|
||||
(fn [{:keys [db]} [value]]
|
||||
{:db (assoc db :lightbox/exit-signal value)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/zoom-out-signal
|
||||
(fn [{:keys [db]} [value]]
|
||||
{:db (assoc db :lightbox/zoom-out-signal value)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/orientation-change
|
||||
(fn [{:keys [db]} [value]]
|
||||
{:db (assoc db :lightbox/orientation value)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/lightbox-scale
|
||||
(fn [{:keys [db]} [value]]
|
||||
{:db (assoc db :lightbox/scale value)}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/share-image
|
||||
(fn [_ [uri]]
|
||||
{:effects.lightbox/share-image uri}))
|
||||
|
||||
(rf/reg-event-fx :lightbox/save-image-to-gallery
|
||||
(fn [_ [uri on-success]]
|
||||
{:effects.lightbox/save-image-to-gallery [uri on-success]}))
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.style
|
||||
(ns status-im.common.lightbox.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as c]))
|
||||
[status-im.common.lightbox.constants :as constants]))
|
||||
|
||||
;;;; VIEW
|
||||
(defn image
|
||||
@@ -33,7 +33,7 @@
|
||||
{:position :absolute
|
||||
:padding-horizontal 20
|
||||
:top (if (or platform/ios? (not landscape?)) top-inset 0)
|
||||
:height c/top-view-height
|
||||
:height constants/top-view-height
|
||||
:z-index 4
|
||||
:flex-direction :row
|
||||
:justify-content :space-between
|
||||
@@ -44,7 +44,7 @@
|
||||
(defn top-gradient
|
||||
[insets]
|
||||
{:position :absolute
|
||||
:height (+ c/top-view-height (:top insets) 0)
|
||||
:height (+ constants/top-view-height (:top insets) 0)
|
||||
:top (- (:top insets))
|
||||
:left 0
|
||||
:right 0})
|
||||
@@ -71,12 +71,12 @@
|
||||
:display (if @transparent? :none :flex)
|
||||
:bottom 0
|
||||
:padding-bottom (:bottom insets)
|
||||
:padding-top c/text-min-height
|
||||
:padding-top constants/text-min-height
|
||||
:z-index 3}))
|
||||
|
||||
(defn content-container
|
||||
[padding-horizontal]
|
||||
{:padding-vertical c/small-list-padding-vertical
|
||||
{:padding-vertical constants/small-list-padding-vertical
|
||||
:padding-horizontal padding-horizontal
|
||||
:align-items :center
|
||||
:justify-content :center})
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.text-sheet.style
|
||||
(ns status-im.common.lightbox.text-sheet.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as constants]))
|
||||
[status-im.common.lightbox.constants :as constants]))
|
||||
|
||||
(defn sheet-container
|
||||
[{:keys [height top]}]
|
||||
@@ -14,10 +14,10 @@
|
||||
:right 0}))
|
||||
|
||||
(defn text-style
|
||||
[expanding-message?]
|
||||
[expandable-text?]
|
||||
{:color colors/white
|
||||
:margin-horizontal 20
|
||||
:align-items (when-not expanding-message? :center)
|
||||
:align-items (when-not expandable-text? :center)
|
||||
:flex-grow 1})
|
||||
|
||||
(def bar-container
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.text-sheet.utils
|
||||
(ns status-im.common.lightbox.text-sheet.utils
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as r]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as constants]))
|
||||
[status-im.common.lightbox.constants :as constants]))
|
||||
|
||||
(defn- collapse-sheet
|
||||
[{:keys [derived-value overlay-opacity saved-top expanded? overlay-z-index]}]
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
(defn sheet-gesture
|
||||
[{:keys [derived-value saved-top overlay-opacity gradient-opacity]}
|
||||
expanded-height max-height full-height overlay-z-index expanded? dragging? expanding-message?]
|
||||
expanded-height max-height full-height overlay-z-index expanded? dragging? expandable-text?]
|
||||
(let [disable-gesture-update (r/atom false)]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/enabled expanding-message?)
|
||||
(gesture/enabled expandable-text?)
|
||||
(gesture/on-start (fn []
|
||||
(reset! overlay-z-index 1)
|
||||
(reset! dragging? true)
|
||||
+30
-34
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.text-sheet.view
|
||||
(ns status-im.common.lightbox.text-sheet.view
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
@@ -8,33 +8,31 @@
|
||||
[react-native.reanimated :as reanimated]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.lightbox.text-sheet.style :as style]
|
||||
[status-im.contexts.chat.messenger.lightbox.text-sheet.utils :as utils]
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as message-view]))
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.text-sheet.style :as style]
|
||||
[status-im.common.lightbox.text-sheet.utils :as utils]))
|
||||
|
||||
(defn- text-sheet
|
||||
[messages overlay-opacity overlay-z-index text-sheet-lock?]
|
||||
[_]
|
||||
(let [text-height (reagent/atom 0)
|
||||
expanded? (reagent/atom false)
|
||||
dragging? (atom false)]
|
||||
(fn []
|
||||
(let [{:keys [chat-id content]} (first messages)
|
||||
insets (safe-area/get-insets)
|
||||
window-height (:height (rn/get-window))
|
||||
max-height (- window-height
|
||||
constants/text-min-height
|
||||
constants/top-view-height
|
||||
(:bottom insets)
|
||||
(when platform/ios? (:top insets)))
|
||||
full-height (+ constants/bar-container-height
|
||||
constants/text-margin
|
||||
constants/line-height
|
||||
@text-height)
|
||||
expanded-height (min max-height full-height)
|
||||
animations (utils/init-animations overlay-opacity)
|
||||
derived (utils/init-derived-animations animations)
|
||||
expanding-message? (> @text-height (* constants/line-height 2))]
|
||||
(fn [{:keys [overlay-opacity overlay-z-index text-sheet-lock? text-component]}]
|
||||
(let [insets (safe-area/get-insets)
|
||||
window-height (:height (rn/get-window))
|
||||
max-height (- window-height
|
||||
constants/text-min-height
|
||||
constants/top-view-height
|
||||
(:bottom insets)
|
||||
(when platform/ios? (:top insets)))
|
||||
full-height (+ constants/bar-container-height
|
||||
constants/text-margin
|
||||
constants/line-height
|
||||
@text-height)
|
||||
expanded-height (min max-height full-height)
|
||||
animations (utils/init-animations overlay-opacity)
|
||||
derived (utils/init-derived-animations animations)
|
||||
expandable-text? (> @text-height (* constants/line-height 2))]
|
||||
[rn/view
|
||||
[reanimated/linear-gradient
|
||||
{:colors [colors/neutral-100-opa-0 colors/neutral-100]
|
||||
@@ -51,10 +49,10 @@
|
||||
overlay-z-index
|
||||
expanded?
|
||||
dragging?
|
||||
expanding-message?)}
|
||||
expandable-text?)}
|
||||
[gesture/gesture-detector
|
||||
{:gesture (-> (gesture/gesture-tap)
|
||||
(gesture/enabled (and expanding-message? (not @expanded?)))
|
||||
(gesture/enabled (and expandable-text? (not @expanded?)))
|
||||
(gesture/on-start (fn []
|
||||
(utils/expand-sheet animations
|
||||
expanded-height
|
||||
@@ -63,7 +61,7 @@
|
||||
expanded?
|
||||
text-sheet-lock?))))}
|
||||
[reanimated/view {:style (style/sheet-container derived)}
|
||||
(when expanding-message?
|
||||
(when expandable-text?
|
||||
[rn/view {:style style/bar-container}
|
||||
[rn/view {:style style/bar}]])
|
||||
[linear-gradient/linear-gradient
|
||||
@@ -72,19 +70,17 @@
|
||||
:end {:x 0 :y 0}
|
||||
:locations [0.7 0.8 1]
|
||||
:style (style/bottom-gradient (:bottom insets))}]
|
||||
|
||||
[gesture/scroll-view
|
||||
{:scroll-enabled false
|
||||
:scroll-event-throttle 16
|
||||
:bounces false
|
||||
:style {:height (- max-height constants/bar-container-height)}
|
||||
:content-container-style {:padding-top (when (not expanding-message?)
|
||||
:content-container-style {:padding-top (when (not expandable-text?)
|
||||
constants/bar-container-height)}}
|
||||
[message-view/render-parsed-text
|
||||
{:content content
|
||||
:chat-id chat-id
|
||||
:style-override (style/text-style expanding-message?)
|
||||
:on-layout #(utils/on-layout % text-height)}]]]]]]))))
|
||||
[rn/view {:on-layout #(utils/on-layout % text-height)}
|
||||
text-component]]]]]]))))
|
||||
|
||||
(defn view
|
||||
[messages {:keys [overlay-opacity]} {:keys [overlay-z-index]} {:keys [text-sheet-lock?]}]
|
||||
[:f> text-sheet messages overlay-opacity overlay-z-index text-sheet-lock?])
|
||||
[props]
|
||||
[:f> text-sheet props])
|
||||
+24
-26
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.top-view
|
||||
(ns status-im.common.lightbox.top-view
|
||||
(:require
|
||||
[quo.core :as quo]
|
||||
[quo.foundations.colors :as colors]
|
||||
@@ -6,10 +6,9 @@
|
||||
[react-native.orientation :as orientation]
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as c]
|
||||
[status-im.contexts.chat.messenger.lightbox.style :as style]
|
||||
[utils.datetime :as datetime]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.style :as style]
|
||||
[utils.i18n :as i18n]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
@@ -17,7 +16,7 @@
|
||||
(defn animate-rotation
|
||||
[result screen-width screen-height insets
|
||||
{:keys [rotate top-view-y top-view-x top-view-width top-view-bg]}]
|
||||
(let [top-x (+ (/ c/top-view-height 2) (:top insets))]
|
||||
(let [top-x (+ (/ constants/top-view-height 2) (:top insets))]
|
||||
(cond
|
||||
(= result orientation/landscape-left)
|
||||
(do
|
||||
@@ -42,9 +41,9 @@
|
||||
(anim/animate top-view-bg colors/neutral-100-opa-0)))))
|
||||
|
||||
(defn drawer
|
||||
[messages index]
|
||||
(let [{:keys [content]} (nth messages index)
|
||||
uri (url/replace-port (:image content) (rf/sub [:mediaserver/port]))]
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
uri (url/replace-port image (rf/sub [:mediaserver/port]))]
|
||||
[quo/action-drawer
|
||||
[[{:icon :i/save
|
||||
:accessibility-label :save-image
|
||||
@@ -52,7 +51,7 @@
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:hide-bottom-sheet])
|
||||
(rf/dispatch
|
||||
[:chat.ui/save-image-to-gallery
|
||||
[:lightbox/save-image-to-gallery
|
||||
uri
|
||||
#(rf/dispatch [:toasts/upsert
|
||||
{:id :random-id
|
||||
@@ -61,20 +60,19 @@
|
||||
:text (i18n/label :t/photo-saved)}])]))}]]]))
|
||||
|
||||
(defn share-image
|
||||
[messages index]
|
||||
(let [{:keys [content]} (nth messages index)
|
||||
uri (url/replace-port (:image content) (rf/sub [:mediaserver/port]))]
|
||||
(rf/dispatch [:chat.ui/share-image uri])))
|
||||
[images index]
|
||||
(let [{:keys [image]} (nth images index)
|
||||
uri (url/replace-port image (rf/sub [:mediaserver/port]))]
|
||||
(rf/dispatch [:lightbox/share-image uri])))
|
||||
|
||||
(defn top-view
|
||||
[messages insets index animations derived landscape? screen-width]
|
||||
(let [{:keys [from timestamp]} (first messages)
|
||||
[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from])
|
||||
bg-color (if landscape?
|
||||
colors/neutral-100-opa-70
|
||||
colors/neutral-100-opa-0)
|
||||
[images insets index animations derived landscape? screen-width]
|
||||
(let [{:keys [description header]} (nth images @index)
|
||||
bg-color (if landscape?
|
||||
colors/neutral-100-opa-70
|
||||
colors/neutral-100-opa-0)
|
||||
{:keys [background-color opacity
|
||||
overlay-opacity]} animations]
|
||||
overlay-opacity]} animations]
|
||||
[reanimated/view
|
||||
{:style
|
||||
(style/top-view-container (:top insets) screen-width bg-color landscape? animations derived)}
|
||||
@@ -92,7 +90,7 @@
|
||||
(anim/animate opacity 0)
|
||||
(anim/animate overlay-opacity 0)
|
||||
(rf/dispatch (if platform/ios?
|
||||
[:chat.ui/exit-lightbox-signal @index]
|
||||
[:lightbox/exit-lightbox-signal @index]
|
||||
[:navigate-back])))
|
||||
:style style/close-container}
|
||||
[quo/icon :close {:size 20 :color colors/white}]]
|
||||
@@ -100,22 +98,22 @@
|
||||
[quo/text
|
||||
{:weight :semi-bold
|
||||
:size :paragraph-1
|
||||
:style {:color colors/white}} primary-name]
|
||||
:style {:color colors/white}} header]
|
||||
[quo/text
|
||||
{:weight :medium
|
||||
:size :paragraph-2
|
||||
:style {:color colors/neutral-40}} (when timestamp (datetime/to-short-str timestamp))]]]
|
||||
:style {:color colors/neutral-40}} description]]]
|
||||
[rn/view {:style style/top-right-buttons}
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:accessibility-label :share-image
|
||||
:on-press #(share-image messages @index)
|
||||
:on-press #(share-image images @index)
|
||||
:style (merge style/close-container {:margin-right 12})}
|
||||
[quo/icon :share {:size 20 :color colors/white}]]
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:accessibility-label :image-options
|
||||
:on-press #(rf/dispatch [:show-bottom-sheet
|
||||
{:content (fn [] [drawer messages @index])}])
|
||||
{:content (fn [] [drawer images @index])}])
|
||||
:style style/close-container}
|
||||
[quo/icon :options {:size 20 :color colors/white}]]]]))
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.utils
|
||||
(ns status-im.common.lightbox.utils
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
@@ -9,9 +9,9 @@
|
||||
[react-native.platform :as platform]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.lightbox.top-view :as top-view]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.top-view :as top-view]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.worklets.chat.messenger.lightbox :as worklet]))
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
(if platform/ios? 250 100)))
|
||||
(swap! timers assoc :mount-index-lock (js/setTimeout #(reset! scroll-index-lock? false) 300))
|
||||
(fn []
|
||||
(rf/dispatch [:chat.ui/zoom-out-signal nil])
|
||||
(rf/dispatch [:lightbox/zoom-out-signal nil])
|
||||
(when platform/android?
|
||||
(rf/dispatch [:chat.ui/lightbox-scale 1]))
|
||||
(clear-timers timers)))))
|
||||
@@ -69,7 +69,7 @@
|
||||
landscape? (string/includes? result orientation/landscape)
|
||||
item-width (if (and landscape? platform/ios?) screen-height screen-width)]
|
||||
(when (or landscape? (= result orientation/portrait))
|
||||
(rf/dispatch [:chat.ui/orientation-change result]))
|
||||
(rf/dispatch [:lightbox/orientation-change result]))
|
||||
(cond
|
||||
landscape?
|
||||
(orientation/lock-to-landscape "lightbox")
|
||||
@@ -148,11 +148,11 @@
|
||||
:timers (atom {})})
|
||||
|
||||
(defn init-state
|
||||
[messages index]
|
||||
[images index]
|
||||
;; The initial value of data is the image that was pressed (and not the whole album) in order
|
||||
;; for the transition animation to execute properly, otherwise it would animate towards
|
||||
;; outside the screen (even if we have `initialScrollIndex` set).
|
||||
{:data (reagent/atom (if (number? index) [(nth messages index)] []))
|
||||
{:data (reagent/atom (if (number? index) [(nth images index)] []))
|
||||
:scroll-index (reagent/atom index)
|
||||
:transparent? (reagent/atom false)
|
||||
:set-full-height? (reagent/atom false)
|
||||
+65
-42
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.view
|
||||
(ns status-im.common.lightbox.view
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[oops.core :as oops]
|
||||
@@ -9,13 +9,13 @@
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.bottom-view :as bottom-view]
|
||||
[status-im.contexts.chat.messenger.lightbox.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.lightbox.style :as style]
|
||||
[status-im.contexts.chat.messenger.lightbox.top-view :as top-view]
|
||||
[status-im.contexts.chat.messenger.lightbox.utils :as utils]
|
||||
[status-im.contexts.chat.messenger.lightbox.zoomable-image.view :as zoomable-image]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.bottom-view :as bottom-view]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.common.lightbox.style :as style]
|
||||
[status-im.common.lightbox.top-view :as top-view]
|
||||
[status-im.common.lightbox.utils :as utils]
|
||||
[status-im.common.lightbox.zoomable-image.view :as zoomable-image]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn get-item-layout
|
||||
@@ -33,9 +33,10 @@
|
||||
(reset! scroll-index index)
|
||||
(when @small-list-ref
|
||||
(.scrollToIndex ^js @small-list-ref #js {:animated true :index index}))
|
||||
(rf/dispatch [:chat.ui/update-shared-element-id (:message-id (oops/oget changed :item))]))))
|
||||
(rf/dispatch [:lightbox/update-animation-shared-element-id
|
||||
(:id (oops/oget changed :item))]))))
|
||||
|
||||
(defn image
|
||||
(defn image-view
|
||||
[message index _ {:keys [screen-width screen-height] :as args}]
|
||||
[rn/view
|
||||
{:style (style/image (+ screen-width constants/separator-width) screen-height)}
|
||||
@@ -43,30 +44,31 @@
|
||||
[rn/view {:style {:width constants/separator-width}}]])
|
||||
|
||||
(defn lightbox-content
|
||||
[props {:keys [data transparent? scroll-index set-full-height?] :as state}
|
||||
animations derived messages index handle-items-changed]
|
||||
(let [insets (safe-area/get-insets)
|
||||
window (rn/get-window)
|
||||
window-width (:width window)
|
||||
window-height (if platform/android?
|
||||
(+ (:height window) (:top insets))
|
||||
(:height window))
|
||||
curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
|
||||
landscape? (string/includes? curr-orientation orientation/landscape)
|
||||
horizontal? (or platform/android? (not landscape?))
|
||||
inverted? (and platform/ios? (= curr-orientation orientation/landscape-right))
|
||||
screen-width (if (or platform/ios? (= curr-orientation orientation/portrait))
|
||||
window-width
|
||||
window-height)
|
||||
screen-height (if (or platform/ios? (= curr-orientation orientation/portrait))
|
||||
window-height
|
||||
window-width)
|
||||
item-width (if (and landscape? platform/ios?) screen-height screen-width)]
|
||||
[{:keys [props state animations derived images index handle-items-changed bottom-text-component]}]
|
||||
(let [{:keys [data transparent? scroll-index
|
||||
set-full-height?]} state
|
||||
insets (safe-area/get-insets)
|
||||
window (rn/get-window)
|
||||
window-width (:width window)
|
||||
window-height (if platform/android?
|
||||
(+ (:height window) (:top insets))
|
||||
(:height window))
|
||||
curr-orientation (or (rf/sub [:lightbox/orientation]) orientation/portrait)
|
||||
landscape? (string/includes? curr-orientation orientation/landscape)
|
||||
horizontal? (or platform/android? (not landscape?))
|
||||
inverted? (and platform/ios? (= curr-orientation orientation/landscape-right))
|
||||
screen-width (if (or platform/ios? (= curr-orientation orientation/portrait))
|
||||
window-width
|
||||
window-height)
|
||||
screen-height (if (or platform/ios? (= curr-orientation orientation/portrait))
|
||||
window-height
|
||||
window-width)
|
||||
item-width (if (and landscape? platform/ios?) screen-height screen-width)]
|
||||
[reanimated/view
|
||||
{:style (reanimated/apply-animations-to-style {:background-color (:background-color animations)}
|
||||
{:height screen-height})}
|
||||
(when-not @transparent?
|
||||
[:f> top-view/top-view messages insets scroll-index animations derived landscape?
|
||||
[:f> top-view/top-view images insets scroll-index animations derived landscape?
|
||||
screen-width])
|
||||
[gesture/gesture-detector
|
||||
{:gesture (utils/drag-gesture animations (and landscape? platform/ios?) set-full-height?)}
|
||||
@@ -92,7 +94,7 @@
|
||||
:scroll-event-throttle 8
|
||||
:style {:width (+ screen-width constants/separator-width)}
|
||||
:data @data
|
||||
:render-fn image
|
||||
:render-fn image-view
|
||||
:render-data {:opacity-value (:opacity animations)
|
||||
:overlay-opacity (:overlay-opacity animations)
|
||||
:border-value (:border animations)
|
||||
@@ -118,26 +120,47 @@
|
||||
;; NOTE: not un-mounting bottom-view based on `transparent?` (like we do with the top-view
|
||||
;; above), since we need to save the state of the text-sheet position. Instead, we use
|
||||
;; the `:display` style property to hide the bottom-sheet.
|
||||
(when (not landscape?)
|
||||
[:f> bottom-view/bottom-view messages index scroll-index insets animations derived
|
||||
item-width props state transparent?])]))
|
||||
(when (and (not landscape?) (or bottom-text-component (> (count images) 1)))
|
||||
[:f> bottom-view/bottom-view
|
||||
{:images images
|
||||
:index index
|
||||
:scroll-index scroll-index
|
||||
:insets insets
|
||||
:animations animations
|
||||
:derived derived
|
||||
:item-width item-width
|
||||
:props props
|
||||
:state state
|
||||
:transparent? transparent?
|
||||
:bottom-text-component bottom-text-component}])]))
|
||||
|
||||
(defn- f-lightbox
|
||||
[]
|
||||
(let [{:keys [messages index]} (rf/sub [:get-screen-params])
|
||||
props (utils/init-props)
|
||||
state (utils/init-state messages index)
|
||||
handle-items-changed (fn [e]
|
||||
(on-viewable-items-changed e props state))]
|
||||
(let [{:keys [images index bottom-text-component]} (rf/sub [:get-screen-params])
|
||||
props
|
||||
(utils/init-props)
|
||||
state
|
||||
(utils/init-state images index)
|
||||
handle-items-changed
|
||||
(fn [e]
|
||||
(on-viewable-items-changed e props state))]
|
||||
(fn []
|
||||
(let [animations (utils/init-animations (count messages) index)
|
||||
(let [animations (utils/init-animations (count images) index)
|
||||
derived (utils/init-derived-animations animations)]
|
||||
(anim/animate (:background-color animations) colors/neutral-100)
|
||||
(reset! (:data state) messages)
|
||||
(reset! (:data state) images)
|
||||
(when platform/ios? ; issue: https://github.com/wix/react-native-navigation/issues/7726
|
||||
(utils/orientation-change props state animations))
|
||||
(utils/effect props animations index)
|
||||
[:f> lightbox-content props state animations derived messages index handle-items-changed]))))
|
||||
[:f> lightbox-content
|
||||
{:props props
|
||||
:state state
|
||||
:animations animations
|
||||
:derived derived
|
||||
:images images
|
||||
:index index
|
||||
:handle-items-changed handle-items-changed
|
||||
:bottom-text-component bottom-text-component}]))))
|
||||
|
||||
(defn lightbox
|
||||
[]
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.zoomable-image.constants)
|
||||
(ns status-im.common.lightbox.zoomable-image.constants)
|
||||
|
||||
(def ^:const min-scale 1)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.zoomable-image.style
|
||||
(ns status-im.common.lightbox.zoomable-image.style
|
||||
(:require
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.zoomable-image.utils
|
||||
(ns status-im.common.lightbox.zoomable-image.utils
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[react-native.core :as rn]
|
||||
@@ -6,8 +6,8 @@
|
||||
[react-native.orientation :as orientation]
|
||||
[react-native.platform :as platform]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.zoomable-image.constants :as constants]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.zoomable-image.constants :as constants]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
;;; Helpers
|
||||
@@ -97,7 +97,7 @@
|
||||
(rescale constants/min-scale true)
|
||||
(js/setTimeout #(rf/dispatch [:navigate-back]) 70))
|
||||
(rf/dispatch [:navigate-back]))
|
||||
(js/setTimeout #(rf/dispatch [:chat.ui/exit-lightbox-signal nil]) 500)))
|
||||
(js/setTimeout #(rf/dispatch [:lightbox/exit-lightbox-signal nil]) 500)))
|
||||
|
||||
(defn handle-zoom-out-signal
|
||||
"Zooms out when pressing on another photo from the small bottom list"
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.zoomable-image.view
|
||||
(ns status-im.common.lightbox.zoomable-image.view
|
||||
(:require
|
||||
[oops.core :refer [oget]]
|
||||
[react-native.core :as rn]
|
||||
@@ -7,10 +7,10 @@
|
||||
[react-native.platform :as platform]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[reagent.core :as r]
|
||||
[status-im.contexts.chat.messenger.lightbox.animations :as anim]
|
||||
[status-im.contexts.chat.messenger.lightbox.zoomable-image.constants :as c]
|
||||
[status-im.contexts.chat.messenger.lightbox.zoomable-image.style :as style]
|
||||
[status-im.contexts.chat.messenger.lightbox.zoomable-image.utils :as utils]
|
||||
[status-im.common.lightbox.animations :as anim]
|
||||
[status-im.common.lightbox.zoomable-image.constants :as c]
|
||||
[status-im.common.lightbox.zoomable-image.style :as style]
|
||||
[status-im.common.lightbox.zoomable-image.utils :as utils]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
(anim/animate-decay pan-y-start velocity [lower-bound upper-bound]))))))))
|
||||
|
||||
(defn- f-zoomable-image
|
||||
[dimensions animations state rescale curr-orientation content focused? index render-data
|
||||
[dimensions animations state rescale curr-orientation image focused? index render-data
|
||||
image-dimensions-nil?]
|
||||
(let [{:keys [transparent? set-full-height?]} render-data
|
||||
portrait? (= curr-orientation orientation/portrait)
|
||||
@@ -229,7 +229,7 @@
|
||||
(= curr-orientation orientation/portrait))}
|
||||
[reanimated/fast-image
|
||||
(merge
|
||||
{:source {:uri (url/replace-port (:image content) (rf/sub [:mediaserver/port]))}
|
||||
{:source {:uri (url/replace-port image (rf/sub [:mediaserver/port]))}
|
||||
:native-ID (when focused? :shared-element)
|
||||
:style (style/image dimensions animations render-data index)}
|
||||
(when image-dimensions-nil?
|
||||
@@ -238,12 +238,12 @@
|
||||
(defn zoomable-image
|
||||
[]
|
||||
(let [state (utils/init-state)]
|
||||
(fn [{:keys [image-width image-height content message-id]} index render-data]
|
||||
(let [shared-element-id (rf/sub [:shared-element-id])
|
||||
(fn [{:keys [image-width image-height image id]} index render-data]
|
||||
(let [animation-shared-element-id (rf/sub [:animation-shared-element-id])
|
||||
exit-lightbox-signal (rf/sub [:lightbox/exit-signal])
|
||||
zoom-out-signal (rf/sub [:lightbox/zoom-out-signal])
|
||||
{:keys [set-full-height? curr-orientation]} render-data
|
||||
focused? (= shared-element-id message-id)
|
||||
focused? (= animation-shared-element-id id)
|
||||
;; TODO - remove `image-dimensions` check, once
|
||||
;; https://github.com/status-im/status-desktop/issues/10944 is fixed
|
||||
image-dimensions-nil? (not (and image-width image-height))
|
||||
@@ -269,5 +269,5 @@
|
||||
rescale
|
||||
set-full-height?))
|
||||
(utils/handle-zoom-out-signal zoom-out-signal index (anim/get-val (:scale animations)) rescale)
|
||||
[:f> f-zoomable-image dimensions animations state rescale curr-orientation content focused?
|
||||
[:f> f-zoomable-image dimensions animations state rescale curr-orientation image focused?
|
||||
index render-data image-dimensions-nil?]))))
|
||||
@@ -15,6 +15,11 @@
|
||||
(goog-define ALCHEMY_OPTIMISM_GOERLI_TOKEN "")
|
||||
(goog-define ALCHEMY_OPTIMISM_MAINNET_TOKEN "")
|
||||
(goog-define ALCHEMY_ARBITRUM_MAINNET_TOKEN "")
|
||||
(goog-define ALCHEMY_ETHEREUM_SEPOLIA_TOKEN "")
|
||||
(goog-define ALCHEMY_ARBITRUM_SEPOLIA_TOKEN "")
|
||||
(goog-define ALCHEMY_OPTIMISM_SEPOLIA_TOKEN "")
|
||||
(goog-define RARIBLE_MAINNET_API_KEY "")
|
||||
(goog-define RARIBLE_TESTNET_API_KEY "")
|
||||
(goog-define OPENSEA_API_KEY "")
|
||||
|
||||
(def mainnet-rpc-url (str "https://eth-archival.gateway.pokt.network/v1/lb/" POKT_TOKEN))
|
||||
|
||||
@@ -406,11 +406,14 @@
|
||||
|
||||
;; wallet
|
||||
(def ^:const mainnet-chain-id 1)
|
||||
(def ^:const optimism-chain-id 10)
|
||||
(def ^:const optimism-test-chain-id 420)
|
||||
(def ^:const arbitrum-chain-id 42161)
|
||||
(def ^:const arbitrum-test-chain-id 421613)
|
||||
(def ^:const goerli-chain-id 5)
|
||||
(def ^:const sepolia-chain-id 11155111)
|
||||
(def ^:const optimism-chain-id 10)
|
||||
(def ^:const optimism-goerli-chain-id 420)
|
||||
(def ^:const optimism-sepolia-chain-id 11155420)
|
||||
(def ^:const arbitrum-chain-id 42161)
|
||||
(def ^:const arbitrum-goerli-chain-id 421613)
|
||||
(def ^:const arbitrum-sepolia-chain-id 421614)
|
||||
|
||||
(def ^:const mainnet-short-name "eth")
|
||||
(def ^:const optimism-short-name "opt")
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
[legacy.status-im.chat.models.loading :as loading]
|
||||
[legacy.status-im.data-store.chats :as chats-store]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]
|
||||
status-im.common.lightbox.events
|
||||
[status-im.common.muting.helpers :refer [format-mute-till]]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.chat.contacts.events :as contacts-store]
|
||||
status-im.contexts.chat.effects
|
||||
[status-im.contexts.chat.messenger.composer.link-preview.events :as link-preview]
|
||||
status-im.contexts.chat.messenger.lightbox.events
|
||||
status-im.contexts.chat.messenger.messages.content.reactions.events
|
||||
[status-im.contexts.chat.messenger.messages.delete-message-for-me.events :as delete-for-me]
|
||||
[status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message]
|
||||
@@ -394,38 +393,6 @@
|
||||
[cofx chat-id]
|
||||
(navigation/navigate-to cofx :chat-pinned-messages {:chat-id chat-id}))
|
||||
|
||||
|
||||
(rf/defn update-shared-element-id
|
||||
{:events [:chat.ui/update-shared-element-id]}
|
||||
[{:keys [db]} shared-element-id]
|
||||
{:db (assoc db :shared-element-id shared-element-id)})
|
||||
|
||||
(rf/defn navigate-to-lightbox
|
||||
{:events [:chat.ui/navigate-to-lightbox]}
|
||||
[{:keys [db]} shared-element-id screen-params]
|
||||
(reagent/next-tick #(rf/dispatch [:navigate-to :lightbox screen-params]))
|
||||
{:db (assoc db :shared-element-id shared-element-id)})
|
||||
|
||||
(rf/defn exit-lightbox-signal
|
||||
{:events [:chat.ui/exit-lightbox-signal]}
|
||||
[{:keys [db]} value]
|
||||
{:db (assoc db :lightbox/exit-signal value)})
|
||||
|
||||
(rf/defn zoom-out-signal
|
||||
{:events [:chat.ui/zoom-out-signal]}
|
||||
[{:keys [db]} value]
|
||||
{:db (assoc db :lightbox/zoom-out-signal value)})
|
||||
|
||||
(rf/defn orientation-change
|
||||
{:events [:chat.ui/orientation-change]}
|
||||
[{:keys [db]} value]
|
||||
{:db (assoc db :lightbox/orientation value)})
|
||||
|
||||
(rf/defn lightbox-scale
|
||||
{:events [:chat.ui/lightbox-scale]}
|
||||
[{:keys [db]} value]
|
||||
{:db (assoc db :lightbox/scale value)})
|
||||
|
||||
(rf/defn check-last-chat
|
||||
{:events [:chat/check-last-chat]}
|
||||
[{:keys [db]}]
|
||||
@@ -452,3 +419,15 @@
|
||||
{:events [:chat.ui/scroll-to-bottom]}
|
||||
[_]
|
||||
{:effects.chat/scroll-to-bottom nil})
|
||||
|
||||
(rf/reg-event-fx :chat.ui/clear-sending-images
|
||||
(fn [{:keys [db]}]
|
||||
{:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})}))
|
||||
|
||||
(rf/reg-event-fx :chat.ui/image-unselected
|
||||
(fn [{:keys [db]} [original]]
|
||||
(let [current-chat-id (:current-chat-id db)]
|
||||
{:db (update-in db
|
||||
[:chat/inputs current-chat-id :metadata :sending-image]
|
||||
dissoc
|
||||
(:uri original))})))
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
(ns status-im.contexts.chat.messenger.lightbox.events
|
||||
(:require status-im.contexts.chat.messenger.lightbox.effects
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(rf/reg-event-fx :chat.ui/clear-sending-images
|
||||
(fn [{:keys [db]}]
|
||||
{:db (update-in db [:chat/inputs (:current-chat-id db) :metadata] assoc :sending-image {})}))
|
||||
|
||||
(rf/reg-event-fx :chat.ui/image-unselected
|
||||
(fn [{:keys [db]} [original]]
|
||||
(let [current-chat-id (:current-chat-id db)]
|
||||
{:db (update-in db
|
||||
[:chat/inputs current-chat-id :metadata :sending-image]
|
||||
dissoc
|
||||
(:uri original))})))
|
||||
|
||||
(rf/reg-event-fx :chat.ui/share-image
|
||||
(fn [_ [uri]]
|
||||
{:effects.chat/share-image uri}))
|
||||
|
||||
(rf/reg-event-fx :chat.ui/save-image-to-gallery
|
||||
(fn [_ [uri on-success]]
|
||||
{:effects.chat/save-image-to-gallery [uri on-success]}))
|
||||
@@ -7,6 +7,8 @@
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.messages.content.album.style :as style]
|
||||
[status-im.contexts.chat.messenger.messages.content.image.view :as image]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.utils :as lightbox-utils]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.view :as lightbox]
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as text]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
@@ -21,17 +23,19 @@
|
||||
|
||||
(defn album-message
|
||||
[{:keys [albumize?] :as message} context on-long-press message-container-data]
|
||||
(let [shared-element-id (rf/sub [:shared-element-id])
|
||||
media-server-port (rf/sub [:mediaserver/port])
|
||||
first-image (first (:album message))
|
||||
album-style (if (> (:image-width first-image) (:image-height first-image))
|
||||
:landscape
|
||||
:portrait)
|
||||
images-count (count (:album message))
|
||||
(let [animation-shared-element-id (rf/sub [:animation-shared-element-id])
|
||||
media-server-port (rf/sub [:mediaserver/port])
|
||||
album-messages (:album message)
|
||||
first-image (first album-messages)
|
||||
album-style (if (> (:image-width first-image) (:image-height first-image))
|
||||
:landscape
|
||||
:portrait)
|
||||
images-count (count album-messages)
|
||||
;; album images are always square, except when we have 3 images, then they must be
|
||||
;; rectangular
|
||||
;; (portrait or landscape)
|
||||
portrait? (and (= images-count rectangular-style-count) (= album-style :portrait))]
|
||||
portrait? (and (= images-count rectangular-style-count)
|
||||
(= album-style :portrait))]
|
||||
(if (and albumize? (> images-count 1))
|
||||
[:<>
|
||||
[rn/view {:style {:margin-bottom 4}} [text/text-content first-image]]
|
||||
@@ -52,14 +56,21 @@
|
||||
{:key (:message-id item)
|
||||
:active-opacity 1
|
||||
:on-long-press #(on-long-press message context)
|
||||
:on-press #(rf/dispatch [:chat.ui/navigate-to-lightbox
|
||||
(:message-id item)
|
||||
{:messages (:album message)
|
||||
:index index}])}
|
||||
:on-press #(rf/dispatch
|
||||
[:lightbox/navigate-to-lightbox
|
||||
(:message-id item)
|
||||
{:index index
|
||||
:images (into []
|
||||
(map
|
||||
lightbox-utils/convert-message-to-lightbox-image
|
||||
album-messages))
|
||||
:bottom-text-component
|
||||
[lightbox/bottom-text-for-lightbox
|
||||
first-image]}])}
|
||||
[fast-image/fast-image
|
||||
{:style (style/image dimensions index portrait? images-count)
|
||||
:source {:uri (url/replace-port (:image (:content item)) media-server-port)}
|
||||
:native-ID (when (and (= shared-element-id (:message-id item))
|
||||
:native-ID (when (and (= animation-shared-element-id (:message-id item))
|
||||
(< index constants/max-album-photos))
|
||||
:shared-element)}]
|
||||
(when (and (> images-count constants/max-album-photos)
|
||||
@@ -71,11 +82,11 @@
|
||||
:size :heading-2
|
||||
:style {:color colors/white}}
|
||||
(str "+" (- images-count (dec constants/max-album-photos)))]])]))
|
||||
(:album message))]]
|
||||
album-messages)]]
|
||||
[:<>
|
||||
(map-indexed
|
||||
(fn [index item]
|
||||
[:<> {:key (:message-id item)}
|
||||
[image/image-message index item {:on-long-press #(on-long-press message context)}
|
||||
message-container-data]])
|
||||
(:album message))])))
|
||||
album-messages)])))
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
[react-native.fast-image :as fast-image]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.utils :as lightbox-utils]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.view :as lightbox]
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as text]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.url :as url]))
|
||||
@@ -39,7 +41,7 @@
|
||||
image-height
|
||||
max-container-width
|
||||
max-container-height)
|
||||
shared-element-id (rf/sub [:shared-element-id])
|
||||
animation-shared-element-id (rf/sub [:animation-shared-element-id])
|
||||
image-local-url (url/replace-port (:image content) (rf/sub [:mediaserver/port]))]
|
||||
[:<>
|
||||
(when (= index 0)
|
||||
@@ -48,13 +50,16 @@
|
||||
{:active-opacity 1
|
||||
:style {:margin-top 4}
|
||||
:on-long-press on-long-press
|
||||
:on-press #(rf/dispatch [:chat.ui/navigate-to-lightbox
|
||||
:on-press #(rf/dispatch [:lightbox/navigate-to-lightbox
|
||||
message-id
|
||||
{:messages [message]
|
||||
:index 0
|
||||
:insets insets}])}
|
||||
{:images [(lightbox-utils/convert-message-to-lightbox-image
|
||||
message)]
|
||||
:index 0
|
||||
:insets insets
|
||||
:bottom-text-component
|
||||
[lightbox/bottom-text-for-lightbox message]}])}
|
||||
[fast-image/fast-image
|
||||
{:source {:uri image-local-url}
|
||||
:style (merge dimensions {:border-radius 12})
|
||||
:native-ID (when (= shared-element-id message-id) :shared-element)
|
||||
:native-ID (when (= animation-shared-element-id message-id) :shared-element)
|
||||
:accessibility-label :image-message}]]]))
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.content.lightbox.style
|
||||
(:require
|
||||
[quo.foundations.colors :as colors]))
|
||||
|
||||
(defn bottom-text
|
||||
[expandable-text?]
|
||||
{:color colors/white
|
||||
:margin-horizontal 20
|
||||
:align-items (when-not expandable-text? :center)
|
||||
:flex-grow 1})
|
||||
@@ -0,0 +1,14 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.content.lightbox.utils
|
||||
(:require
|
||||
[utils.datetime :as datetime]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(defn convert-message-to-lightbox-image
|
||||
[{:keys [timestamp image-width image-height message-id from content]}]
|
||||
(let [[primary-name _] (rf/sub [:contacts/contact-two-names-by-identity from])]
|
||||
{:image (:image content)
|
||||
:image-width image-width
|
||||
:image-height image-height
|
||||
:id message-id
|
||||
:header primary-name
|
||||
:description (when timestamp (datetime/to-short-str timestamp))}))
|
||||
@@ -0,0 +1,18 @@
|
||||
(ns status-im.contexts.chat.messenger.messages.content.lightbox.view
|
||||
(:require
|
||||
[oops.core :as oops]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.common.lightbox.constants :as constants]
|
||||
[status-im.contexts.chat.messenger.messages.content.lightbox.style :as style]
|
||||
[status-im.contexts.chat.messenger.messages.content.text.view :as message-view]))
|
||||
|
||||
(defn bottom-text-for-lightbox
|
||||
[_]
|
||||
(let [text-height (reagent/atom 0)]
|
||||
(fn [{:keys [content chat-id] :as _message}]
|
||||
(let [expandable-text? (> @text-height (* constants/line-height 2))]
|
||||
[message-view/render-parsed-text
|
||||
{:content content
|
||||
:chat-id chat-id
|
||||
:style-override (style/bottom-text expandable-text?)
|
||||
:on-layout #(reset! text-height (oops/oget % "nativeEvent.layout.height"))}]))))
|
||||
@@ -21,7 +21,9 @@
|
||||
{:key :currency
|
||||
:type :select
|
||||
:options [{:key :usd}
|
||||
{:key :eur}]}])
|
||||
{:key :eur}]}
|
||||
{:key :error?
|
||||
:type :boolean}])
|
||||
|
||||
(defn view
|
||||
[]
|
||||
|
||||
@@ -13,10 +13,15 @@
|
||||
:openseaAPIKey config/opensea-api-key
|
||||
:poktToken config/POKT_TOKEN
|
||||
:infuraToken config/INFURA_TOKEN
|
||||
:raribleMainnetApiKey config/RARIBLE_MAINNET_API_KEY
|
||||
:raribleTestnetApiKey config/RARIBLE_TESTNET_API_KEY
|
||||
:alchemyOptimismMainnetToken config/ALCHEMY_OPTIMISM_MAINNET_TOKEN
|
||||
:alchemyOptimismGoerliToken config/ALCHEMY_OPTIMISM_GOERLI_TOKEN
|
||||
:alchemyArbitrumMainnetToken config/ALCHEMY_ARBITRUM_MAINNET_TOKEN
|
||||
:alchemyArbitrumGoerliToken config/ALCHEMY_ARBITRUM_GOERLI_TOKEN})
|
||||
:alchemyArbitrumGoerliToken config/ALCHEMY_ARBITRUM_GOERLI_TOKEN
|
||||
:alchemyEthereumSepoliaToken config/ALCHEMY_ETHEREUM_SEPOLIA_TOKEN
|
||||
:alchemyArbitrumSepoliaToken config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN
|
||||
:alchemyOptimismSepoliaToken config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN})
|
||||
|
||||
(defn create
|
||||
[]
|
||||
|
||||
@@ -122,17 +122,17 @@
|
||||
(rf/dispatch [:wallet/clean-scanned-address])
|
||||
(rf/dispatch [:wallet/clear-address-activity-check])
|
||||
(rf/dispatch [:navigate-back]))}]
|
||||
:footer
|
||||
[quo/button
|
||||
{:customization-color customization-color
|
||||
:disabled? (or (string/blank? @input-value) (some? (validate @input-value)))
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:navigate-to
|
||||
:confirm-address-to-watch
|
||||
{:address @input-value}])
|
||||
(clear-input))
|
||||
:container-style {:z-index 2}}
|
||||
(i18n/label :t/continue)]}
|
||||
:footer [quo/button
|
||||
{:customization-color customization-color
|
||||
:disabled? (or (string/blank? @input-value)
|
||||
(some? (validate @input-value)))
|
||||
:on-press (fn []
|
||||
(rf/dispatch [:navigate-to
|
||||
:confirm-address-to-watch
|
||||
{:address @input-value}])
|
||||
(clear-input))
|
||||
:container-style {:z-index 2}}
|
||||
(i18n/label :t/continue)]}
|
||||
[quo/text-combinations
|
||||
{:container-style style/header-container
|
||||
:title (i18n/label :t/add-address)
|
||||
|
||||
@@ -75,12 +75,14 @@
|
||||
(let [selected-tab (reagent/atom :overview)
|
||||
on-tab-change #(reset! selected-tab %)]
|
||||
(fn []
|
||||
(let [collectible (rf/sub [:wallet/last-collectible-details])
|
||||
(let [collectible (rf/sub [:wallet/last-collectible-details])
|
||||
animation-shared-element-id (rf/sub [:animation-shared-element-id])
|
||||
{:keys [collectible-data preview-url
|
||||
collection-data]} collectible
|
||||
collection-data id]} collectible
|
||||
token-id (:token-id id)
|
||||
{collection-image :image-url
|
||||
collection-name :name} collection-data
|
||||
{collectible-name :name} collectible-data]
|
||||
collection-name :name} collection-data
|
||||
{collectible-name :name} collectible-data]
|
||||
[scroll-page/scroll-page
|
||||
{:navigate-back? true
|
||||
:height 148
|
||||
@@ -95,9 +97,26 @@
|
||||
:picture preview-url}}
|
||||
[rn/view {:style style/container}
|
||||
[rn/view {:style style/preview-container}
|
||||
[rn/image
|
||||
{:source preview-url
|
||||
:style style/preview}]]
|
||||
[rn/touchable-opacity
|
||||
{:active-opacity 1
|
||||
:on-press #(rf/dispatch [:lightbox/navigate-to-lightbox
|
||||
token-id
|
||||
{:images [{:image preview-url
|
||||
:image-width 300 ; collectibles don't have
|
||||
; width/height but we need
|
||||
; to pass something
|
||||
:image-height 300 ; without it animation
|
||||
; doesn't work smoothly and
|
||||
; :border-radius not
|
||||
; applied
|
||||
:id token-id
|
||||
:header collectible-name
|
||||
:description collection-name}]
|
||||
:index 0}])}
|
||||
[rn/image
|
||||
{:source preview-url
|
||||
:style style/preview
|
||||
:native-ID (when (= animation-shared-element-id token-id) :shared-element)}]]]
|
||||
[header collectible-name collection-name collection-image]
|
||||
[cta-buttons]
|
||||
[quo/tabs
|
||||
|
||||
@@ -178,12 +178,15 @@
|
||||
address))
|
||||
|
||||
(def id->network
|
||||
{constants/mainnet-chain-id :ethereum
|
||||
constants/goerli-chain-id :ethereum
|
||||
constants/optimism-chain-id :optimism
|
||||
constants/optimism-test-chain-id :optimism
|
||||
constants/arbitrum-chain-id :arbitrum
|
||||
constants/arbitrum-test-chain-id :arbitrum})
|
||||
{constants/mainnet-chain-id :ethereum
|
||||
constants/goerli-chain-id :ethereum
|
||||
constants/sepolia-chain-id :ethereum
|
||||
constants/optimism-chain-id :optimism
|
||||
constants/optimism-goerli-chain-id :optimism
|
||||
constants/optimism-sepolia-chain-id :optimism
|
||||
constants/arbitrum-chain-id :arbitrum
|
||||
constants/arbitrum-goerli-chain-id :arbitrum
|
||||
constants/arbitrum-sepolia-chain-id :arbitrum})
|
||||
|
||||
(defn get-standard-fiat-format
|
||||
[crypto-value currency-symbol fiat-value]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
(:require [camel-snake-kebab.core :as csk]
|
||||
[camel-snake-kebab.extras :as cske]
|
||||
[clojure.string :as string]
|
||||
[re-frame.core :as rf]
|
||||
[taoensso.timbre :as log]
|
||||
[utils.ethereum.chain :as chain]
|
||||
[utils.re-frame :as rf]
|
||||
[utils.transforms :as types]))
|
||||
|
||||
(def collectible-data-types
|
||||
@@ -66,8 +66,9 @@
|
||||
data-type (collectible-data-types :header)
|
||||
fetch-criteria {:fetch-type (fetch-type :fetch-if-not-cached)
|
||||
:max-cache-age-seconds max-cache-age-seconds}
|
||||
chain-ids (chain/chain-ids db)
|
||||
request-params [request-id
|
||||
[(chain/chain-id db)]
|
||||
chain-ids
|
||||
(keys (get-in db [:wallet :accounts]))
|
||||
collectibles-filter
|
||||
start-at-index
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
(h/is-truthy (h/get-by-text "0"))
|
||||
(h/is-truthy (h/get-by-text "ETH"))
|
||||
(h/is-truthy (h/get-by-text "$0.00"))
|
||||
(h/is-truthy (h/get-by-label-text :container))
|
||||
(h/is-disabled (h/get-by-label-text :button-one)))
|
||||
|
||||
(h/test "Fill token input and confirm"
|
||||
@@ -77,6 +78,7 @@
|
||||
(-> (h/wait-for #(h/get-by-text "$1234.50"))
|
||||
(.then (fn []
|
||||
(h/is-truthy (h/get-by-label-text :button-one))
|
||||
(h/is-truthy (h/get-by-label-text :container))
|
||||
(h/fire-event :press (h/get-by-label-text :button-one))
|
||||
(h/was-called on-confirm))))))
|
||||
|
||||
@@ -99,61 +101,33 @@
|
||||
(-> (h/wait-for #(h/get-by-text "$1234.50"))
|
||||
(.then (fn []
|
||||
(h/is-truthy (h/get-by-label-text :button-one))
|
||||
(h/is-truthy (h/get-by-label-text :container))
|
||||
(h/fire-event :press (h/get-by-label-text :button-one))
|
||||
(h/was-called on-confirm))))))
|
||||
|
||||
(h/test "Try to fill more than limit"
|
||||
(h/setup-subs sub-mocks)
|
||||
(h/render [input-amount/view
|
||||
{:crypto-decimals 10
|
||||
:limit-crypto 286}])
|
||||
{:crypto-decimals 1
|
||||
:limit-crypto 1}])
|
||||
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-2))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-9))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
|
||||
(-> (h/wait-for #(h/is-truthy (h/get-by-text "$290.00")))
|
||||
(.then (fn []
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-backspace))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-8))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
(h/wait-for #(h/get-by-text "$2850.00"))))))
|
||||
|
||||
(h/test "Try to fill more than limit"
|
||||
(h/setup-subs sub-mocks)
|
||||
(h/render [input-amount/view
|
||||
{:crypto-decimals 10
|
||||
:limit-crypto 286
|
||||
:on-confirm #()}])
|
||||
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-2))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-9))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
|
||||
(-> (h/wait-for #(h/get-by-text "$290.00"))
|
||||
(.then (fn []
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-backspace))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-8))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
(h/wait-for #(h/get-by-text "$2850.00"))))))
|
||||
(h/is-truthy (h/get-by-label-text :container-error)))
|
||||
|
||||
(h/test "Switch from crypto to fiat and check limit"
|
||||
(h/setup-subs sub-mocks)
|
||||
(h/render [input-amount/view
|
||||
{:crypto-decimals 2
|
||||
:limit-crypto 250
|
||||
{:crypto-decimals 1
|
||||
:limit-crypto 1
|
||||
:on-confirm #()}])
|
||||
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-2))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-0))
|
||||
(-> (h/wait-for #(h/get-by-text "$200.00"))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-9))
|
||||
(h/is-truthy (h/get-by-label-text :container-error))
|
||||
(h/fire-event :press (h/query-by-label-text :reorder))
|
||||
|
||||
(-> (h/wait-for #(h/get-by-text "Max: 1000.00 USD"))
|
||||
(.then (fn []
|
||||
(h/fire-event :press (h/query-by-label-text :reorder))
|
||||
(h/wait-for #(h/get-by-text "2.00 ETH"))))
|
||||
(.then (fn []
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
(h/wait-for #(h/get-by-text "20.50 ETH"))))
|
||||
(.then (fn []
|
||||
(h/fire-event :press (h/query-by-label-text :keyboard-key-5))
|
||||
(h/wait-for #(h/get-by-text "20.50 ETH")))))))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :container))))))))
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
(>= (js/parseFloat balance) input-value)))
|
||||
(map first)))
|
||||
|
||||
(defn- reset-input-error
|
||||
[new-value prev-value input-error]
|
||||
(reset! input-error
|
||||
(> new-value prev-value)))
|
||||
|
||||
(defn- f-view-internal
|
||||
;; crypto-decimals and limit-crypto args are needed for component tests only
|
||||
[{:keys [crypto-decimals limit-crypto]}]
|
||||
@@ -75,6 +80,7 @@
|
||||
limit-fiat (.toFixed (* (:total-balance token) conversion-rate) 2)
|
||||
crypto-decimals (or crypto-decimals (utils/get-crypto-decimals-count token))
|
||||
input-value (reagent/atom "")
|
||||
input-error (reagent/atom false)
|
||||
current-limit (reagent/atom {:amount limit-crypto
|
||||
:currency token-symbol})
|
||||
handle-swap (fn [crypto?]
|
||||
@@ -84,27 +90,30 @@
|
||||
:currency token-symbol}
|
||||
{:amount limit-fiat
|
||||
:currency currency}))
|
||||
(when (> num-value (:amount @current-limit))
|
||||
(reset! input-value ""))))
|
||||
(reset-input-error num-value
|
||||
(:amount @current-limit)
|
||||
input-error)))
|
||||
handle-keyboard-press (fn [v]
|
||||
(let [current-value @input-value
|
||||
new-value (make-new-input current-value v)
|
||||
num-value (or (parse-double new-value) 0)]
|
||||
(when (and (not loading-suggested-routes?)
|
||||
(<= num-value (:amount @current-limit)))
|
||||
(let [current-value @input-value
|
||||
new-value (make-new-input current-value v)
|
||||
num-value (or (parse-double new-value) 0)
|
||||
current-limit-amount (:amount @current-limit)]
|
||||
(when (not loading-suggested-routes?)
|
||||
(reset! input-value new-value)
|
||||
(reset-input-error num-value current-limit-amount input-error)
|
||||
(reagent/flush))))
|
||||
handle-delete (fn [_]
|
||||
(when-not loading-suggested-routes?
|
||||
(swap! input-value #(subs % 0 (dec (count %))))
|
||||
(reagent/flush)))
|
||||
(let [current-limit-amount (:amount @current-limit)]
|
||||
(swap! input-value #(subs % 0 (dec (count %))))
|
||||
(reset-input-error @input-value current-limit-amount input-error)
|
||||
(reagent/flush))))
|
||||
handle-on-change (fn [v]
|
||||
(when (valid-input? @input-value v)
|
||||
(let [num-value (or (parse-double v) 0)
|
||||
current-limit-amount (:amount @current-limit)]
|
||||
(if (> num-value current-limit-amount)
|
||||
(reset! input-value (str current-limit-amount))
|
||||
(reset! input-value v))
|
||||
(reset! input-value v)
|
||||
(reset-input-error num-value current-limit-amount input-error)
|
||||
(reagent/flush))))]
|
||||
(fn [{:keys [on-confirm]
|
||||
:or {on-confirm #(rf/dispatch [:wallet/send-select-amount
|
||||
@@ -119,7 +128,8 @@
|
||||
(empty? @input-value)
|
||||
(<= input-num-value 0)
|
||||
(> input-num-value (:amount @current-limit)))
|
||||
amount (str @input-value " " token-symbol)]
|
||||
amount (str @input-value " " token-symbol)
|
||||
{:keys [color]} (rf/sub [:wallet/current-viewing-account])]
|
||||
(rn/use-effect
|
||||
(fn []
|
||||
(let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!))
|
||||
@@ -135,7 +145,8 @@
|
||||
100)))
|
||||
[@input-value])
|
||||
[rn/view
|
||||
{:style style/screen}
|
||||
{:style style/screen
|
||||
:accessibility-label (str "container" (when @input-error "-error"))}
|
||||
[account-switcher/view
|
||||
{:icon-name :i/arrow-left
|
||||
:on-press #(rf/dispatch [:navigate-back-within-stack :wallet-send-input-amount])
|
||||
@@ -145,6 +156,7 @@
|
||||
:token token-symbol
|
||||
:currency currency
|
||||
:crypto-decimals crypto-decimals
|
||||
:error? @input-error
|
||||
:networks (:networks token)
|
||||
:title (i18n/label :t/send-limit {:limit limit-label})
|
||||
:conversion conversion-rate
|
||||
@@ -159,10 +171,11 @@
|
||||
:loading-networks (find-affordable-networks token @input-value)
|
||||
:networks (:networks token)}]
|
||||
[quo/bottom-actions
|
||||
{:actions :1-action
|
||||
:button-one-label (i18n/label :t/confirm)
|
||||
:button-one-props {:disabled? confirm-disabled?
|
||||
:on-press on-confirm}}]
|
||||
{:actions :1-action
|
||||
:button-one-label (i18n/label :t/confirm)
|
||||
:button-one-props {:disabled? confirm-disabled?
|
||||
:on-press on-confirm}
|
||||
:customization-color color}]
|
||||
[quo/numbered-keyboard
|
||||
{:container-style (style/keyboard-container bottom)
|
||||
:left-action :dot
|
||||
|
||||
@@ -128,26 +128,30 @@
|
||||
(fn []
|
||||
(let [selected-tab (or (rf/sub [:wallet/send-tab]) (:id (first tabs-data)))
|
||||
token (rf/sub [:wallet/wallet-send-token])
|
||||
valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?]))]
|
||||
valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?]))
|
||||
{:keys [color]} (rf/sub [:wallet/current-viewing-account])]
|
||||
(rn/use-effect (fn []
|
||||
(fn []
|
||||
(rf/dispatch [:wallet/clean-scanned-address])
|
||||
(rf/dispatch [:wallet/clean-local-suggestions]))))
|
||||
[floating-button-page/view
|
||||
{:header [account-switcher/view
|
||||
{:on-press on-close
|
||||
:margin-top (safe-area/get-top)
|
||||
:switcher-type :select-account}]
|
||||
:footer (when (> (count @input-value) 0)
|
||||
[quo/button
|
||||
{:accessibility-label :continue-button
|
||||
:type :primary
|
||||
:disabled? (not valid-ens-or-address?)
|
||||
:on-press #(rf/dispatch [:wallet/select-send-address
|
||||
{:address @input-value
|
||||
:token token
|
||||
:stack-id :wallet-select-address}])}
|
||||
(i18n/label :t/continue)])}
|
||||
{:footer-container-padding 0
|
||||
:header [account-switcher/view
|
||||
{:on-press on-close
|
||||
:margin-top (safe-area/get-top)
|
||||
:switcher-type :select-account}]
|
||||
:footer (when (> (count @input-value) 0)
|
||||
[quo/button
|
||||
{:accessibility-label :continue-button
|
||||
:type :primary
|
||||
:disabled? (not valid-ens-or-address?)
|
||||
:on-press #(rf/dispatch [:wallet/select-send-address
|
||||
{:address @input-value
|
||||
:token token
|
||||
:stack-id
|
||||
:wallet-select-address}])
|
||||
:customization-color color}
|
||||
(i18n/label :t/continue)])}
|
||||
[quo/text-combinations
|
||||
{:title (i18n/label :t/send-to)
|
||||
:container-style style/title-container
|
||||
|
||||
@@ -162,28 +162,30 @@
|
||||
:address (utils/get-shortened-address to-address)}]
|
||||
[rn/view {:style {:flex 1}}
|
||||
[floating-button-page/view
|
||||
{:header [quo/page-nav
|
||||
{:icon-name :i/arrow-left
|
||||
:on-press on-close
|
||||
:margin-top (safe-area/get-top)
|
||||
:background :blur
|
||||
:accessibility-label :top-bar
|
||||
:right-side [{:icon-name :i/advanced
|
||||
:on-press #(js/alert
|
||||
"to be implemented")
|
||||
:accessibility-label :advanced-options}]}]
|
||||
:footer (if route
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-send)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color account-color
|
||||
:on-auth-success #(rf/dispatch [:wallet/send-transaction
|
||||
(security/safe-unmask-data %)])
|
||||
:auth-button-label (i18n/label :t/confirm)}]
|
||||
[rn/activity-indicator])
|
||||
:gradient-cover? true
|
||||
:customization-color (:color account)}
|
||||
{:footer-container-padding 0
|
||||
:header [quo/page-nav
|
||||
{:icon-name :i/arrow-left
|
||||
:on-press on-close
|
||||
:margin-top (safe-area/get-top)
|
||||
:background :blur
|
||||
:accessibility-label :top-bar
|
||||
:right-side [{:icon-name :i/advanced
|
||||
:on-press #(js/alert
|
||||
"to be implemented")
|
||||
:accessibility-label :advanced-options}]}]
|
||||
:footer (if route
|
||||
[standard-auth/slide-button
|
||||
{:size :size-48
|
||||
:track-text (i18n/label :t/slide-to-send)
|
||||
:container-style {:z-index 2}
|
||||
:customization-color account-color
|
||||
:on-auth-success #(rf/dispatch [:wallet/send-transaction
|
||||
(security/safe-unmask-data
|
||||
%)])
|
||||
:auth-button-label (i18n/label :t/confirm)}]
|
||||
[rn/activity-indicator])
|
||||
:gradient-cover? true
|
||||
:customization-color (:color account)}
|
||||
[rn/view
|
||||
[transaction-title
|
||||
{:token-symbol token-symbol
|
||||
|
||||
@@ -32,19 +32,20 @@
|
||||
(fn []
|
||||
(let [transaction-details (rf/sub [:wallet/send-transaction-progress])]
|
||||
[floating-button-page/view
|
||||
{:header [quo/page-nav
|
||||
{:type :no-title
|
||||
:background :blur
|
||||
:icon-name :i/close
|
||||
:margin-top (safe-area/get-top)
|
||||
:on-press leave-page
|
||||
:accessibility-label :top-bar}]
|
||||
:footer [quo/button
|
||||
{:customization-color color
|
||||
:on-press leave-page}
|
||||
(i18n/label :t/done)]
|
||||
:customization-color color
|
||||
:gradient-cover? true}
|
||||
{:footer-container-padding 0
|
||||
:header [quo/page-nav
|
||||
{:type :no-title
|
||||
:background :blur
|
||||
:icon-name :i/close
|
||||
:margin-top (safe-area/get-top)
|
||||
:on-press leave-page
|
||||
:accessibility-label :top-bar}]
|
||||
:footer [quo/button
|
||||
{:customization-color color
|
||||
:on-press leave-page}
|
||||
(i18n/label :t/done)]
|
||||
:customization-color color
|
||||
:gradient-cover? true}
|
||||
[rn/view {:style style/content-container}
|
||||
[rn/image
|
||||
{:source (resources/get-image :transaction-progress)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
(:require
|
||||
[legacy.status-im.ui.screens.screens :as old-screens]
|
||||
[status-im.common.emoji-picker.view :as emoji-picker]
|
||||
[status-im.common.lightbox.view :as lightbox]
|
||||
[status-im.config :as config]
|
||||
[status-im.contexts.chat.group-details.view :as group-details]
|
||||
[status-im.contexts.chat.home.add-new-contact.scan.scan-profile-qr-page :as scan-profile-qr-page]
|
||||
[status-im.contexts.chat.home.add-new-contact.views :as add-new-contact]
|
||||
[status-im.contexts.chat.home.new-chat.view :as new-chat]
|
||||
[status-im.contexts.chat.messenger.camera.view :as camera-screen]
|
||||
[status-im.contexts.chat.messenger.lightbox.view :as lightbox]
|
||||
[status-im.contexts.chat.messenger.messages.view :as chat]
|
||||
[status-im.contexts.chat.messenger.photo-selector.view :as photo-selector]
|
||||
[status-im.contexts.communities.actions.accounts-selection.view :as communities.accounts-selection]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
;;view
|
||||
(reg-root-key-sub :view-id :view-id)
|
||||
(reg-root-key-sub :screen-params :navigation/screen-params)
|
||||
(reg-root-key-sub :shared-element-id :shared-element-id)
|
||||
(reg-root-key-sub :animation-shared-element-id :animation-shared-element-id)
|
||||
|
||||
;;bottom sheet
|
||||
(reg-root-key-sub :bottom-sheet :bottom-sheet)
|
||||
|
||||
@@ -12,14 +12,17 @@
|
||||
:short-name "eth"
|
||||
:network-name :ethereum
|
||||
:related-chain-id 1
|
||||
:chain-id 3
|
||||
:layer 1}
|
||||
{:test? true
|
||||
:short-name "arb1"
|
||||
:related-chain-id 42161
|
||||
:chain-id 4
|
||||
:layer 2}
|
||||
{:test? true
|
||||
:short-name "opt"
|
||||
:related-chain-id 10
|
||||
:chain-id 5
|
||||
:layer 2}]
|
||||
:prod [{:test? false
|
||||
:short-name "eth"
|
||||
|
||||
@@ -84,3 +84,10 @@
|
||||
(defn chain-id
|
||||
[db]
|
||||
(network->chain-id (get-current-network db)))
|
||||
|
||||
(defn chain-ids
|
||||
[db]
|
||||
(let [test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?])
|
||||
networks (get-in db [:wallet :networks])
|
||||
env-networks (get networks (if test-networks-enabled? :test :prod))]
|
||||
(map :chain-id env-networks)))
|
||||
|
||||
@@ -3,7 +3,21 @@
|
||||
[cljs.test :refer-macros [deftest is]]
|
||||
[utils.ethereum.chain :as chain]))
|
||||
|
||||
(defn chain-ids-db
|
||||
[test-networks-enabled?]
|
||||
{:profile/profile {:test-networks-enabled? test-networks-enabled?}
|
||||
:wallet {:networks {:test [{:chain-id 3}
|
||||
{:chain-id 4}
|
||||
{:chain-id 5}]
|
||||
:prod [{:chain-id 1}
|
||||
{:chain-id 42161}
|
||||
{:chain-id 10}]}}})
|
||||
|
||||
(deftest chain-id->chain-keyword
|
||||
(is (= (chain/chain-id->chain-keyword 1) :mainnet))
|
||||
(is (= (chain/chain-id->chain-keyword 5) :goerli))
|
||||
(is (= (chain/chain-id->chain-keyword 5777) :custom)))
|
||||
|
||||
(deftest chain-ids
|
||||
(is (= (chain/chain-ids (chain-ids-db false)) [1 42161 10]))
|
||||
(is (= (chain/chain-ids (chain-ids-db true)) [3 4 5])))
|
||||
|
||||
Reference in New Issue
Block a user