Compare commits

...
Author SHA1 Message Date
Alexander d53ca8d658 Merge branch 'develop' into kotlininfy-pushnotifications 2024-01-24 13:08:25 +01:00
Siddarth Kumar ee1f93defb fix: xcbeautify usage without omitting crucial logs (#18605)
## Summary

I introduced `xcbeautify` inside `make run-ios` command which would sometimes skip spitting crucial logs to the terminal.
In cases where the `react-native-cli` would prompt me of a port being used by another metro server, those prompts would also never reach me since they were piped into `xcbeautify` essentially giving me the impression that `make run-ios` is still doing something when it was actually hung.

This is how `react-native-community/cli` looks for `xcbeautify` ->
https://github.com/react-native-community/cli/blob/main/packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts#L139

This commit fixes the problem by exporting an env variable making `react-native-cli` aware that we have `xcbeautify` installed and this way we now get the full log output we were used to.
2024-01-24 15:28:42 +05:30
John Ngei 3665fe26da fixed: mute-icon when channel is muted 2024-01-24 00:25:16 +01:00
John Ngei 3ec7209fcc Update default wallet name
* updated default wallet name to 'Account 1'

https://github.com/status-im/status-go/compare/ea3e59ff...64c8ce2e

* updated status go
2024-01-23 23:44:47 +01:00
Paul FitzgeraldandAjay Sivan 195826a7f4 chore(quo): add component list item - network list to design system (#18547)
---------

Co-authored-by: Ajay Sivan <ajayesivan@gmail.com>
2024-01-23 11:41:20 -08:00
Ibrahem KhalilandYevheniia Berdnyk 5bb4f3632a [17514] Use OS emojis in emoji picker (#18397)
---------

Co-authored-by: Yevheniia Berdnyk <ie.berdnyk@gmail.com>
2024-01-23 14:41:43 +02:00
Mohamed Javid 4fe62553ca Wallet - Refresh tokens and prices (#18456)
This commit adds

- RPC call for start wallet which will signal the client to refresh data (tokens, prices, ...etc) every 10 minutes
- Refreshes tokens and prices every 10 minutes on receiving the "wallet-tick-reload" signal

---------

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2024-01-23 17:59:29 +05:30
BalogunofAfrica cb586c8676 fix: make identifier carousel swipe-able (#18515) 2024-01-23 12:25:03 +01:00
Ibrahem Khalil fd50e4281c Add scroll view to profile tab (#18448) 2024-01-23 12:52:34 +02:00
Ulises Manuel dcbb080217 [#18458] Token input fixes (#18520)
* Add custom keyboard to token-input preview
* Fix token input flickering
2024-01-22 20:01:00 -06:00
Parvesh Monu 97d1b3faa1 fix cannot add existing ENS owned by user (#18567) 2024-01-23 04:42:56 +05:30
Nikolay 9c322397a2 chore(wallet): remove account confirmation UI (#18518) 2024-01-22 10:00:07 -08:00
Mohsen ca2b1d5c8e fix: remove quo preview from nightly builds (#18556) 2024-01-22 19:08:34 +03:30
Jamie Caprani 9a4bc7e250 feat(wallet): connect backend to transaction progress page (#18506) 2024-01-22 14:40:13 +00:00
Paul Fitzgerald c6dbfb6244 chore: add background color gradient to share qr code component (#18503) 2024-01-22 06:08:43 -08:00
Alexander 6a7a8ee813 Merge branch 'develop' into kotlininfy-pushnotifications 2024-01-22 09:37:43 +01:00
Omar Basem 826ab6625e Wallet: token input conversion (#18539)
Wallet: token input conversion
2024-01-20 10:23:19 +04:00
Ulises Manuel a9ec2ca7ba Add support to display token images in token gated communities (#18174) 2024-01-19 20:51:50 -06:00
Parvesh Monu 1e11edc442 Remove last reagent atom from messages/view file (#18551) 2024-01-19 21:41:02 +05:30
codemaster bf8461f007 fix: wrong navigation bar color in transparent screens (#18451) 2024-01-19 07:19:58 -08:00
Mohamed Javid 5926426672 [Fix] Currency symbol on recovered profile (#18433)
This commit fixes the currency symbol not shown on the recovered profile.

--

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2024-01-19 20:03:28 +05:30
yqrashawn e871357b67 refactor: move perm/airdrop addrs into community (#18571)
Signed-off-by: yqrashawn <namy.19@gmail.com>
2024-01-19 20:44:17 +08:00
Omar Basem 9f5e53cc63 wallet: fix address inconsistencies (#18558)
wallet: fix address inconsistencies
2024-01-19 15:45:17 +04:00
yqrashawn 61c0650cec fix: do not show community view until it's loaded (#18570) 2024-01-19 16:36:14 +08:00
Omar Basem 7cc570b6d0 wallet: fix route test net (#18553)
wallet: fix route test net
2024-01-19 07:47:37 +04:00
Brian Sztamfater e609e2ff6a fix: crash when selecting a collectible (#18541)
Signed-off-by: Brian Sztamfater <brian@status.im>
2024-01-18 16:05:09 -03:00
Alexander f8cdb3bd7b Style fix (#18191) 2024-01-18 16:23:42 +01:00
Alexander 40d8cf3121 Merge branch 'develop' into kotlininfy-pushnotifications 2024-01-18 15:59:21 +01:00
alwx c198017513 Kotlinify im.status.ethereum.pushnotifications package 2024-01-18 15:08:42 +01:00
yqrashawn fe9a4eac59 feat: re-fetch community info in community overview and channel (#18465)
Signed-off-by: yqrashawn <namy.19@gmail.com>
2024-01-18 22:05:31 +08:00
frank d934dc26ef Remove the invocation of wakuext_chatMentionReplaceWithPublicKey (#18533) 2024-01-18 21:33:37 +08:00
Mohamed Javid ed3cb42572 Wallet - Add token filter and prices fallback for fiat amount calculation (#18398)
This commit:

- Adds a filter to remove tokens with no name or symbol to prevent displaying tokens with no data
- Adds a fallback value (zero) for token fiat value calculation and to prevent crash on token price calculation

---------

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
2024-01-18 15:16:00 +05:30
142 changed files with 3059 additions and 2518 deletions
+3 -2
View File
@@ -282,11 +282,12 @@ SIMULATOR=iPhone 13
# TODO: fix IOS_STATUS_GO_TARGETS to be either amd64 or arm64 when RN is upgraded
run-ios: export TARGET := ios
run-ios: export IOS_STATUS_GO_TARGETS := ios/arm64;iossimulator/amd64
run-ios: export XCBeautify=$(shell which xcbeautify) # for react-native-cli to pick this up and to auto format output
run-ios: ##@run Build iOS app and start it in a simulator/device
ifneq ("$(SIMULATOR)", "")
npx react-native run-ios --simulator="$(SIMULATOR)" | xcbeautify
npx react-native run-ios --simulator="$(SIMULATOR)"
else
npx react-native run-ios | xcbeautify
npx react-native run-ios
endif
show-ios-devices: ##@other shows connected ios device and its name
@@ -1,102 +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;
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;
}
}
@@ -0,0 +1,88 @@
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"
}
}
@@ -1,146 +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.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();
}
}
}
@@ -0,0 +1,135 @@
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()
}
}
}
@@ -1,93 +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;
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();
}
}
}
});
}
}
}
@@ -0,0 +1,89 @@
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()
}
}
}
})
}
}
}
@@ -1,886 +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 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);
}
}
@@ -0,0 +1,771 @@
// 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)
}
}
@@ -1,86 +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;
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);
}
}
}
@@ -0,0 +1,81 @@
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)
}
}
}
@@ -1,27 +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;
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();
}
}
@@ -0,0 +1,22 @@
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()
}
}
@@ -1,136 +0,0 @@
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);
}
}
}
}
@@ -0,0 +1,112 @@
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)
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -51,3 +51,9 @@
(visibility-status-updates/<-rpc-settings)
(set/rename-keys {:compressedKey :compressed-key
:emojiHash :emoji-hash})))
(defn rpc->setting-value
[{:keys [name] :as setting}]
(condp = name
:currency (update setting :value keyword)
setting))
+1 -1
View File
@@ -231,7 +231,7 @@
[db]
(into #{}
(remove #(= (:type %) :watch)
(map #(eip55/address->checksum (:address %)) (get db :profile/wallet-accounts)))))
(map #(eip55/address->checksum (:address %)) (vals (get-in db [:wallet :accounts]))))))
;;NOTE we want to handle only last resolve
(def resolve-last-id (atom nil))
@@ -79,10 +79,15 @@
"recent-history-ready" (recent-history-fetching-ended cofx event)
"fetching-history-error" (fetching-error cofx event)
"non-archival-node-detected" (non-archival-node-detected cofx event)
"pending-transaction-status-changed" {:fx
[[:dispatch
[:wallet/pending-transaction-status-changed-received
event]]]}
"wallet-owned-collectibles-filtering-done" {:fx [[:dispatch
[:wallet/owned-collectibles-filtering-done
event]]]}
"wallet-get-collectibles-details-done" {:fx [[:dispatch
[:wallet/get-collectible-details-done
event]]]}
"wallet-tick-reload" {:fx [[:dispatch [:wallet/reload]]]}
(log/debug ::unknown-wallet-event :type type :event event)))
+1
View File
@@ -43,6 +43,7 @@
[react-native.permissions :as permissions]
[react-native.platform :as platform]
[status-im.common.biometric.events :as biometric]
status-im.common.serialization
status-im.common.standard-authentication.events
[status-im.common.theme.core :as theme]
[status-im.common.universal-links :as universal-links]
@@ -246,7 +246,7 @@
[rn/touchable-opacity
{:on-press #(do
(rf/dispatch [:pop-to-root :shell-stack])
(rf/dispatch [:navigate-to :community-overview (:id community)])
(rf/dispatch [:communities/navigate-to-community-overview (:id community)])
(rf/dispatch [:chat/close]))}
[rn/text
{:style {:text-align :center
+11 -11
View File
@@ -11,17 +11,17 @@
(defn token-requirement-list-row
[tokens padding?]
[rn/view {:style (style/token-row padding?)}
(map-indexed (fn [token-index token]
(let [{:keys [amount sufficient? purchasable?]} token]
^{:key token-index}
[rn/view {:style style/token-tag-spacing}
[token-tag/view
{:token-symbol (:symbol token)
:token-value amount
:size :size-24
:options (cond
sufficient? :hold
purchasable? :add)}]]))
(map-indexed (fn [token-index {:keys [img-src amount sufficient? purchasable?] :as token}]
^{:key token-index}
[rn/view {:style style/token-tag-spacing}
[token-tag/view
{:token-symbol (:symbol token)
:token-img-src img-src
:token-value amount
:size :size-24
:options (cond
sufficient? :hold
purchasable? :add)}]])
tokens)])
(defn- internal-view
+33 -20
View File
@@ -76,15 +76,19 @@
(str description " · " (i18n/label :t/on-device))])
(defn- context-tag-subtitle
[{:keys [community-logo community-name]}]
[rn/view
{:accessibility-label :context-tag-wrapper
:style {:flex-wrap :wrap}}
[context-tag/view
{:type :community
:community-name community-name
:community-logo community-logo
:size 24}]])
[{:keys [context-tag-type community-logo community-name account-name emoji customization-color]}]
(let [tag-type (or context-tag-type :account)]
[rn/view
{:accessibility-label :context-tag-wrapper
:style {:flex-wrap :wrap}}
[context-tag/view
{:type tag-type
:account-name account-name
:emoji emoji
:community-name community-name
:community-logo community-logo
:size 24
:customization-color customization-color}]]))
(defn- description-subtitle
[{:keys [theme blur? description]}]
@@ -95,7 +99,8 @@
description])
(defn- subtitle
[{:keys [type theme blur? keycard? networks description community-name community-logo]}]
[{:keys [type theme blur? keycard? networks description community-name community-logo
context-tag-type account-name emoji customization-color]}]
(cond
(= :keypair type)
[keypair-subtitle
@@ -118,8 +123,12 @@
(= :context-tag type)
[context-tag-subtitle
{:community-logo community-logo
:community-name community-name}]
{:context-tag-type context-tag-type
:community-logo community-logo
:community-name community-name
:account-name account-name
:emoji emoji
:customization-color customization-color}]
(and (not= :label type) description)
[description-subtitle
@@ -173,6 +182,7 @@
(defn- view-internal
[{:keys [title title-icon type theme description blur? community-name community-logo button-icon
account-name emoji
on-button-press
on-button-long-press
button-disabled? account-avatar-emoji account-avatar-type customization-color icon-avatar
@@ -196,14 +206,17 @@
:theme theme
:blur? blur?}]
[subtitle
{:type type
:theme theme
:blur? blur?
:keycard? keycard?
:networks networks
:description description
:community-name community-name
:community-logo community-logo}]]
{:type type
:theme theme
:blur? blur?
:keycard? keycard?
:networks networks
:description description
:community-name community-name
:community-logo community-logo
:customization-color customization-color
:account-name account-name
:emoji emoji}]]
[right-icon
{:theme theme
:type type
@@ -1,6 +1,6 @@
(ns quo.components.gradient.gradient-cover.style)
(defn root-container
[opacity]
{:height 252
[opacity height]
{:height (or height 252)
:opacity opacity})
@@ -6,7 +6,7 @@
[react-native.linear-gradient :as linear-gradient]))
(defn- view-internal
[{:keys [customization-color opacity container-style] :or {customization-color :blue}}]
[{:keys [customization-color opacity container-style height] :or {customization-color :blue}}]
(let [color-top (colors/custom-color customization-color 50 20)
color-bottom (colors/custom-color customization-color 50 0)]
[linear-gradient/linear-gradient
@@ -14,6 +14,6 @@
:colors [color-top color-bottom]
:start {:x 0 :y 0}
:end {:x 0 :y 1}
:style (merge (style/root-container opacity) container-style)}]))
:style (merge (style/root-container opacity height) container-style)}]))
(def view (quo.theme/with-theme view-internal))
@@ -0,0 +1,40 @@
(ns quo.components.list-items.network-list.component-spec
(:require [quo.components.list-items.network-list.view :as network-list]
[quo.foundations.colors :as colors]
[test-helpers.component :as h]))
(defn- render
[component]
(h/render-with-theme-provider component :light))
(def props
{:theme :light
:state :default
:label "Mainnet"
:network-image 873
:customization-color :blue
:token-value "100.00 ETH"
:fiat-value "€100.00"})
(h/describe "List items: Network List"
(h/test "default state explicit"
(render [network-list/view props])
(h/is-truthy (h/get-by-text "Mainnet")))
(h/test "Pressed state"
(render [network-list/view props])
(h/fire-event :on-press-in (h/get-by-label-text ::network-list))
(h/wait-for #(h/has-style (h/query-by-label-text ::network-list)
{:backgroundColor (colors/resolve-color :blue :light 5)})))
(h/test "Active state"
(render [network-list/view (assoc props :state :active)])
(h/has-style (h/query-by-label-text ::network-list)
{:backgroundColor (colors/resolve-color :blue :light 10)}))
(h/test "Call on-press"
(let [on-press (h/mock-fn)]
(render [network-list/view (assoc props :on-press on-press)])
(h/fire-event :on-press (h/get-by-label-text ::network-list))
(h/was-called on-press))))
@@ -0,0 +1,41 @@
(ns quo.components.list-items.network-list.style
(:require [quo.foundations.colors :as colors]))
(defn- background-color
[state customization-color theme]
(case state
:pressed (colors/resolve-color customization-color theme 5)
:active (colors/resolve-color customization-color theme 10)
:transparent))
(defn container
[state customization-color theme]
{:flex-direction :row
:justify-content :space-between
:align-items :center
:padding-horizontal 12
:padding-vertical 8
:border-radius 12
:height 56
:background-color (background-color state customization-color theme)})
(def info
{:flex-direction :row
:align-items :center
:gap 2})
(def network-image
{:border-width 1
:border-radius 16
:border-color colors/neutral-80-opa-5
:margin-right 8
:background-color colors/neutral-80-opa-5
:height 32
:width 32})
(def values-container
{:align-items :flex-end})
(defn fiat-value
[theme]
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)})
@@ -0,0 +1,76 @@
(ns quo.components.list-items.network-list.view
(:require
[clojure.string :as string]
[quo.components.list-items.network-list.style :as style]
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]
[schema.core :as schema]))
(defn- info
[{:keys [network-image label]}]
[rn/view {:style style/info}
[rn/image
{:source network-image
:style style/network-image}]
[rn/view
[text/text
{:weight :semi-bold
:style {:text-transform :capitalize}
:number-of-lines 1}
(if (string/blank? label) "-" label)]]])
(defn- values
[{:keys [token-value fiat-value theme]}]
[rn/view {:style style/values-container}
[text/text
{:weight :medium
:size :paragraph-2
:number-of-lines 1}
token-value]
[text/text
{:style (style/fiat-value theme)
:size :paragraph-2
:number-of-lines 1}
fiat-value]])
(def ?schema
[:=>
[:catn
[:props
[:map {:closed true}
[:network-image :int]
[:label :string]
[:fiat-value :string]
[:token-value :string]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]]
[:state [:enum :pressed :active :default]]
[:on-press {:optional true} [:maybe fn?]]
[:theme :schema.common/theme]]]]
:any])
(defn- view-internal
[]
(let [pressed? (reagent/atom false)
on-press-in #(reset! pressed? true)
on-press-out #(reset! pressed? false)]
(fn [{:keys [on-press state customization-color theme]
:as props
:or {customization-color :blue}}]
(let [internal-state (if @pressed?
:pressed
state)]
[rn/pressable
{:style (style/container internal-state customization-color theme)
:on-press-in on-press-in
:on-press-out on-press-out
:on-press on-press
:accessibility-label :network-list}
[info props]
[values props]]))))
(def view
(quo.theme/with-theme
(schema/instrument #'view-internal ?schema)))
@@ -1,4 +1,4 @@
(ns quo.components.list-items.user-list
(ns quo.components.list-items.user
(:require
[quo.components.avatars.user-avatar.view :as user-avatar]
[quo.components.icon :as icons]
@@ -18,14 +18,14 @@
:align-items :center})
(defn action-icon
[{:keys [type on-press on-check disabled? checked?]}]
[{:keys [type on-press on-check disabled? checked?]} theme]
[rn/touchable-opacity
{:on-press (when on-press on-press)}
(case type
:options
[icons/icon :i/options
{:size 20
:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}]
:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}]
:checkbox
[selectors/view
{:type :checkbox
@@ -37,30 +37,33 @@
[text/text "not implemented"]
[rn/view])])
(defn user-list
(defn user
[{:keys [short-chat-key primary-name secondary-name photo-path online? contact? verified?
untrustworthy? on-press on-long-press accessory]}]
[rn/touchable-opacity
untrustworthy? on-press on-long-press accessory customization-color theme]}]
[rn/touchable-highlight
{:style container-style
:underlay-color (colors/resolve-color customization-color theme 5)
:accessibility-label :user-list
:on-press (when on-press on-press)
:on-long-press (when on-long-press on-long-press)}
[user-avatar/user-avatar
{:full-name primary-name
:profile-picture photo-path
:online? online?
:size :small}]
[rn/view {:style {:margin-horizontal 8 :flex 1}}
[author/view
{:primary-name primary-name
:secondary-name secondary-name
:contact? contact?
:verified? verified?
:untrustworthy? untrustworthy?}]
(when short-chat-key
[text/text
{:size :paragraph-1
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}}
short-chat-key])]
(when accessory
[action-icon accessory])])
[:<>
[user-avatar/user-avatar
{:full-name primary-name
:profile-picture photo-path
:online? online?
:size :small}]
[rn/view {:style {:margin-horizontal 8 :flex 1}}
[author/view
{:primary-name primary-name
:secondary-name secondary-name
:contact? contact?
:verified? verified?
:untrustworthy? untrustworthy?
:size 15}]
(when short-chat-key
[text/text
{:size :paragraph-2
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}}
short-chat-key])]
(when accessory
[action-icon accessory theme])]])
@@ -38,7 +38,7 @@
(defn icon-container
[is-first?]
{:margin-left (if is-first? 4 2)
:margin-bottom 2})
:margin-bottom 4})
(defn time-text
[theme]
+1 -2
View File
@@ -51,8 +51,7 @@
[icons/icon :main-icons2/untrustworthy
{:size 12
:no-color true
:container-style (style/icon-container contact?)}])
]
:container-style (style/icon-container contact?)}])]
(when (and (not verified?) short-chat-key)
[text/text
{:weight :monospace
@@ -34,10 +34,9 @@
(= :pinned use-case)
theme)
container-style)}
[rn/image
{:style {:width 15 :height 15}
:accessibility-label :emoji
:source (reaction.resource/get-reaction emoji)}]
[rn/text
{:accessibility-label :emoji}
(reaction.resource/system-emojis emoji)]
[text/text
{:size :paragraph-2
:weight :semi-bold
@@ -4,6 +4,14 @@
(def ^:private reactions
(resolve-all-reactions))
(def system-emojis
{:reaction/thumbs-up "👍"
:reaction/thumbs-down "👎"
:reaction/love "❤"
:reaction/laugh "😂"
:reaction/sad "😢"
:reaction/angry "😡"})
(defn get-reaction
[reaction]
(assert (keyword? reaction) "Reaction should be a keyword")
@@ -19,6 +19,5 @@
(swap! pressed? not)
(when on-press
(on-press e)))}
[rn/image
{:source (reactions.resource/get-reaction emoji)
:style {:width 20 :height 20}}]])))
[rn/text
(reactions.resource/system-emojis emoji)]])))
@@ -8,19 +8,20 @@
(defn- category-internal
[{:keys [label data container-style] :as props}]
[rn/view {:style (merge (style/container label) container-style)}
(when label
[text/text
{:weight :medium
:size :paragraph-2
:style (style/label props)}
label])
[rn/view {:style (style/settings-items props)}
(for [item data]
^{:key item}
[:<>
[settings-item/view item]
(when-not (= item (last data))
[rn/view {:style (style/settings-separator props)}])])]])
(let [settings-item (filter identity data)]
[rn/view {:style (merge (style/container label) container-style)}
(when label
[text/text
{:weight :medium
:size :paragraph-2
:style (style/label props)}
label])
[rn/view {:style (style/settings-items props)}
(for [item settings-item]
^{:key item}
[:<>
[settings-item/view item]
(when-not (= item (last settings-item))
[rn/view {:style (style/settings-separator props)}])])]]))
(def settings-category (quo.theme/with-theme category-internal))
@@ -20,7 +20,7 @@
network-image]
:or {subtitle-type :default}}]
[rn/view {:style style/subtitle-container}
(when (not= :small size)
(when (and subtitle-type (not= :small size))
[rn/view {:style (style/subtitle-icon-container subtitle-type)}
(case subtitle-type
:icon [icons/icon icon
@@ -16,6 +16,12 @@
:padding-top 12
:padding-bottom 8})
(def gradient-bg
{:position :absolute
:top 0
:left 0
:right 0})
;;; Header
(def header-container
{:flex-direction :row
@@ -4,14 +4,13 @@
[oops.core :as oops]
[quo.components.avatars.account-avatar.view :as account-avatar]
[quo.components.buttons.button.view :as button]
[quo.components.gradient.gradient-cover.view :as gradient-cover]
[quo.components.markdown.text :as text]
[quo.components.share.qr-code.view :as qr-code]
[quo.components.share.share-qr-code.style :as style]
[quo.components.tabs.tab.view :as tab]
[quo.theme]
[react-native.blur :as blur]
[react-native.core :as rn]
[react-native.platform :as platform]
[reagent.core :as reagent]
[utils.i18n :as i18n]))
@@ -126,41 +125,44 @@
[{:keys [share-qr-type qr-image-uri component-width customization-color full-name
profile-picture emoji on-share-press]
:as props}]
[rn/view {:style style/content-container}
[rn/view
{:style style/share-qr-container}
[:<>
[rn/view {:style style/gradient-bg}
[gradient-cover/view {:customization-color customization-color :height 463}]]
[rn/view {:style style/content-container}
[rn/view
{:style style/share-qr-inner-container}
[account-avatar/view
{:customization-color customization-color
:emoji emoji
:size 32}]
[text/text
{:size :heading-2
:weight :semi-bold
:style {:margin-left 8}} full-name]]
[share-button {:on-press on-share-press}]]
(when (#{:wallet-legacy :wallet-multichain} share-qr-type)
[header props])
[quo.theme/provider {:theme :light}
[qr-code/view
{:qr-image-uri qr-image-uri
:size (style/qr-code-size component-width)
:avatar (if (= share-qr-type :profile)
:profile
:wallet-account)
:customization-color customization-color
:full-name full-name
:profile-picture profile-picture
:emoji emoji}]]
[rn/view {:style style/bottom-container}
(case share-qr-type
:profile [profile-bottom props]
:wallet-legacy [wallet-legacy-bottom props]
:wallet-multichain [wallet-multichain-bottom props]
nil)]])
{:style style/share-qr-container}
[rn/view
{:style style/share-qr-inner-container}
[account-avatar/view
{:customization-color customization-color
:emoji emoji
:size 32}]
[text/text
{:size :heading-2
:weight :semi-bold
:style {:margin-left 8}} full-name]]
[share-button {:on-press on-share-press}]]
(when (#{:wallet-legacy :wallet-multichain} share-qr-type)
[header props])
[quo.theme/provider {:theme :light}
[qr-code/view
{:qr-image-uri qr-image-uri
:size (style/qr-code-size component-width)
:avatar (if (= share-qr-type :profile)
:profile
:wallet-account)
:customization-color customization-color
:full-name full-name
:profile-picture profile-picture
:emoji emoji}]]
[rn/view {:style style/bottom-container}
(case share-qr-type
:profile [profile-bottom props]
:wallet-legacy [wallet-legacy-bottom props]
:wallet-multichain [wallet-multichain-bottom props]
nil)]]])
(defn view
(defn- view-internal
"Receives the following properties:
- type: :profile | :wallet-legacy | :wallet-multichain
- qr-image-uri: Image source value.
@@ -190,16 +192,9 @@
Sometimes while using a blur layer on top of another on Android, this component looks
bad because of the `blur/view`, so we can set `unblur-on-android? true` to fix it.
"
[{:keys [unblur-on-android?] :as props}]
[props]
(reagent/with-let [component-width (reagent/atom nil)
container-component (if (and platform/android? unblur-on-android?)
[rn/view {:background-color style/overlay-color}]
[blur/view
{:blur-radius 20
:blur-amount 20
:blur-type :transparent
:overlay-color style/overlay-color
:background-color style/overlay-color}])]
container-component [rn/view {:background-color style/overlay-color}]]
[quo.theme/provider {:theme :dark}
[rn/view
{:accessibility-label :share-qr-code
@@ -211,3 +206,6 @@
(-> props
(assoc :component-width @component-width)
(clojure.set/rename-keys {:type :share-qr-type}))]))]]))
(def view (quo.theme/with-theme view-internal))
+7 -6
View File
@@ -20,7 +20,7 @@
- :token-symbol - string"
[]
(let [container-width (reagent/atom 0)]
(fn [{:keys [options size blur? theme token-value token-symbol]
(fn [{:keys [options size blur? theme token-value token-img-src token-symbol]
:or {size :size-24}}]
[rn/view
{:on-layout #(reset! container-width
@@ -42,11 +42,12 @@
[])}
[rn/view {:style (style/container size options blur? theme)}
[token/view
{:style (style/token-img size)
:token token-symbol
:size (case size
:size-24 :size-20
:size-32 :size-28)}]
{:style (style/token-img size)
:token token-symbol
:size (case size
:size-24 :size-20
:size-32 :size-28)
:image-source token-img-src}]
[text/text
{:size :paragraph-2
:weight :medium
+20 -18
View File
@@ -15,9 +15,7 @@
[:token {:optional true} [:or keyword? string?]]
[:style {:optional true} map?]
;; Ignores `token` and uses this as parameter to `rn/image`'s source.
[:image-source {:optional true} [:or :schema.common/image-source :string]]
;; If true, adds `data:image/png;base64,` as prefix to the string passed as `image-source`
[:add-b64-prefix? {:optional true} boolean?]]]
[:image-source {:optional true} [:or :schema.common/image-source :string]]]]
:any])
(defn- size->number
@@ -52,28 +50,32 @@
first
string/capitalize)]])
(defn- normalize-b64-string
[b64-image]
(if (string/starts-with? b64-image "data:image/")
b64-image
(str b64-png-image-prefix b64-image)))
(defn view-internal
"Render a token image.
Props:
- style: extra styles to apply to the `rn/image` component.
- size: `:size-nn` or just `nn`, being `nn` any number. Defaults to 32.
- token: string or keyword, it can contain upper case letters or not.
E.g. all of these are valid and resolve to the same:
:token/snt | :snt | :SNT | \"SNT\" | \"snt\".
- image-source: Ignores `token` and uses this as parameter to `rn/image`'s source.
- add-b64-prefix?: If true, adds `data:image/png;base64,` as prefix to the string
passed as `image-source`.
- style: extra styles to apply to the `rn/image` component.
- size: `:size-nn` or just `nn`, being `nn` any number. Defaults to 32.
- token: string or keyword, it can contain upper case letters or not.
E.g. all of these are valid and resolve to the same:
:token/snt | :snt | :SNT | \"SNT\" | \"snt\".
- image-source: Ignores `token` and uses this as parameter to `rn/image`'s source, it
can be a b64 string representing an image.
"
[{:keys [token size style image-source add-b64-prefix?]
[{:keys [token size style image-source]
:or {size 32}}]
(let [b64-string (if (and image-source add-b64-prefix?)
(str b64-png-image-prefix image-source)
image-source)
source (or b64-string (token-loader/get-token-image token))]
(if source
(let [img-src (if (string? image-source)
(normalize-b64-string image-source)
image-source)]
(if-let [existing-source (or img-src (token-loader/get-token-image token))]
[rn/image
{:style (token-style style size)
:source source}]
:source existing-source}]
[temp-empty-symbol token size style])))
(def view (schema/instrument #'view-internal ?schema))
@@ -1,8 +1,7 @@
(ns quo.components.wallet.token-input.style
(:require
[quo.foundations.colors :as colors]
[quo.foundations.typography :as typography]
[react-native.platform :as platform]))
[quo.foundations.typography :as typography]))
(defn main-container
[width]
@@ -16,23 +15,51 @@
:flex-direction :row
:justify-content :space-between})
(defn token-name
[theme]
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)
:margin-right 8
:padding-bottom 2})
(def token-label-container
{:position :absolute
:left 40 ; token image size + margin
:right 0
:bottom 0
:top 0
:flex-direction :row
:align-items :flex-end})
(def text-input-container
{:position :absolute
:top 0
:bottom 0
:left 40 ; token image size + margin
:right 0})
(def text-input-dimensions
(-> typography/heading-1
(dissoc :letter-spacing)
(assoc :font-weight "600"
:margin-right 5
:padding-left 0
:padding-right 0
:padding-top 0
:padding-bottom 0
:height "100%")))
(defn text-input
[theme]
(merge typography/heading-1
{:font-weight "600"
:margin-left 8
:margin-right (if platform/ios? 6 4)
:color (colors/theme-colors colors/neutral-100 colors/white theme)
:padding 0
:text-align :center
:height "100%"}))
(assoc text-input-dimensions :color (colors/theme-colors colors/neutral-100 colors/white theme)))
(defn placeholder-text
[theme]
(colors/theme-colors colors/neutral-40 colors/neutral-50 theme))
(defn divider
[width theme]
{:height 1
:width width
:background-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)
:margin-vertical 8})
[theme]
{:margin-vertical 8
:background-color (colors/theme-colors colors/neutral-10 colors/neutral-90 theme)})
(def data-container
{:padding-top 4
@@ -40,3 +67,7 @@
:flex-direction :row
:justify-content :space-between
:align-items :center})
(defn fiat-amount
[theme]
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)})
+112 -63
View File
@@ -7,85 +7,134 @@
[quo.components.tags.network-tags.view :as network-tag]
[quo.components.utilities.token.view :as token]
[quo.components.wallet.token-input.style :as style]
[quo.foundations.colors :as colors]
[quo.foundations.common :as common]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[reagent.core :as reagent]))
(defn fiat-format
[currency num-value conversion]
(str (get common/currency-label currency) (.toFixed (* num-value conversion) 2)))
(defn crypto-format
[num-value conversion crypto-decimals token]
(str (.toFixed (/ num-value conversion) (or crypto-decimals 2))
" "
(string/upper-case (or (clj->js token) ""))))
(defn calc-value
[crypto? currency token value conversion]
(let [num-value (if (string? value) (parse-double (or value "0")) value)]
[{:keys [crypto? currency token value conversion crypto-decimals]}]
(let [num-value (if (string? value)
(or (parse-double value) 0)
value)]
(if crypto?
(str (get common/currency-label currency) (.toFixed (* num-value conversion) 2))
(str (.toFixed (/ num-value conversion) 2) " " (string/upper-case (or (clj->js token) ""))))))
(fiat-format currency num-value conversion)
(crypto-format num-value conversion crypto-decimals token))))
(defn- data-info
[{:keys [theme token crypto-decimals conversion networks title crypto? currency amount]}]
[rn/view {:style style/data-container}
[network-tag/view {:networks networks :title title}]
[text/text
{:size :paragraph-2
:weight :medium
:style (style/fiat-amount theme)}
(calc-value {:crypto? crypto?
:currency currency
:token token
:value amount
:conversion conversion
:crypto-decimals crypto-decimals})]])
(defn- token-name-text
[theme text]
[text/text
{:style (style/token-name theme)
:size :paragraph-2
:weight :semi-bold}
(string/upper-case (or (clj->js text) ""))])
(defn- token-label
[{:keys [theme value text]}]
[rn/view
{:style style/token-label-container
:pointer-events :none}
[rn/text-input
{:max-length 12
:style style/text-input-dimensions
:editable false
:placeholder "0"
:opacity 0
:value value}]
[token-name-text theme text]])
(defn input-section
[{:keys [on-change-text value value-atom]}]
(let [input-ref (atom nil)
set-ref #(reset! input-ref %)
focus-input #(when-let [ref ^js @input-ref]
(.focus ref))
controlled-input? (some? value)
handle-on-change-text (fn [v]
(when-not controlled-input?
(reset! value-atom v))
(when on-change-text
(on-change-text v)))]
(fn [{:keys [theme token customization-color show-keyboard? crypto? currency value]
:or {show-keyboard? true}}]
[rn/pressable
{:on-press focus-input
:style {:flex 1}}
[token/view
{:token token
:size :size-32}]
[rn/view {:style style/text-input-container}
[rn/text-input
(cond-> {:style (style/text-input theme)
:placeholder-text-color (style/placeholder-text theme)
:auto-focus true
:ref set-ref
:placeholder "0"
:keyboard-type :numeric
:max-length 12
:on-change-text handle-on-change-text
:selection-color customization-color
:show-soft-input-on-focus show-keyboard?}
controlled-input? (assoc :value value)
(not controlled-input?) (assoc :default-value @value-atom))]]
[token-label
{:theme theme
:text (if crypto? token currency)
:value (if controlled-input? value @value-atom)}]])))
(defn- view-internal
[{external-value :value}]
(let [width (:width (rn/get-window))
value (reagent/atom nil)
crypto? (reagent/atom true)
input-ref (atom nil)
controlled-input? (some? external-value)]
(fn [{:keys [theme token currency conversion networks title customization-color
on-change-text on-swap container-style show-keyboard?]
:or {show-keyboard? true}
external-value :value}]
[rn/view
{:style (merge
(style/main-container width)
container-style)}
[{:keys [on-swap]}]
(let [width (:width (rn/get-window))
value-atom (reagent/atom nil)
crypto? (reagent/atom true)
handle-on-swap (fn []
(swap! crypto? not)
(when on-swap
(on-swap @crypto?)))]
(fn [{:keys [theme container-style value] :as props}]
[rn/view {:style (merge (style/main-container width) container-style)}
[rn/view {:style style/amount-container}
[rn/pressable
{:on-press #(when @input-ref (.focus ^js @input-ref))
:style {:flex-direction :row
:flex-grow 1
:align-items :flex-end}}
[token/view {:token token :size :size-32}]
[rn/text-input
(cond-> {:auto-focus true
:ref #(reset! input-ref %)
:placeholder "0"
:placeholder-text-color (colors/theme-colors colors/neutral-40
colors/neutral-50
theme)
:keyboard-type :numeric
:max-length 12
:on-change-text (fn [v]
(when-not controlled-input?
(reset! value v))
(when on-change-text
(on-change-text v)))
:style (style/text-input theme)
:selection-color customization-color
:show-soft-input-on-focus show-keyboard?}
controlled-input? (assoc :value external-value)
(not controlled-input?) (assoc :default-value @value))]
[text/text
{:size :paragraph-2
:weight :semi-bold
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)
:margin-right 8
:padding-bottom 2}}
(string/upper-case (or (clj->js (if @crypto? token currency)) ""))]]
[input-section
(assoc props
:value-atom value-atom
:crypto? @crypto?)]
[button/button
{:icon true
:icon-only? true
:size 32
:on-press (fn []
(swap! crypto? not)
(when on-swap
(on-swap @crypto?)))
:on-press handle-on-swap
:type :outline
:accessibility-label :reorder}
:i/reorder]]
[divider-line/view {:container-style {:margin-vertical 8}}]
[rn/view {:style style/data-container}
[network-tag/view {:networks networks :title title}]
[text/text
{:size :paragraph-2
:weight :medium
:style {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}}
(calc-value @crypto? currency token (or external-value @value) conversion)]]])))
[divider-line/view {:container-style (style/divider theme)}]
[data-info
(assoc props
:crypto? @crypto?
:amount (or value @value-atom))]])))
(def view (quo.theme/with-theme view-internal))
+5 -3
View File
@@ -80,13 +80,14 @@
quo.components.list-items.community.view
quo.components.list-items.dapp.view
quo.components.list-items.menu-item
quo.components.list-items.network-list.view
quo.components.list-items.preview-list.view
quo.components.list-items.quiz-item.view
quo.components.list-items.saved-address.view
quo.components.list-items.saved-contact-address.view
quo.components.list-items.token-network.view
quo.components.list-items.token-value.view
quo.components.list-items.user-list
quo.components.list-items.user
quo.components.loaders.skeleton-list.view
quo.components.markdown.list.view
quo.components.markdown.text
@@ -299,16 +300,17 @@
(def account-list-card quo.components.list-items.account-list-card.view/view)
(def address quo.components.list-items.address.view/view)
(def channel quo.components.list-items.channel.view/view)
(def community-list-item quo.components.list-items.community.view/view)
(def community-list quo.components.list-items.community.view/view)
(def dapp quo.components.list-items.dapp.view/view)
(def menu-item quo.components.list-items.menu-item/menu-item)
(def network-list quo.components.list-items.network-list.view/view)
(def preview-list quo.components.list-items.preview-list.view/view)
(def quiz-item quo.components.list-items.quiz-item.view/view)
(def saved-address quo.components.list-items.saved-address.view/view)
(def saved-contact-address quo.components.list-items.saved-contact-address.view/view)
(def token-network quo.components.list-items.token-network.view/view)
(def token-value quo.components.list-items.token-value.view/view)
(def user-list quo.components.list-items.user-list/user-list)
(def user quo.components.list-items.user/user)
;;;; Loaders
(def skeleton-list quo.components.loaders.skeleton-list.view/view)
+1
View File
@@ -48,6 +48,7 @@
quo.components.list-items.channel.component-spec
quo.components.list-items.community.component-spec
quo.components.list-items.dapp.component-spec
quo.components.list-items.network-list.component-spec
quo.components.list-items.quiz-item.component-spec
quo.components.list-items.saved-address.component-spec
quo.components.list-items.saved-contact-address.component-spec
+13 -11
View File
@@ -17,19 +17,19 @@
(fn [^js data]
(let [^js section (.-section data)]
(reagent/as-element [f
{:title (.-title section)
{:index (.-index section)
:title (.-title section)
:data (.-data section)}]))))
(defn- wrap-per-section-render-fn
[props]
(update
(if-let [f (:render-fn props)]
(assoc (dissoc props :render-fn :render-data)
:renderItem
(memo-wrap-render-fn f (:render-data props)))
props)
:data
to-array))
[index props]
(-> (if-let [f (:render-fn props)]
(assoc (dissoc props :render-fn :render-data)
:renderItem
(memo-wrap-render-fn f (:render-data props)))
props)
(update :data to-array)
(assoc :index index)))
(defn section-list
"A wrapper for SectionList.
@@ -42,6 +42,8 @@
props
(when render-section-footer-fn
{:renderSectionFooter (wrap-render-section-header-fn render-section-footer-fn)})
{:sections (clj->js (map wrap-per-section-render-fn sections))
{:sections (->> sections
(map-indexed wrap-per-section-render-fn)
(clj->js))
:renderSectionHeader (wrap-render-section-header-fn render-section-header-fn)
:style style})])
@@ -0,0 +1,5 @@
(ns status-im.common.contact-list.style)
(defn contacts-section-header
[first-item?]
{:padding-top (if first-item? 0 8)})
+6 -3
View File
@@ -1,7 +1,10 @@
(ns status-im.common.contact-list.view
(:require
[quo.core :as quo]))
[quo.core :as quo]
[react-native.core :as rn]
[status-im.common.contact-list.style :as style]))
(defn contacts-section-header
[{:keys [title]}]
[quo/divider-label title])
[{:keys [title index]}]
[rn/view (style/contacts-section-header (= index 0))
[quo/divider-label title]])
@@ -6,17 +6,21 @@
(defn contact-list-item
[{:keys [on-press on-long-press accessory]}
{:keys [primary-name secondary-name public-key compressed-key ens-verified added?]}]
(let [photo-path (rf/sub [:chats/photo-path public-key])
online? (rf/sub [:visibility-status-updates/online? public-key])]
[quo/user-list
{:short-chat-key (address/get-shortened-compressed-key (or compressed-key public-key))
:primary-name primary-name
:secondary-name secondary-name
:photo-path photo-path
:online? online?
:verified? ens-verified
:contact? added?
:on-press on-press
:on-long-press on-long-press
:accessory accessory}]))
{:keys [primary-name secondary-name public-key compressed-key ens-verified added?]}
theme]
(let [photo-path (rf/sub [:chats/photo-path public-key])
online? (rf/sub [:visibility-status-updates/online? public-key])
customization-color (rf/sub [:profile/customization-color])]
[quo/user
{:customization-color customization-color
:theme theme
:short-chat-key (address/get-shortened-compressed-key (or compressed-key public-key))
:primary-name primary-name
:secondary-name secondary-name
:photo-path photo-path
:online? online?
:verified? ens-verified
:contact? added?
:on-press on-press
:on-long-press on-long-press
:accessory accessory}]))
+3
View File
@@ -38,6 +38,9 @@
:no-contacts
{:light (js/require "../resources/images/ui2/no-contacts-light.png")
:dark (js/require "../resources/images/ui2/no-contacts-dark.png")}
:no-contacts-to-chat
{:light (js/require "../resources/images/ui2/no-contacts-to-chat-light.png")
:dark (js/require "../resources/images/ui2/no-contacts-to-chat-dark.png")}
:keycard-chip
{:light (js/require "../resources/images/ui2/keycard-chip-light.png")
:dark (js/require "../resources/images/ui2/keycard-chip-dark.png")}
+2 -8
View File
@@ -259,12 +259,6 @@
{:type :wallet-account
:account (when account (string/lower-case account))})
(defn community-route-type
[route-params]
(if (string/starts-with? (:community-id route-params) "z")
:desktop-community
:community))
(defn handle-uri
[chain chats uri cb]
(let [{:keys [handler route-params query-params]} (match-uri uri)]
@@ -297,14 +291,14 @@
(cb {:type handler :community-id (:community-id route-params)})
(and (= handler :community) (:community-id route-params))
(cb {:type (community-route-type route-params)
(cb {:type :community
:community-id (:community-id route-params)})
(and (= handler :community-chat) (:community-channel-id route-params) (:community-id route-params))
(match-community-channel-async route-params cb)
(and (= handler :community-chat) (:community-id route-params))
(cb {:type (community-route-type route-params)
(cb {:type :community
:community-id (:community-id route-params)})
;; NOTE: removed in `match-uri`, might need this in the future
+27
View File
@@ -0,0 +1,27 @@
(ns status-im.common.serialization
(:require
[native-module.core :as native-module]
[status-im.constants :as constants]
[utils.re-frame :as rf]
[utils.transforms :as transforms]))
(rf/reg-fx :serialization/deserialize-and-compress-key
(fn [{:keys [serialized-key on-success on-error]}]
(native-module/deserialize-and-compress-key
serialized-key
(fn [resp]
(let [{:keys [error]} (transforms/json->clj resp)]
(if error
(on-error error)
(on-success resp)))))))
(rf/reg-fx :serialization/decompress-public-key
(fn [{:keys [compressed-key on-success on-error]}]
(native-module/compressed-key->public-key
compressed-key
constants/deserialization-key
(fn [resp]
(let [{:keys [error]} (transforms/json->clj resp)]
(if error
(on-error error)
(on-success (str "0x" (subs resp 5)))))))))
+4 -31
View File
@@ -2,12 +2,12 @@
(:require
[clojure.string :as string]
[goog.string :as gstring]
[native-module.core :as native-module]
[re-frame.core :as re-frame]
[react-native.async-storage :as async-storage]
[react-native.core :as rn]
[schema.core :as schema]
[status-im.constants :as constants]
[status-im.contexts.communities.events :as communities.events]
[status-im.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.ethereum.chain :as chain]
@@ -68,32 +68,6 @@
(log/info "universal-links: handling community request " community-id)
(navigation/navigate-to cofx :community-requests-to-join {:community-id community-id}))
(rf/defn handle-community
[cofx {:keys [community-id]}]
(log/info "universal-links: handling community" community-id)
(navigation/navigate-to cofx :community {:community-id community-id}))
(rf/defn handle-navigation-to-desktop-community-from-mobile
{:events [:handle-navigation-to-desktop-community-from-mobile]}
[cofx deserialized-key]
(rf/merge
cofx
{:dispatch [:navigate-to :community-overview deserialized-key]}
(navigation/pop-to-root :shell-stack)))
(rf/defn handle-desktop-community
[_ {:keys [community-id]}]
(native-module/deserialize-and-compress-key
community-id
(fn [deserialized-key]
(rf/dispatch [:chat.ui/fetch-community (str deserialized-key)])
(rf/dispatch [:handle-navigation-to-desktop-community-from-mobile (str deserialized-key)]))))
(rf/defn handle-community-chat
[cofx {:keys [chat-id]}]
(log/info "universal-links: handling community chat" chat-id)
{:dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]})
(rf/defn handle-view-profile
[{:keys [db] :as cofx} {:keys [public-key ens-name]}]
(log/info "universal-links: handling view profile" public-key)
@@ -143,14 +117,13 @@
(rf/defn on-handle
{:events [::match-value]}
[cofx url {:keys [type] :as data}]
[cofx url {:keys [type chat-id] :as data}]
(case type
:group-chat (handle-group-chat cofx data)
:private-chat (handle-private-chat cofx data)
:community-requests (handle-community-requests cofx data)
:community (handle-community cofx data)
:desktop-community (handle-desktop-community cofx data)
:community-chat (handle-community-chat cofx data)
:community (communities.events/navigate-to-serialized-community cofx data)
:community-chat {:dispatch [:communities/navigate-to-community-chat chat-id :pop-to-root]}
:contact (handle-view-profile cofx data)
:browser (handle-browse cofx data)
:eip681 (handle-eip681 cofx data)
+7
View File
@@ -338,6 +338,13 @@
(def ^:const local-pairing-action-sync-device 3)
(def ^:const local-pairing-action-pairing-installation 4)
(def ^:const serialization-key
"We pass this serialization key as a parameter to MultiformatSerializePublicKey
function at status-go, This key determines the output base of the serialization.
according to https://specs.status.im/spec/2#public-key-serialization we serialize
keys with base58btc encoding"
"z")
(def ^:const deserialization-key
"We pass this deserialization key as a parameter to MultiformatDeserializePublicKey
function at status-go, This key determines the output base of the deserialization.
@@ -29,7 +29,7 @@
{:icon :i/add-user
:accessibility-label :add-a-contact
:label (i18n/label :t/add-a-contact)
:sub-label (i18n/label :t/enter-a-chat-key)
:sub-label (i18n/label :t/enter-chat-key)
:add-divider? true
:on-press #(debounce/dispatch-and-chill
[:open-modal :new-contact]
@@ -1,21 +1,7 @@
(ns status-im.contexts.chat.home.add-new-contact.effects
(:require
[legacy.status-im.ethereum.ens :as ens]
[native-module.core :as native-module]
[status-im.constants :as constants]
[utils.re-frame :as rf]
[utils.transforms :as transforms]))
(rf/reg-fx :effects.contacts/decompress-public-key
(fn [{:keys [compressed-key on-success on-error]}]
(native-module/compressed-key->public-key
compressed-key
constants/deserialization-key
(fn [resp]
(let [{:keys [error]} (transforms/json->clj resp)]
(if error
(on-error error)
(on-success (str "0x" (subs resp 5)))))))))
[utils.re-frame :as rf]))
(rf/reg-fx :effects.contacts/resolve-public-key-from-ens
(fn [{:keys [chain-id ens on-success on-error]}]
@@ -104,7 +104,7 @@
:empty {:db (dissoc db :contacts/new-identity)}
(:valid :invalid) {:db (assoc db :contacts/new-identity contact)}
:decompress-key {:db (assoc db :contacts/new-identity contact)
:effects.contacts/decompress-public-key
:serialization/decompress-public-key
{:compressed-key id
:on-success
(dispatcher :contacts/set-new-identity-success input)
@@ -132,7 +132,7 @@
:type :compressed-key
:public-key nil ; not yet...
:state :decompress-key}))
:effects.contacts/decompress-public-key
:serialization/decompress-public-key
{:compressed-key user-ckey
:on-success [:contacts/set-new-identity-success user-ckey]
:on-error [:contacts/set-new-identity-error user-ckey]}})))
@@ -21,7 +21,7 @@
[]
{:size :heading-1
:weight :semi-bold
:style {:margin-top 32
:style {:margin-top 28
:margin-bottom 6
:color (colors/theme-colors colors/neutral-100 colors/white)}})
@@ -16,33 +16,38 @@
(defn- no-contacts-view
[{:keys [theme]}]
[rn/view
{:style (style/no-contacts)}
[rn/image {:source (resources/get-themed-image :no-contacts theme)}]
[quo/text
{:weight :semi-bold
:size :paragraph-1
:style {:margin-bottom 2
:margin-top 12}}
(i18n/label :t/you-have-no-contacts)]
[quo/text
{:weight :regular
:size :label}
(i18n/label :t/dont-yell-at-me)]
[quo/button
{:type :primary
:size 32
:container-style {:margin-top 20
:margin-bottom 12}
:on-press #(rf/dispatch [:invite.events/share-link])}
(i18n/label :t/invite-friends)]
[quo/button
{:type :grey
:size 32
:on-press #(do
(rf/dispatch [:navigate-back])
(rf/dispatch [:open-modal :new-contact]))}
(i18n/label :t/add-a-contact)]])
(let [customization-color (rf/sub [:profile/customization-color])]
[rn/view
{:style (style/no-contacts)}
[rn/image {:source (resources/get-themed-image :no-contacts-to-chat theme)}]
[quo/text
{:weight :semi-bold
:size :paragraph-1
:style {:margin-bottom 2
:margin-top 12}}
(i18n/label :t/you-have-no-contacts)]
[quo/text
{:weight :regular
:size :paragraph-2}
(i18n/label :t/dont-yell-at-me)]
[quo/button
{:customization-color customization-color
:theme theme
:type :primary
:size 32
:container-style {:margin-top 20
:margin-bottom 12}
:on-press #(rf/dispatch [:invite.events/share-link])}
(i18n/label :t/invite-friends)]
[quo/button
{:customization-color customization-color
:theme theme
:type :grey
:size 32
:on-press #(do
(rf/dispatch [:navigate-back])
(rf/dispatch [:open-modal :new-contact]))}
(i18n/label :t/add-a-contact)]]))
(defn contact-item-render
[_]
+5 -3
View File
@@ -73,7 +73,7 @@
:shared-value scroll-shared-value})}])))
(defn contact-item-render
[{:keys [public-key] :as item}]
[{:keys [public-key] :as item} theme]
(let [current-pk (rf/sub [:multiaccount/public-key])
show-profile-actions #(rf/dispatch [:show-bottom-sheet
{:content (fn [] [actions/contact-actions item])}])]
@@ -83,7 +83,8 @@
:on-long-press show-profile-actions
:accessory {:type :options
:on-press show-profile-actions}})
item]))
item
theme]))
(defn contacts
[{:keys [theme pending-contact-requests set-scroll-ref scroll-shared-value]}]
@@ -105,7 +106,8 @@
:sections items
:sticky-section-headers-enabled false
:render-section-header-fn contact-list/contacts-section-header
:render-fn contact-item-render
:render-fn (fn [data]
(contact-item-render data theme))
:scroll-event-throttle 8
:on-scroll #(common.banner/set-scroll-shared-value
{:scroll-input (oops/oget % "nativeEvent.contentOffset.y")
@@ -30,7 +30,7 @@
(js/setTimeout #(reanimated/set-shared-value background-y
(- window-height))
300)
(rf/dispatch [:chat.ui/replace-mentions-and-send-current-message])
(rf/dispatch [:chat.ui/send-current-message])
(rf/dispatch [:chat.ui/set-input-maximized false])
(rf/dispatch [:chat.ui/set-input-content-height comp-constants/input-height])
(rf/dispatch [:chat.ui/set-chat-input-text nil])
@@ -114,9 +114,8 @@
[{:keys [input-ref]}
{:keys [text-value saved-cursor-position cursor-position]}
{:keys [edit input-with-mentions]}
messages-list-on-layout-finished?]
(let [mention? (some #(= :mention (first %)) (seq input-with-mentions))
composer-just-opened? (not @messages-list-on-layout-finished?)]
chat-screen-layout-calculations-complete?]
(let [mention? (some #(= :mention (first %)) (seq input-with-mentions))]
(rn/use-effect
(fn []
(let [mention-text (reduce (fn [acc item]
@@ -128,7 +127,9 @@
;; NOTE: using text-value for cases when the user
;; leaves the app with an unfinished edit and re-opens
;; the chat.
(and (seq @text-value) composer-just-opened?)
(and (seq @text-value)
(not (reanimated/get-shared-value
chat-screen-layout-calculations-complete?)))
@text-value
:else (get-in edit [:content :text]))
selection-pos (count edit-text)
@@ -150,7 +151,9 @@
;; UI. This way, `on-focus` will trigger first without changing the height, after which
;; `on-content-size-change` will animate the height of the input based on the injected
;; text.
(js/setTimeout #(do (when @messages-list-on-layout-finished? (.focus ^js @input-ref))
(js/setTimeout #(do (when (reanimated/get-shared-value
chat-screen-layout-calculations-complete?)
(.focus ^js @input-ref))
(reagent/next-tick inject-edit-text))
600))))
[(:message-id edit)])))
@@ -159,12 +162,12 @@
[{:keys [input-ref]}
{:keys [container-opacity]}
{:keys [reply]}
messages-list-on-layout-finished?]
chat-screen-layout-calculations-complete?]
(rn/use-effect
(fn []
(when reply
(reanimated/animate container-opacity 1))
(when (and reply @input-ref @messages-list-on-layout-finished?)
(when (and reply @input-ref (reanimated/get-shared-value chat-screen-layout-calculations-complete?))
(js/setTimeout #(.focus ^js @input-ref) 600)))
[(:message-id reply)]))
@@ -207,24 +207,12 @@
(link-preview/reset-unfurled)
(cancel-message-edit)))
(rf/defn replace-mentions-and-send-current-message
(rf/defn send-current-message
"Sends message from current chat input"
{:events [:chat.ui/replace-mentions-and-send-current-message]}
[{{:keys [current-chat-id] :as db} :db}]
(let [{:keys [input-text metadata]} (get-in db [:chat/inputs current-chat-id])]
{:json-rpc/call [{:method "wakuext_chatMentionReplaceWithPublicKey"
:params [current-chat-id input-text]
:on-error #(log/error "[wakuext_chatMentionReplaceWithPublicKey] on-error" %)
:on-success #(rf/dispatch [:chat.ui/send-current-message-with-mentions
current-chat-id
(:editing-message metadata)
input-text
%])}]}))
(rf/defn send-current-message-with-mentions
{:events [:chat.ui/send-current-message-with-mentions]}
[{:keys [db] :as cofx} current-chat-id editing-message input-text new-text]
(rf/merge cofx
(if editing-message
(send-edited-message new-text editing-message)
(send-messages new-text current-chat-id))))
{:events [:chat.ui/send-current-message]}
[{{:keys [current-chat-id] :as db} :db :as cofx}]
(let [{:keys [input-text metadata]} (get-in db [:chat/inputs current-chat-id])
editing-message (:editing-message metadata)]
(if editing-message
(send-edited-message cofx input-text editing-message)
(send-messages cofx input-text current-chat-id))))
@@ -31,12 +31,12 @@
(defn sheet-component
[{:keys [insets
chat-list-scroll-y
chat-screen-layout-calculations-complete?
window-height
blur-height
opacity
background-y
theme
messages-list-on-layout-finished?]} props state]
theme]} props state]
(let [subscriptions (utils/init-subs)
content-height (reagent/atom (or (:input-content-height ; Actual text height
subscriptions)
@@ -75,8 +75,8 @@
animations
dimensions
subscriptions)
(effects/use-edit props state subscriptions messages-list-on-layout-finished?)
(effects/use-reply props animations subscriptions messages-list-on-layout-finished?)
(effects/use-edit props state subscriptions chat-screen-layout-calculations-complete?)
(effects/use-reply props animations subscriptions chat-screen-layout-calculations-complete?)
(effects/update-input-mention props state subscriptions)
(effects/link-previews props state animations subscriptions)
(effects/use-images props state animations subscriptions)
@@ -145,22 +145,20 @@
[:f> actions/view props state animations window-height insets subscriptions]]]]]))
(defn f-composer
[{:keys [insets chat-list-scroll-y messages-list-on-layout-finished?]}]
[props]
(let [window-height (:height (rn/get-window))
theme (quo.theme/use-theme-value)
opacity (reanimated/use-shared-value 0)
background-y (reanimated/use-shared-value (- window-height)) ; Y position of background
; overlay
blur-height (reanimated/use-shared-value (+ constants/composer-default-height
(:bottom insets)))
extra-params {:insets insets
:window-height window-height
:chat-list-scroll-y chat-list-scroll-y
:blur-height blur-height
:opacity opacity
:background-y background-y
:theme theme
:messages-list-on-layout-finished? messages-list-on-layout-finished?}
(:bottom (:insets props))))
extra-params (assoc props
:window-height window-height
:blur-height blur-height
:opacity opacity
:background-y background-y
:theme theme)
props (utils/init-non-reactive-state)
state (utils/init-reactive-state)]
[rn/view (when platform/ios? {:style {:z-index 1}})
@@ -52,7 +52,7 @@
(get constants/reactions (:emoji-id reaction))))
(defn- view-internal
[{:keys [message-id chat-id pinned-by theme]} user-message-content]
[{:keys [message-id chat-id pinned-by preview? theme]} user-message-content]
(let [reactions (rf/sub [:chats/message-reactions message-id chat-id])]
[:<>
(when (seq reactions)
@@ -60,7 +60,7 @@
{:container-style {:margin-left 44
:margin-top 8}
:reactions (map add-emoji-key reactions)
:add-reaction? true
:add-reaction? (not preview?)
:use-case (when pinned-by :pinned)
:on-press #(on-press (assoc % :message-id message-id))
:on-long-press #(on-long-press (assoc %
@@ -2,6 +2,8 @@
(:require
[quo.foundations.colors :as colors]))
(def ^:private message-padding-scaling-ratio 4.5)
(defn message-container
([]
(message-container false nil nil false))
@@ -16,10 +18,16 @@
(assoc :margin-top 4))))
(defn user-message-content
[{:keys [outgoing outgoing-status]}]
[{:keys [outgoing outgoing-status six-reactions? window-scale small-screen?]}]
{:border-radius 16
:padding-horizontal 8
:padding-vertical 4
:padding-top 4
:padding-bottom (if (or small-screen?
(and
(> 3 window-scale)
six-reactions?))
(* message-padding-scaling-ratio window-scale)
4)
:opacity (if (and outgoing (= outgoing-status :sending))
0.5
1)})
@@ -119,31 +119,38 @@
[]
(let [show-delivery-state? (reagent/atom false)]
(fn [{:keys [message-data context keyboard-shown? show-reactions? in-reaction-and-action-menu?
show-user-info? theme]}]
show-user-info? preview? theme]}]
(let [{:keys [content-type quoted-message content
outgoing outgoing-status pinned-by]} message-data
first-image (first (:album message-data))
outgoing-status (if (= content-type
constants/content-type-album)
(:outgoing-status first-image)
outgoing-status)
outgoing (if (= content-type
constants/content-type-album)
(:outgoing first-image)
outgoing)
context (assoc context
:on-long-press
#(on-long-press message-data
context
keyboard-shown?))
response-to (:response-to content)
height (rf/sub [:dimensions/window-height])
{window-width :width} (rn/get-window)
message-container-data {:window-width window-width
:padding-right 20
:padding-left 20
:avatar-container-width 32
:message-margin-left 8}]
outgoing outgoing-status pinned-by
message-id chat-id]} message-data
first-image (first (:album message-data))
outgoing-status (if (= content-type
constants/content-type-album)
(:outgoing-status first-image)
outgoing-status)
outgoing (if (= content-type
constants/content-type-album)
(:outgoing first-image)
outgoing)
context (assoc context
:on-long-press
#(on-long-press message-data
context
keyboard-shown?))
response-to (:response-to content)
height (rf/sub [:dimensions/window-height])
{window-width :width
window-scale :scale} (rn/get-window)
message-container-data {:window-width window-width
:padding-right 20
:padding-left 20
:avatar-container-width 32
:message-margin-left 8}
reactions (rf/sub [:chats/message-reactions message-id
chat-id])
six-reactions? (-> reactions
count
(= 6))]
[rn/touchable-highlight
{:accessibility-label (if (and outgoing (= outgoing-status :sending))
:message-sending
@@ -152,7 +159,10 @@
:style (style/user-message-content
{:first-in-group? (:first-in-group? message-data)
:outgoing outgoing
:outgoing-status outgoing-status})
:outgoing-status outgoing-status
:small-screen? rn/small-screen?
:window-scale window-scale
:six-reactions? six-reactions?})
:on-press (fn []
(if (and platform/ios? keyboard-shown?)
(do
@@ -212,7 +222,7 @@
(when @show-delivery-state?
[status/status outgoing-status])])]
(when show-reactions?
[reactions/message-reactions-row message-data
[reactions/message-reactions-row (assoc message-data :preview? preview?)
[rn/view {:pointer-events :none}
[user-message-content-internal
{:theme theme
@@ -226,22 +236,24 @@
(defn on-long-press
[{:keys [deleted? deleted-for-me?] :as message-data} context keyboard-shown?]
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch [:show-bottom-sheet
{:content (drawers/reactions-and-actions message-data context)
:border-radius 16
:selected-item
(if (or deleted? deleted-for-me?)
(fn [] [content.deleted/deleted-message message-data])
(fn []
[rn/view
{:pointer-events :none
:padding-top 4}
[user-message-content
{:message-data message-data
:context context
:keyboard-shown? keyboard-shown?
:show-reactions? true
:show-user-info? true}]]))}]))
(rf/dispatch
[:show-bottom-sheet
{:content (drawers/reactions-and-actions message-data context)
:border-radius 16
:selected-item
(if (or deleted? deleted-for-me?)
(fn [] [content.deleted/deleted-message message-data])
(fn []
[rn/view
{:pointer-events :none
:padding-top 4}
[user-message-content
{:message-data message-data
:context context
:keyboard-shown? keyboard-shown?
:show-reactions? true
:show-user-info? true
:preview? true}]]))}]))
(defn system-message?
[content-type]
@@ -8,9 +8,7 @@
:justify-content :center})
(def tab-icon
{:margin-right 4
:width 20
:height 20})
{:margin-right 4})
(defn tab-count
[active? theme]
@@ -34,10 +34,10 @@
{:id reaction-type-int
:accessibility-label (keyword (str "authors-for-reaction-" reaction-type-int))
:label [rn/view {:style style/tab}
[rn/image
{:source (reactions.resource/get-reaction
(get constants/reactions reaction-type-int))
:style style/tab-icon}]
[rn/text
{:style style/tab-icon}
(reactions.resource/system-emojis
(get constants/reactions reaction-type-int))]
[quo/text
{:weight :medium
:size :paragraph-1
@@ -1,8 +1,6 @@
(ns status-im.contexts.chat.messenger.messages.link-preview.events
(:require
[camel-snake-kebab.core :as csk]
[legacy.status-im.mailserver.core :as mailserver]
[schema.core :as schema]
[status-im.contexts.profile.settings.events :as profile.settings.events]
[taoensso.timbre :as log]
[utils.collection]
@@ -42,96 +40,6 @@
(boolean enabled?)
{}))
(defn community-resolved
[{:keys [db]} [community-id community]]
(when community
{:db (update db :communities/resolve-community-info dissoc community-id)
:fx [[:dispatch [:communities/handle-community community]]
[:dispatch
[:chat.ui/cache-link-preview-data (community-link community-id) community]]]}))
(rf/reg-event-fx :chat.ui/community-resolved community-resolved)
(defn community-failed-to-resolve
[{:keys [db]} [community-id]]
{:db (update db :communities/resolve-community-info dissoc community-id)})
(rf/reg-event-fx :chat.ui/community-failed-to-resolve community-failed-to-resolve)
(defn fetch-community
[{:keys [db]} [community-id]]
(when community-id
{:db (assoc-in db [:communities/resolve-community-info community-id] true)
:json-rpc/call [{:method "wakuext_fetchCommunity"
:params [{:CommunityKey community-id
:TryDatabase true
:WaitForResponse true}]
:on-success (fn [community]
(rf/dispatch [:chat.ui/community-resolved community-id community]))
:on-error (fn [err]
(rf/dispatch [:chat.ui/community-failed-to-resolve community-id])
(log/error {:message
"Failed to request community info from mailserver"
:error err}))}]}))
(schema/=> fetch-community
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema [:catn [:community-id [:? :string]]]]]]
[:maybe
[:map
[:db map?]
[:json-rpc/call :schema.common/rpc-call]]]])
(rf/reg-event-fx :chat.ui/fetch-community fetch-community)
(defn spectate-community-success
[{:keys [db]} [{:keys [communities]}]]
(when-let [community (first communities)]
{:db (-> db
(assoc-in [:communities (:id community) :spectated] true)
(assoc-in [:communities (:id community) :spectating] false))
:fx [[:dispatch [:communities/handle-community community]]
[:dispatch [::mailserver/request-messages]]]}))
(rf/reg-event-fx :chat.ui/spectate-community-success spectate-community-success)
(defn spectate-community-failed
[{:keys [db]} [community-id]]
{:db (assoc-in db [:communities community-id :spectating] false)})
(rf/reg-event-fx :chat.ui/spectate-community-failed spectate-community-failed)
(defn spectate-community
[{:keys [db]} [community-id]]
(let [{:keys [spectated spectating joined]} (get-in db [:communities community-id])]
(when (and (not joined) (not spectated) (not spectating))
{:db (assoc-in db [:communities community-id :spectating] true)
:json-rpc/call [{:method "wakuext_spectateCommunity"
:params [community-id]
:on-success [:chat.ui/spectate-community-success]
:on-error (fn [err]
(log/error {:message
"Failed to spectate community"
:error err})
(rf/dispatch [:chat.ui/spectate-community-failed
community-id]))}]})))
(schema/=> spectate-community
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema [:catn [:community-id [:? :string]]]]]]
[:maybe
[:map
[:db map?]
[:json-rpc/call :schema.common/rpc-call]]]])
(rf/reg-event-fx :chat.ui/spectate-community spectate-community)
(rf/defn save-link-preview-whitelist
{:events [:chat.ui/link-preview-whitelist-received]}
[{:keys [db]} whitelist]
@@ -1,139 +0,0 @@
(ns status-im.contexts.chat.messenger.messages.link-preview.events-test
(:require [cljs.test :as t]
[legacy.status-im.mailserver.core :as mailserver]
matcher-combinators.test
[status-im.contexts.chat.messenger.messages.link-preview.events :as sut]))
(t/deftest fetch-community
(t/testing "with community id"
(t/testing "update resolving indicator in db"
(t/is (match?
{:db {:communities/resolve-community-info {"community-id" true}}}
(sut/fetch-community {} ["community-id"]))))
(t/testing "call the fetch community rpc method with correct community id"
(t/is (match?
{:json-rpc/call [{:method "wakuext_fetchCommunity"
:params [{:CommunityKey "community-id"
:TryDatabase true
:WaitForResponse true}]}]}
(sut/fetch-community {} ["community-id"])))))
(t/testing "with no community id"
(t/testing "do nothing"
(t/is (match?
nil
(sut/fetch-community {} []))))))
(t/deftest community-failed-to-resolve
(t/testing "given a community id"
(t/testing "remove community id from resolving indicator in db"
(t/is (match?
nil
(get-in (sut/community-failed-to-resolve {:db {:communities/resolve-community-info
{"community-id" true}}}
["community-id"])
[:db :communities/resolve-community-info "community-id"]))))))
(t/deftest community-resolved
(with-redefs [sut/community-link (fn [id] (str "community-link+" id))]
(t/testing "given a community"
(let [cofx {:db {:communities/resolve-community-info {"community-id" true}}}
arg ["community-id" {:id "community-id"}]]
(t/testing "remove community id from resolving indicator in db"
(t/is (match?
nil
(get-in (sut/community-resolved cofx arg)
[:db :communities/resolve-community-info "community-id"]))))
(t/testing "dispatch fxs"
(t/is (match?
{:fx [[:dispatch [:communities/handle-community {:id "community-id"}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id "community-id"}]]]}
(sut/community-resolved cofx arg))))))
(t/testing "given a joined community"
(let [cofx {:db {:communities/resolve-community-info {"community-id" true}}}
arg ["community-id" {:id "community-id" :joined true}]]
(t/testing "dispatch fxs, do not spectate community"
(t/is (match?
{:fx [[:dispatch [:communities/handle-community {:id "community-id"}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id "community-id"}]]]}
(sut/community-resolved cofx arg))))))
(t/testing "given a token-gated community"
(let [cofx {:db {:communities/resolve-community-info {"community-id" true}}}
arg ["community-id" {:id "community-id" :tokenPermissions [1]}]]
(t/testing "dispatch fxs, do not spectate community"
(t/is (match?
{:fx [[:dispatch [:communities/handle-community {:id "community-id"}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id "community-id"}]]]}
(sut/community-resolved cofx arg))))))
(t/testing "given nil community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/community-resolved {} ["community-id" nil])))))))
(t/deftest spectate-community
(t/testing "given a joined community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/spectate-community {:db {:communities {"community-id" {:joined true}}}}
["community-id"])))))
(t/testing "given a spectated community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/spectate-community {:db {:communities {"community-id" {:spectated true}}}}
["community-id"])))))
(t/testing "given a spectating community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/spectate-community {:db {:communities {"community-id" {:spectating true}}}}
["community-id"])))))
(t/testing "given a community"
(t/testing "mark community spectating"
(t/is (match?
{:db {:communities {"community-id" {:spectating true}}}}
(sut/spectate-community {:db {:communities {"community-id" {}}}} ["community-id"]))))
(t/testing "call spectate community rpc with correct community id"
(t/is (match?
{:json-rpc/call [{:method "wakuext_spectateCommunity"
:params ["community-id"]}]}
(sut/spectate-community {:db {:communities {"community-id" {}}}} ["community-id"]))))))
(t/deftest spectate-community-failed
(t/testing "mark community spectating false"
(t/is (match?
{:db {:communities {"community-id" {:spectating false}}}}
(sut/spectate-community-failed {} ["community-id"])))))
(t/deftest spectate-community-success
(t/testing "given communities"
(t/testing "mark first community spectating false"
(t/is (match?
{:db {:communities {"community-id" {:spectating false}}}}
(sut/spectate-community-success {} [{:communities [{:id "community-id"}]}]))))
(t/testing "mark first community spectated true"
(t/is (match?
{:db {:communities {"community-id" {:spectated true}}}}
(sut/spectate-community-success {} [{:communities [{:id "community-id"}]}]))))
(t/testing "dispatch fxs for first community"
(t/is (match?
{:fx [[:dispatch [:communities/handle-community {:id "community-id"}]]
[:dispatch [::mailserver/request-messages]]]}
(sut/spectate-community-success {} [{:communities [{:id "community-id"}]}])))))
(t/testing "given empty community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/spectate-community-success {} [{:communities []}])))))
(t/testing "given nil community"
(t/testing "do nothing"
(t/is (match?
nil
(sut/spectate-community-success {} []))))))
@@ -81,7 +81,7 @@
(fn []
(when-not cached-preview-data
(let [community-id (community-id-from-link community-link)]
(rf/dispatch [:chat.ui/fetch-community community-id]))))
(rf/dispatch [:communities/fetch-community community-id]))))
:reagent-render
(fn []
(when cached-preview-data
@@ -159,7 +159,7 @@
unmute-chat-label
mute-chat-label))
:color cover-bg-color
:icon (if muted? :i/muted :i/activity-center)
:icon (if muted? :i/activity-center :i/muted)
:on-press (fn []
(if muted?
(home.actions/unmute-chat-action chat-id)
@@ -267,7 +267,7 @@
(defn on-layout
[{:keys [event layout-height distance-atom distance-from-list-top
calculations-complete? messages-list-on-layout-finished?]}]
chat-screen-layout-calculations-complete?]}]
(let [layout-height-new (oops/oget event "nativeEvent.layout.height")
change (- layout-height-new @layout-height)
new-distance (- @distance-atom change)]
@@ -275,9 +275,8 @@
(reanimated/set-shared-value distance-from-list-top new-distance)
(reset! distance-atom new-distance)
(reset! layout-height layout-height-new))
(when-not (reanimated/get-shared-value calculations-complete?)
(reanimated/set-shared-value calculations-complete? true))
(js/setTimeout #(reset! messages-list-on-layout-finished? true) 1000)))
(when-not (reanimated/get-shared-value chat-screen-layout-calculations-complete?)
(reanimated/set-shared-value chat-screen-layout-calculations-complete? true))))
(defn on-scroll-fn
[distance-atom layout-height-atom]
@@ -288,7 +287,7 @@
(defn f-messages-list-content
[{:keys [insets distance-from-list-top content-height layout-height cover-bg-color distance-atom
calculations-complete? messages-list-on-layout-finished? chat-list-scroll-y]}]
chat-screen-layout-calculations-complete? chat-list-scroll-y]}]
(let [theme (quo.theme/use-theme-value)
chat (rf/sub [:chats/current-chat-chat-view])
{:keys [keyboard-shown]} (hooks/use-keyboard)
@@ -351,8 +350,7 @@
:layout-height layout-height
:distance-atom distance-atom
:distance-from-list-top distance-from-list-top
:calculations-complete? calculations-complete?
:messages-list-on-layout-finished?
messages-list-on-layout-finished?})
:chat-screen-layout-calculations-complete?
chat-screen-layout-calculations-complete?})
:scroll-enabled (not recording?)
:content-inset-adjustment-behavior :never}]]]))
@@ -18,7 +18,7 @@
[utils.worklets.chat.messenger.navigation :as worklets]))
(defn f-header-content-container
[{:keys [chat distance-from-list-top all-loaded? calculations-complete?]}]
[{:keys [chat distance-from-list-top all-loaded? chat-screen-layout-calculations-complete?]}]
(let [{:keys [chat-id group-chat chat-type chat-name
emoji]} chat
display-name (cond
@@ -34,7 +34,7 @@
header-opacity (worklets/navigation-header-opacity
distance-from-list-top
all-loaded?
calculations-complete?
chat-screen-layout-calculations-complete?
(if platform/ios?
messages.constants/content-animation-start-position-ios
messages.constants/content-animation-start-position-android))
@@ -104,7 +104,7 @@
:top-offset navigation-view-height}]]))
(defn f-view
[{:keys [distance-from-list-top calculations-complete?]}]
[{:keys [distance-from-list-top chat-screen-layout-calculations-complete?]}]
(let [{:keys [chat-id chat-type] :as chat} (rf/sub [:chats/current-chat-chat-view])
all-loaded? (reanimated/use-shared-value false)
all-loaded-sub (rf/sub [:chats/all-loaded? chat-id])
@@ -129,10 +129,10 @@
:on-press #(rf/dispatch [:navigate-back])}
(if (= chat-type constants/community-chat-type) :i/arrow-left :i/close)]
[:f> f-header-content-container
{:chat chat
:distance-from-list-top distance-from-list-top
:all-loaded? all-loaded?
:calculations-complete? calculations-complete?}]
{:chat chat
:distance-from-list-top distance-from-list-top
:all-loaded? all-loaded?
:chat-screen-layout-calculations-complete? chat-screen-layout-calculations-complete?}]
[quo/button
{:icon-only? true
:type :grey
@@ -3,7 +3,6 @@
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im.contexts.chat.messenger.composer.view :as composer.view]
[status-im.contexts.chat.messenger.messages.list.style :as style]
[status-im.contexts.chat.messenger.messages.list.view :as list.view]
@@ -15,49 +14,47 @@
;; improvement. Please avoid refactoring these files. Also if you are not already working on bug
;; fixes related to the composer, please skip them. And ping me, so I can address them while refactoring
(defn- f-chat-screen
[calculations-complete?]
(let [insets (safe-area/get-insets)
content-height (atom 0)
layout-height (atom 0)
distance-atom (atom 0)
messages-list-on-layout-finished? (reagent/atom false)
distance-from-list-top (reanimated/use-shared-value 0)
chat-list-scroll-y (reanimated/use-shared-value 0)]
[chat-screen-layout-calculations-complete?]
(let [insets (safe-area/get-insets)
content-height (atom 0)
layout-height (atom 0)
distance-atom (atom 0)
distance-from-list-top (reanimated/use-shared-value 0)
chat-list-scroll-y (reanimated/use-shared-value 0)]
[rn/keyboard-avoiding-view
{:style style/keyboard-avoiding-container
:keyboard-vertical-offset (- (:bottom insets))}
[:f> messages.navigation/f-view
{:distance-from-list-top distance-from-list-top
:calculations-complete? calculations-complete?}]
{:distance-from-list-top distance-from-list-top
:chat-screen-layout-calculations-complete? chat-screen-layout-calculations-complete?}]
[:f> list.view/f-messages-list-content
{:insets insets
:layout-height layout-height
:content-height content-height
:distance-atom distance-atom
:calculations-complete? calculations-complete?
:distance-from-list-top distance-from-list-top
:chat-list-scroll-y chat-list-scroll-y
:messages-list-on-layout-finished? messages-list-on-layout-finished?
:cover-bg-color :turquoise}]
{:insets insets
:layout-height layout-height
:content-height content-height
:distance-atom distance-atom
:chat-screen-layout-calculations-complete? chat-screen-layout-calculations-complete?
:distance-from-list-top distance-from-list-top
:chat-list-scroll-y chat-list-scroll-y
:cover-bg-color :turquoise}]
[composer.view/composer
{:insets insets
:chat-list-scroll-y chat-list-scroll-y
:messages-list-on-layout-finished? messages-list-on-layout-finished?}]]))
{:insets insets
:chat-screen-layout-calculations-complete? chat-screen-layout-calculations-complete?
:chat-list-scroll-y chat-list-scroll-y}]]))
(defn lazy-chat-screen
[calculations-complete?]
[chat-screen-layout-calculations-complete?]
(let [screen-loaded? (rf/sub [:shell/chat-screen-loaded?])]
(when-not screen-loaded?
(reanimated/set-shared-value calculations-complete? false))
(reanimated/set-shared-value chat-screen-layout-calculations-complete? false))
(when screen-loaded?
[:f> f-chat-screen calculations-complete?])))
[:f> f-chat-screen chat-screen-layout-calculations-complete?])))
(defn- f-chat
[]
(let [calculations-complete? (reanimated/use-shared-value false)]
(let [chat-screen-layout-calculations-complete? (reanimated/use-shared-value false)]
[:<>
[lazy-chat-screen calculations-complete?]
[:f> placeholder.view/f-view calculations-complete?]]))
[lazy-chat-screen chat-screen-layout-calculations-complete?]
[:f> placeholder.view/f-view chat-screen-layout-calculations-complete?]]))
(defn chat
[]
@@ -15,9 +15,9 @@
:animated? false}])
(defn f-view
[calculations-complete?]
[chat-screen-layout-calculations-complete?]
(let [top (safe-area/get-top)
opacity (worklets/placeholder-opacity calculations-complete?)
z-index (worklets/placeholder-z-index calculations-complete?)]
opacity (worklets/placeholder-opacity chat-screen-layout-calculations-complete?)
z-index (worklets/placeholder-z-index chat-screen-layout-calculations-complete?)]
[reanimated/view {:style (style/container top opacity z-index)}
[loading-skeleton]]))
@@ -23,11 +23,8 @@
[]
(let [{id :community-id} (rf/sub [:get-screen-params])
{:keys [name color images]} (rf/sub [:communities/community id])
airdrop-account (rf/sub [:communities/airdrop-account])
selected-accounts (rf/sub [:communities/selected-permission-accounts])]
(rn/use-effect (fn []
(rf/dispatch [:communities/initialize-permission-addresses]))
[])
airdrop-account (rf/sub [:communities/airdrop-account id])
selected-accounts (rf/sub [:communities/selected-permission-accounts id])]
[rn/view {:style style/container}
[quo/page-nav
{:text-align :left
@@ -8,7 +8,7 @@
[utils.re-frame :as rf]))
(defn- account-item
[item _ _ selected-addresses]
[item _ _ [selected-addresses community-id]]
[quo/account-permissions
{:account {:name (:name item)
:address (:address item)
@@ -17,7 +17,7 @@
:token-details []
:checked? (contains? selected-addresses (:address item))
:on-change #(rf/dispatch [:communities/toggle-selected-permission-address
(:address item)])
(:address item) community-id])
:container-style {:margin-bottom 8}}])
(defn view
@@ -25,7 +25,7 @@
(let [{id :community-id} (rf/sub [:get-screen-params])
{:keys [name color images]} (rf/sub [:communities/community id])
accounts (rf/sub [:wallet/accounts-with-customization-color])
selected-addresses (rf/sub [:communities/selected-permission-addresses])]
selected-addresses (rf/sub [:communities/selected-permission-addresses id])]
[rn/safe-area-view {:style style/container}
[quo/drawer-top
{:type :context-tag
@@ -38,7 +38,7 @@
[rn/flat-list
{:render-fn account-item
:render-data selected-addresses
:render-data [selected-addresses id]
:content-container-style {:padding 20}
:key-fn :address
:data accounts}]
@@ -60,7 +60,7 @@
{:type :grey
:container-style {:flex 1}
:on-press (fn []
(rf/dispatch [:communities/reset-selected-permission-addresses])
(rf/dispatch [:communities/reset-selected-permission-addresses id])
(rf/dispatch [:navigate-back]))}
(i18n/label :t/cancel)]
[quo/button
@@ -68,6 +68,6 @@
:customization-color color
:disabled? (empty? selected-addresses)
:on-press (fn []
(rf/dispatch [:communities/update-previous-permission-addresses])
(rf/dispatch [:communities/update-previous-permission-addresses id])
(rf/dispatch [:navigate-back]))}
(i18n/label :t/confirm-changes)]]]))
@@ -8,12 +8,12 @@
[utils.re-frame :as rf]))
(defn- render-item
[item _ _ airdrop-address]
[item _ _ [airdrop-address community-id]]
[quo/account-item
{:account-props item
:state (when (= airdrop-address (:address item)) :selected)
:on-press (fn []
(rf/dispatch [:communities/set-airdrop-address (:address item)])
(rf/dispatch [:communities/set-airdrop-address (:address item) community-id])
(rf/dispatch [:navigate-back]))
:emoji (:emoji item)}])
@@ -21,8 +21,8 @@
[]
(let [{id :community-id} (rf/sub [:get-screen-params])
{:keys [name images color]} (rf/sub [:communities/community id])
selected-accounts (rf/sub [:communities/selected-permission-accounts])
airdrop-address (rf/sub [:communities/airdrop-address])]
selected-accounts (rf/sub [:communities/selected-permission-accounts id])
airdrop-address (rf/sub [:communities/airdrop-address id])]
[:<>
[quo/drawer-top
{:type :context-tag
@@ -35,6 +35,6 @@
[rn/flat-list
{:data selected-accounts
:render-fn render-item
:render-data airdrop-address
:render-data [airdrop-address id]
:content-container-style style/account-list-container
:key-fn :address}]]))
@@ -6,9 +6,8 @@
[utils.re-frame :as rf]))
(defn token-requirements
[id]
(rf/dispatch [:communities/check-permissions-to-join-community id])
(fn []
[]
(fn [id]
(let [{:keys [can-request-access?
number-of-hold-tokens tokens]} (rf/sub [:community/token-gated-overview id])]
[rn/view {:style {:padding-horizontal 12}}
@@ -20,11 +20,11 @@
[quo/community-card-view-item
{:community (assoc item :cover cover)
:width width
:on-press #(rf/dispatch [:navigate-to :community-overview (:id item)])}]
[quo/community-list-item
:on-press #(rf/dispatch [:communities/navigate-to-community-overview (:id item)])}]
[quo/community-list
{:on-press (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch [:navigate-to :community-overview (:id item)]))
(rf/dispatch [:communities/navigate-to-community-overview (:id item)]))
:on-long-press #(rf/dispatch
[:show-bottom-sheet
{:content (fn []
@@ -142,12 +142,13 @@
(if (= view-type :card-view)
[quo/community-card-view-item
{:community (assoc community :cover cover)
:on-press #(rf/dispatch [:navigate-to :community-overview (:id community)])}]
:on-press #(rf/dispatch [:communities/navigate-to-community-overview (:id community)])}]
[quo/community-list-item
[quo/community-list
{:on-press (fn []
(rf/dispatch [:dismiss-keyboard])
(rf/dispatch [:navigate-to :community-overview (:id community)]))
(rf/dispatch [:communities/navigate-to-community-overview
(:id community)]))
:on-long-press #(js/alert "TODO: to be implemented")}
community])]))
(if communities communities communities-ids))
+178 -40
View File
@@ -1,12 +1,17 @@
(ns status-im.contexts.communities.events
(:require [clojure.set :as set]
[clojure.string :as string]
[clojure.walk :as walk]
[legacy.status-im.data-store.chats :as data-store.chats]
[legacy.status-im.mailserver.core :as mailserver]
[react-native.platform :as platform]
[react-native.share :as share]
[schema.core :as schema]
[status-im.constants :as constants]
[status-im.contexts.chat.messenger.messages.link-preview.events :as link-preview.events]
status-im.contexts.communities.actions.community-options.events
status-im.contexts.communities.actions.leave.events
[status-im.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -71,14 +76,20 @@
(when community-js
(let [{:keys [token-permissions
token-permissions-check joined id]
:as community} (<-rpc community-js)
has-token-permissions? (not (seq token-permissions))]
:as community} (<-rpc community-js)
has-channel-perm? (fn [id-perm-tuple]
(let [{:keys [type]} (second id-perm-tuple)]
(or (= type constants/community-token-permission-can-view-channel)
(=
type
constants/community-token-permission-can-view-and-post-channel))))]
{:db (assoc-in db [:communities id] community)
:fx [(when (and has-token-permissions? (not joined))
:fx [[:dispatch [:communities/initialize-permission-addresses id]]
(when (not joined)
[:dispatch [:chat.ui/spectate-community id]])
(when (and has-token-permissions? (nil? token-permissions-check))
(when (nil? token-permissions-check)
[:dispatch [:communities/check-permissions-to-join-community id]])
(when (and has-token-permissions? (not (get-in db [:community-channels-permissions id])))
(when (some has-channel-perm? token-permissions)
[:dispatch [:communities/check-all-community-channels-permissions id]])]}))))
(rf/defn handle-removed-chats
@@ -189,54 +200,62 @@
:on-error #(log/error "failed to fetch collapsed community categories" %)}]}))
(defn initialize-permission-addresses
[{:keys [db]}]
(let [accounts (get-in db [:wallet :accounts])
sorted-accounts (sort-by :position (vals accounts))
addresses (set (map :address sorted-accounts))]
{:db (assoc db
:communities/previous-permission-addresses addresses
:communities/selected-permission-addresses addresses
:communities/airdrop-address (:address (first sorted-accounts)))}))
[{:keys [db]} [community-id]]
(when community-id
(let [accounts (get-in db [:wallet :accounts])
sorted-accounts (sort-by :position (vals accounts))
addresses (set (map :address sorted-accounts))]
{:db (update-in db
[:communities community-id]
assoc
:previous-permission-addresses addresses
:selected-permission-addresses addresses
:airdrop-address (:address (first sorted-accounts)))})))
(rf/reg-event-fx :communities/initialize-permission-addresses
initialize-permission-addresses)
(defn update-previous-permission-addresses
[{:keys [db]}]
(let [accounts (get-in db [:wallet :accounts])
sorted-accounts (sort-by :position (vals accounts))
selected-permission-addresses (get-in db [:communities/selected-permission-addresses])
selected-accounts (filter #(contains? selected-permission-addresses
(:address %))
sorted-accounts)
current-airdrop-address (get-in db [:communities/airdrop-address])]
{:db (assoc db
:communities/previous-permission-addresses selected-permission-addresses
:communities/airdrop-address (if (contains? selected-permission-addresses
current-airdrop-address)
current-airdrop-address
(:address (first selected-accounts))))}))
[{:keys [db]} [community-id]]
(when community-id
(let [accounts (get-in db [:wallet :accounts])
sorted-accounts (sort-by :position (vals accounts))
selected-permission-addresses (get-in db
[:communities community-id
:selected-permission-addresses])
selected-accounts (filter #(contains? selected-permission-addresses (:address %))
sorted-accounts)
current-airdrop-address (get-in db [:communities community-id :airdrop-address])]
{:db (update-in db
[:communities community-id]
assoc
:previous-permission-addresses selected-permission-addresses
:airdrop-address (if (contains? selected-permission-addresses
current-airdrop-address)
current-airdrop-address
(:address (first selected-accounts))))})))
(rf/reg-event-fx :communities/update-previous-permission-addresses
update-previous-permission-addresses)
(defn toggle-selected-permission-address
[{:keys [db]} [address]]
{:db (update db
:communities/selected-permission-addresses
(fn [selected-addresses]
(if (contains? selected-addresses address)
(disj selected-addresses address)
(conj selected-addresses address))))})
[{:keys [db]} [address community-id]]
{:db (update-in db
[:communities community-id :selected-permission-addresses]
(fn [selected-addresses]
(if (contains? selected-addresses address)
(disj selected-addresses address)
(conj selected-addresses address))))})
(rf/reg-event-fx :communities/toggle-selected-permission-address
toggle-selected-permission-address)
(rf/reg-event-fx :communities/reset-selected-permission-addresses
(fn [{:keys [db]}]
{:db (assoc db
:communities/selected-permission-addresses
(get-in db [:communities/previous-permission-addresses]))}))
(fn [{:keys [db]} [community-id]]
(when community-id
{:db (assoc-in db
[:communities community-id :selected-permission-addresses]
(get-in db [:communities community-id :previous-permission-addresses]))})))
(rf/reg-event-fx :communities/share-community-channel-url-with-data
(fn [_ [chat-id]]
@@ -265,5 +284,124 @@
:event "share-community-channel-url-with-data"}))}]})))
(rf/reg-event-fx :communities/set-airdrop-address
(fn [{:keys [db]} [address]]
{:db (assoc db :communities/airdrop-address address)}))
(fn [{:keys [db]} [address community-id]]
{:db (assoc-in db [:communities community-id :airdrop-address] address)}))
(defn community-fetched
[{:keys [db]} [community-id community]]
(when community
{:db (update db :communities/fetching-community dissoc community-id)
:fx [[:dispatch [:communities/handle-community community]]
[:dispatch
[:chat.ui/cache-link-preview-data (link-preview.events/community-link community-id)
community]]]}))
(rf/reg-event-fx :chat.ui/community-fetched community-fetched)
(defn community-failed-to-fetch
[{:keys [db]} [community-id]]
{:db (update db :communities/fetching-community dissoc community-id)})
(rf/reg-event-fx :chat.ui/community-failed-to-fetch community-failed-to-fetch)
(defn fetch-community
[{:keys [db]} [community-id]]
(when (and community-id (not (get-in db [:communities/fetching-community community-id])))
{:db (assoc-in db [:communities/fetching-community community-id] true)
:json-rpc/call [{:method "wakuext_fetchCommunity"
:params [{:CommunityKey community-id
:TryDatabase true
:WaitForResponse true}]
:on-success (fn [community]
(rf/dispatch [:chat.ui/community-fetched community-id community]))
:on-error (fn [err]
(rf/dispatch [:chat.ui/community-failed-to-fetch community-id])
(log/error {:message
"Failed to request community info from mailserver"
:error err}))}]}))
(schema/=> fetch-community
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema [:catn [:community-id [:? :string]]]]]]
[:maybe
[:map
[:db map?]
[:json-rpc/call :schema.common/rpc-call]]]])
(rf/reg-event-fx :communities/fetch-community fetch-community)
(defn spectate-community-success
[{:keys [db]} [{:keys [communities]}]]
(when-let [community (first communities)]
{:db (-> db
(assoc-in [:communities (:id community) :spectated] true)
(assoc-in [:communities (:id community) :spectating] false))
:fx [[:dispatch [:communities/handle-community community]]
[:dispatch [::mailserver/request-messages]]]}))
(rf/reg-event-fx :chat.ui/spectate-community-success spectate-community-success)
(defn spectate-community-failed
[{:keys [db]} [community-id]]
{:db (assoc-in db [:communities community-id :spectating] false)})
(rf/reg-event-fx :chat.ui/spectate-community-failed spectate-community-failed)
(defn spectate-community
[{:keys [db]} [community-id]]
(let [{:keys [spectated spectating joined]} (get-in db [:communities community-id])]
(when (and (not joined) (not spectated) (not spectating))
{:db (assoc-in db [:communities community-id :spectating] true)
:json-rpc/call [{:method "wakuext_spectateCommunity"
:params [community-id]
:on-success [:chat.ui/spectate-community-success]
:on-error (fn [err]
(log/error {:message
"Failed to spectate community"
:error err})
(rf/dispatch [:chat.ui/spectate-community-failed
community-id]))}]})))
(schema/=> spectate-community
[:=>
[:catn
[:cofx :schema.re-frame/cofx]
[:args
[:schema [:catn [:community-id [:? :string]]]]]]
[:maybe
[:map
[:db map?]
[:json-rpc/call :schema.common/rpc-call]]]])
(rf/reg-event-fx :chat.ui/spectate-community spectate-community)
(rf/defn navigate-to-serialized-community
[_ {:keys [community-id]}]
{:serialization/deserialize-and-compress-key
{:serialized-key community-id
:on-success #(rf/dispatch [:communities/navigate-to-community-overview %])
:on-error #(log/error {:message "Failed to decompress community-id"
:error %
:community-id community-id})}})
(rf/reg-event-fx :communities/navigate-to-community-overview
(fn [cofx [deserialized-key]]
(if (string/starts-with? deserialized-key constants/serialization-key)
(navigate-to-serialized-community cofx deserialized-key)
(rf/merge
cofx
{:fx [[:dispatch [:communities/fetch-community deserialized-key]]
[:dispatch [:navigate-to :community-overview deserialized-key]]]}
(navigation/pop-to-root :shell-stack)))))
(rf/reg-event-fx :communities/navigate-to-community-chat
(fn [{:keys [db]} [chat-id pop-to-root?]]
(let [{:keys [community-id]} (get-in db [:chats chat-id])]
{:fx [(when community-id
[:dispatch [:communities/fetch-community community-id]])
(if pop-to-root?
[:dispatch [:chat/pop-to-root-and-navigate-to-chat chat-id]]
[:dispatch [:chat/navigate-to-chat chat-id]])]})))
@@ -1,25 +1,31 @@
(ns status-im.contexts.communities.events-test
(:require [cljs.test :refer [deftest is]]
(:require [cljs.test :refer [deftest is testing]]
[legacy.status-im.mailserver.core :as mailserver]
matcher-combinators.test
[status-im.contexts.chat.messenger.messages.link-preview.events :as link-preview.events]
[status-im.contexts.communities.events :as events]))
(def community-id "community-id")
(deftest initialize-permission-addresses-test
(let [initial-db {:db {:wallet {:accounts {"0x1" {:address "0x1"
:position 0}
"0x2" {:address "0x2"
:position 1}}}}}
expected-db {:db (assoc (:db initial-db)
:communities/previous-permission-addresses #{"0x1" "0x2"}
:communities/selected-permission-addresses #{"0x1" "0x2"}
:communities/airdrop-address "0x1")}]
(is (match? expected-db (events/initialize-permission-addresses initial-db)))))
expected-db {:db (update-in (:db initial-db)
[:communities community-id]
assoc
:previous-permission-addresses #{"0x1" "0x2"}
:selected-permission-addresses #{"0x1" "0x2"}
:airdrop-address "0x1")}]
(is (match? expected-db (events/initialize-permission-addresses initial-db [community-id])))))
(deftest toggle-selected-permission-address-test
(let [initial-db {:db {:communities/selected-permission-addresses #{"0x1" "0x2"}}}]
(is (match? {:db {:communities/selected-permission-addresses #{"0x1"}}}
(events/toggle-selected-permission-address initial-db ["0x2"])))
(is (match? {:db {:communities/selected-permission-addresses #{"0x1" "0x2" "0x3"}}}
(events/toggle-selected-permission-address initial-db ["0x3"])))))
(let [initial-db {:db {:communities {community-id {:selected-permission-addresses #{"0x1" "0x2"}}}}}]
(is (match? {:db {:communities {community-id {:selected-permission-addresses #{"0x1"}}}}}
(events/toggle-selected-permission-address initial-db ["0x2" community-id])))
(is (match? {:db {:communities {community-id {:selected-permission-addresses #{"0x1" "0x2" "0x3"}}}}}
(events/toggle-selected-permission-address initial-db ["0x3" community-id])))))
(deftest update-previous-permission-addresses-test
(let [wallet {:accounts {"0x1" {:address "0x1"
@@ -28,25 +34,162 @@
:position 1}
"0x3" {:address "0x3"
:position 2}}}]
(let [initial-db {:db {:wallet wallet
:communities/previous-permission-addresses #{"0x1" "0x2"}
:communities/selected-permission-addresses #{"0x1" "0x2" "0x3"}
:communities/airdrop-address "0x1"}}
expected-db {:db {:wallet wallet
:communities/previous-permission-addresses #{"0x1" "0x2" "0x3"}
:communities/selected-permission-addresses #{"0x1" "0x2" "0x3"}
:communities/airdrop-address "0x1"}}]
(let [initial-db {:db {:wallet wallet
:communities {community-id {:previous-permission-addresses #{"0x1" "0x2"}
:selected-permission-addresses #{"0x1" "0x2"
"0x3"}
:airdrop-address "0x1"}}}}
expected-db {:db {:wallet wallet
:communities {community-id {:previous-permission-addresses #{"0x1" "0x2"
"0x3"}
:selected-permission-addresses #{"0x1" "0x2"
"0x3"}
:airdrop-address "0x1"}}}}]
(is
(match? expected-db
(events/update-previous-permission-addresses initial-db))))
(let [initial-db {:db {:wallet wallet
:communities/previous-permission-addresses #{"0x1" "0x2"}
:communities/selected-permission-addresses #{"0x2" "0x3"}
:communities/airdrop-address "0x1"}}
expected-db {:db {:wallet wallet
:communities/previous-permission-addresses #{"0x2" "0x3"}
:communities/selected-permission-addresses #{"0x2" "0x3"}
:communities/airdrop-address "0x2"}}]
(events/update-previous-permission-addresses initial-db [community-id]))))
(let [initial-db {:db {:wallet wallet
:communities {community-id {:previous-permission-addresses #{"0x1" "0x2"}
:selected-permission-addresses #{"0x2" "0x3"}
:airdrop-address "0x1"}}}}
expected-db {:db {:wallet wallet
:communities {community-id {:previous-permission-addresses #{"0x2" "0x3"}
:selected-permission-addresses #{"0x2" "0x3"}
:airdrop-address "0x2"}}}}]
(is
(match? expected-db
(events/update-previous-permission-addresses initial-db))))))
(events/update-previous-permission-addresses initial-db [community-id]))))))
(deftest fetch-community
(testing "with community id"
(testing "update fetching indicator in db"
(is (match?
{:db {:communities/fetching-community {community-id true}}}
(events/fetch-community {} [community-id]))))
(testing "call the fetch community rpc method with correct community id"
(is (match?
{:json-rpc/call [{:method "wakuext_fetchCommunity"
:params [{:CommunityKey community-id
:TryDatabase true
:WaitForResponse true}]}]}
(events/fetch-community {} [community-id])))))
(testing "with no community id"
(testing "do nothing"
(is (match?
nil
(events/fetch-community {} []))))))
(deftest community-failed-to-fetch
(testing "given a community id"
(testing "remove community id from fetching indicator in db"
(is (match?
nil
(get-in (events/community-failed-to-fetch {:db {:communities/fetching-community
{community-id true}}}
[community-id])
[:db :communities/fetching-community community-id]))))))
(deftest community-fetched
(with-redefs [link-preview.events/community-link (fn [id] (str "community-link+" id))]
(testing "given a community"
(let [cofx {:db {:communities/fetching-community {community-id true}}}
arg [community-id {:id community-id}]]
(testing "remove community id from fetching indicator in db"
(is (match?
nil
(get-in (events/community-fetched cofx arg)
[:db :communities/fetching-community community-id]))))
(testing "dispatch fxs"
(is (match?
{:fx [[:dispatch [:communities/handle-community {:id community-id}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id community-id}]]]}
(events/community-fetched cofx arg))))))
(testing "given a joined community"
(let [cofx {:db {:communities/fetching-community {community-id true}}}
arg [community-id {:id community-id :joined true}]]
(testing "dispatch fxs, do not spectate community"
(is (match?
{:fx [[:dispatch [:communities/handle-community {:id community-id}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id community-id}]]]}
(events/community-fetched cofx arg))))))
(testing "given a token-gated community"
(let [cofx {:db {:communities/fetching-community {community-id true}}}
arg [community-id {:id community-id :tokenPermissions [1]}]]
(testing "dispatch fxs, do not spectate community"
(is (match?
{:fx [[:dispatch [:communities/handle-community {:id community-id}]]
[:dispatch
[:chat.ui/cache-link-preview-data "community-link+community-id"
{:id community-id}]]]}
(events/community-fetched cofx arg))))))
(testing "given nil community"
(testing "do nothing"
(is (match?
nil
(events/community-fetched {} [community-id nil])))))))
(deftest spectate-community
(testing "given a joined community"
(testing "do nothing"
(is (match?
nil
(events/spectate-community {:db {:communities {community-id {:joined true}}}}
[community-id])))))
(testing "given a spectated community"
(testing "do nothing"
(is (match?
nil
(events/spectate-community {:db {:communities {community-id {:spectated true}}}}
[community-id])))))
(testing "given a spectating community"
(testing "do nothing"
(is (match?
nil
(events/spectate-community {:db {:communities {community-id {:spectating true}}}}
[community-id])))))
(testing "given a community"
(testing "mark community spectating"
(is (match?
{:db {:communities {community-id {:spectating true}}}}
(events/spectate-community {:db {:communities {community-id {}}}} [community-id]))))
(testing "call spectate community rpc with correct community id"
(is (match?
{:json-rpc/call [{:method "wakuext_spectateCommunity"
:params [community-id]}]}
(events/spectate-community {:db {:communities {community-id {}}}} [community-id]))))))
(deftest spectate-community-failed
(testing "mark community spectating false"
(is (match?
{:db {:communities {community-id {:spectating false}}}}
(events/spectate-community-failed {} [community-id])))))
(deftest spectate-community-success
(testing "given communities"
(testing "mark first community spectating false"
(is (match?
{:db {:communities {community-id {:spectating false}}}}
(events/spectate-community-success {} [{:communities [{:id community-id}]}]))))
(testing "mark first community spectated true"
(is (match?
{:db {:communities {community-id {:spectated true}}}}
(events/spectate-community-success {} [{:communities [{:id community-id}]}]))))
(testing "dispatch fxs for first community"
(is (match?
{:fx [[:dispatch [:communities/handle-community {:id community-id}]]
[:dispatch [::mailserver/request-messages]]]}
(events/spectate-community-success {} [{:communities [{:id community-id}]}])))))
(testing "given empty community"
(testing "do nothing"
(is (match?
nil
(events/spectate-community-success {} [{:communities []}])))))
(testing "given nil community"
(testing "do nothing"
(is (match?
nil
(events/spectate-community-success {} []))))))
@@ -26,7 +26,8 @@
[quo/communities-membership-list-item
{:customization-color customization-color
:style {:padding-horizontal 20}
:on-press #(debounce/dispatch-and-chill [:navigate-to :community-overview id] 500)
:on-press #(debounce/dispatch-and-chill [:communities/navigate-to-community-overview id]
500)
:on-long-press #(rf/dispatch
[:show-bottom-sheet
{:content (fn []
@@ -144,13 +144,12 @@
(defn request-to-join-with-signatures-and-addresses
[{:keys [db]} [community-id signatures]]
(let [airdrop-address (get-in db [:communities/airdrop-address])
addresses-to-reveal (get-in db [:communities/selected-permission-addresses])]
(let [{:keys [airdrop-address selected-permission-addresses]} (get-in db [:communities community-id])]
{:fx [[:json-rpc/call
[{:method "wakuext_requestToJoinCommunity"
:params [{:communityId community-id
:signatures signatures
:addressesToReveal addresses-to-reveal
:addressesToReveal selected-permission-addresses
:airdropAddress airdrop-address}]
:js-response true
:on-success [:communities/requested-to-join]
@@ -173,7 +172,7 @@
[{:keys [db]}
[{:keys [community-id password]}]]
(let [pub-key (get-in db [:profile/profile :public-key])
addresses-to-reveal (get-in db [:communities/selected-permission-addresses])]
addresses-to-reveal (get-in db [:communities community-id :selected-permission-addresses])]
{:fx [[:json-rpc/call
[{:method "wakuext_generateJoiningCommunityRequestsForSigning"
:params [pub-key community-id addresses-to-reveal]
@@ -137,8 +137,7 @@
[quo/icon :i/info {:no-color true}]]])
(defn token-gates
[{:keys [id]}]
(rf/dispatch [:communities/check-permissions-to-join-community id])
[]
(fn [{:keys [id color]}]
(let [{:keys [can-request-access?
number-of-hold-tokens tokens]} (rf/sub [:community/token-gated-overview id])]
@@ -222,8 +221,9 @@
{:on-press (when joined-or-spectated
(fn []
(rf/dispatch [:dismiss-keyboard])
(debounce/dispatch-and-chill [:chat/navigate-to-chat (str community-id id)]
1000)))
(debounce/dispatch-and-chill
[:communities/navigate-to-community-chat (str community-id id)]
1000)))
:on-long-press #(rf/dispatch
[:show-bottom-sheet
{:content (fn []
@@ -370,9 +370,10 @@
(let [{:keys [id joined]
:as community} (rf/sub [:communities/community id])
pending? (rf/sub [:communities/my-pending-request-to-join id])]
(when joined
(rf/dispatch [:activity-center.notifications/dismiss-community-overview id]))
[community-scroll-page community pending?]))
(when community
(when joined
(rf/dispatch [:activity-center.notifications/dismiss-community-overview id]))
[community-scroll-page community pending?])))
(defn overview
[id]
@@ -30,7 +30,7 @@
:status-indicator? false
:customization-color customization-color}]]
[rn/view
{:style style/card-view}
{:pointer-events :none :style style/card-view}
[reanimated/view
{:style (style/card-container container-background)}
[rn/view {:style style/card-header}
@@ -15,3 +15,8 @@
{:justify-self :flex-end
:margin-bottom 46
:margin-horizontal 20})
(defn carousel-background
[height width]
{:height height
:width width})
@@ -26,6 +26,8 @@
paused? (atom nil)
is-dragging? (atom nil)
drag-amount (atom nil)
window-width (rf/sub [:dimensions/window-width])
window-height (rf/sub [:dimensions/window-height])
{:keys [emoji-hash display-name compressed-key
public-key]} (rf/sub [:profile/profile])
{:keys [color]} (rf/sub [:onboarding/profile])
@@ -42,11 +44,16 @@
{:animate? true
:progress progress
:paused? paused?
:gesture :tappable
:gesture :swipeable
:is-dragging? is-dragging?
:drag-amount drag-amount
:header-text header-text}]
[rn/view {:style style/content-container}
:header-text header-text
:background [rn/view
{:style (style/carousel-background window-height
(* (count header-text) window-width))}]}]
[rn/view
{:style style/content-container
:pointer-events :box-none}
[profile-card/profile-card
{:profile-picture photo-path
:name display-name
@@ -1,4 +1,4 @@
(ns status-im.contexts.preview.quo.list-items.community-list
(ns status-im.contexts.preview.quo.community.list-item
(:require
[quo.core :as quo]
[quo.theme :as quo.theme]
@@ -95,7 +95,7 @@
:unread-count 5})]
(fn []
[preview/preview-container {:state state :descriptor (descriptors @state)}
[quo/community-list-item
[quo/community-list
(merge @state
{:container-style {:width 335}
:logo (resources/get-mock-image :status-logo)
@@ -0,0 +1,46 @@
(ns status-im.contexts.preview.quo.list-items.network-list
(:require
[quo.core :as quo]
[quo.foundations.resources :as quo.resources]
[reagent.core :as reagent]
[status-im.contexts.preview.quo.preview :as preview]))
(def descriptor
[{:key :state
:type :select
:options [{:key :default}
{:key :active}
{:key :pressed}]}
{:key :network-image
:type :select
:options [{:key (quo.resources/get-network :ethereum)
:value :ethereum}
{:key (quo.resources/get-network :arbitrum)
:value :arbitrum}
{:key (quo.resources/get-network :optimism)
:value :optimism}]}
{:key :label
:type :text}
{:key :token-value
:type :text}
{:key :fiat-value
:type :text}
{:key :show-alert-on-press?
:type :boolean}])
(defn view
[]
(let [state (reagent/atom {:network-image (quo.resources/get-network :ethereum)
:label "Mainnet"
:token-value "0.00 ETH"
:fiat-value "€0.00"
:state :default
:customization-color :blue})]
(fn []
[preview/preview-container
{:state state
:descriptor descriptor}
[quo/network-list
(merge @state
(when (:show-alert-on-press? @state)
{:on-press #(js/alert "Pressed!")}))]])))
@@ -41,4 +41,4 @@
:descriptor descriptor
:component-container-style {:padding-vertical 30
:padding-horizontal 15}}
[quo/user-list @state]])))
[quo/user @state]])))
+5 -3
View File
@@ -41,6 +41,7 @@
:as community-membership-list-view]
[status-im.contexts.preview.quo.community.community-stat :as community-stat]
[status-im.contexts.preview.quo.community.discover-card :as discover-card]
[status-im.contexts.preview.quo.community.list-item :as community-list-item]
[status-im.contexts.preview.quo.community.token-gating :as token-gating]
[status-im.contexts.preview.quo.counter.counter :as counter]
[status-im.contexts.preview.quo.counter.step :as step]
@@ -92,9 +93,8 @@
account-list-card]
[status-im.contexts.preview.quo.list-items.address :as address]
[status-im.contexts.preview.quo.list-items.channel :as channel]
[status-im.contexts.preview.quo.list-items.community-list :as
community-list]
[status-im.contexts.preview.quo.list-items.dapp :as dapp]
[status-im.contexts.preview.quo.list-items.network-list :as network-list]
[status-im.contexts.preview.quo.list-items.preview-lists :as preview-lists]
[status-im.contexts.preview.quo.list-items.quiz-item :as quiz-item]
[status-im.contexts.preview.quo.list-items.saved-address :as saved-address]
@@ -349,9 +349,11 @@
:component channel/view}
{:name :community-list
:options {:insets {:top? true}}
:component community-list/view}
:component community-list-item/view}
{:name :dapp
:component dapp/preview}
{:name :network-list
:component network-list/view}
{:name :preview-lists
:component preview-lists/view}
{:name :quiz-item
@@ -320,7 +320,7 @@
(defn- f-preview-container
[{:keys [title state descriptor blur? blur-dark-only?
component-container-style
blur-container-style blur-view-props blur-height show-blur-background?]
blur-container-style blur-view-props blur-height show-blur-background? full-screen?]
:or {blur-height 200}}
& children]
(let [theme (quo.theme/use-theme-value)
@@ -337,8 +337,11 @@
[common/navigation-bar {:title title}]
[rn/scroll-view
{:style (style/panel-basic)
:shows-vertical-scroll-indicator false}
[rn/pressable {:on-press rn/dismiss-keyboard!}
:shows-vertical-scroll-indicator false
:content-container-style (when full-screen? {:flex 1})}
[rn/pressable
{:style (when full-screen? {:flex 1})
:on-press rn/dismiss-keyboard!}
(when descriptor
[rn/view {:style style/customizer-container}
[customizer state descriptor]])
@@ -2,6 +2,7 @@
(:require
[quo.core :as quo]
[quo.foundations.resources :as resources]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im.contexts.preview.quo.preview :as preview]))
@@ -24,17 +25,29 @@
(defn view
[]
(let [state (reagent/atom {:token :eth
:currency :usd
:conversion 0.02
:networks networks
:title title
:customization-color :blue})]
(let [state (reagent/atom {:token :eth
:currency :usd
:conversion 0.02
:networks networks
:title title
:customization-color :blue
:show-keyboard? false})
value (reagent/atom "")
set-value (fn [v]
(swap! value str v))
delete (fn [_]
(swap! value #(subs % 0 (dec (count %)))))]
(fn []
[preview/preview-container
{:state state
:descriptor descriptor
:component-container-style {:padding-horizontal 20
:margin-top 50
:align-items :center}}
[quo/token-input @state]])))
:full-screen? true
:component-container-style {:flex 1
:justify-content :space-between}}
[quo/token-input (assoc @state :value @value)]
[quo/numbered-keyboard
{:container-style {:padding-bottom (safe-area/get-top)}
:left-action :dot
:delete-key? true
:on-press set-value
:on-delete delete}]])))
+5 -2
View File
@@ -1,5 +1,6 @@
(ns status-im.contexts.profile.events
(:require
[legacy.status-im.data-store.settings :as data-store.settings]
[native-module.core :as native-module]
[re-frame.core :as re-frame]
[status-im.contexts.profile.edit.name.events]
@@ -52,8 +53,10 @@
(rf/defn update-setting-from-backup
{:events [:profile/update-setting-from-backup]}
[{:keys [db]} {{:keys [name value]} :backedUpSettings}]
{:db (assoc-in db [:profile/profile (keyword name)] value)})
[{:keys [db]} {:keys [backedUpSettings]}]
(let [setting (update backedUpSettings :name keyword)
{:keys [name value]} (data-store.settings/rpc->setting-value setting)]
{:db (assoc-in db [:profile/profile name] value)}))
(rf/defn update-profile-from-backup
{:events [:profile/update-profile-from-backup]}
@@ -1,5 +1,6 @@
(ns status-im.contexts.profile.settings.list-items
(:require [status-im.common.not-implemented :as not-implemented]
[status-im.config :as config]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@@ -89,12 +90,13 @@
:image :icon
:blur? true
:image-props :i/toggle}
{:title "Quo preview"
:on-press #(rf/dispatch [:navigate-to :quo-preview])
:action :arrow
:image :icon
:blur? true
:image-props :i/light}]
(when config/quo-preview-enabled?
{:title "Quo preview"
:on-press #(rf/dispatch [:navigate-to :quo-preview])
:action :arrow
:image :icon
:blur? true
:image-props :i/light})]
[{:title (i18n/label :t/about)
:on-press not-implemented/alert
:action :arrow}

Some files were not shown because too many files have changed in this diff Show More