diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ec188c7d98..bc2dcf954ff 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,7 +93,6 @@ android { versionName "4.68.0" vectorDrawables.useSupportLibrary = true manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] - missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below! resValue "string", "rn_config_reader_custom_package", "chat.rocket.reactnative" } @@ -144,7 +143,6 @@ dependencies { implementation jscFlavor } - implementation project(':react-native-notifications') implementation "com.google.firebase:firebase-messaging:23.3.1" implementation project(':watermelondb-jsi') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 552191d1316..4fb70ab1a89 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -75,6 +75,14 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt index e2ed12f7eab..502ae26784f 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt @@ -7,8 +7,11 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate import android.os.Bundle import com.zoontek.rnbootsplash.RNBootSplash -import android.content.Intent; -import android.content.res.Configuration; +import android.content.Intent +import android.content.res.Configuration +import chat.rocket.reactnative.notification.VideoConfModule +import chat.rocket.reactnative.notification.VideoConfNotification +import com.google.gson.GsonBuilder class MainActivity : ReactActivity() { @@ -25,9 +28,60 @@ class MainActivity : ReactActivity() { override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) - override fun onCreate(savedInstanceState: Bundle?) { + override fun onCreate(savedInstanceState: Bundle?) { RNBootSplash.init(this, R.style.BootTheme) super.onCreate(null) + + // Handle video conf action from notification + intent?.let { handleVideoConfIntent(it) } + } + + public override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + // Handle video conf action when activity is already running + handleVideoConfIntent(intent) + } + + private fun handleVideoConfIntent(intent: Intent) { + if (intent.getBooleanExtra("videoConfAction", false)) { + val notificationId = intent.getIntExtra("notificationId", 0) + val event = intent.getStringExtra("event") ?: return + val rid = intent.getStringExtra("rid") ?: "" + val callerId = intent.getStringExtra("callerId") ?: "" + val callerName = intent.getStringExtra("callerName") ?: "" + val host = intent.getStringExtra("host") ?: "" + val callId = intent.getStringExtra("callId") ?: "" + + android.util.Log.d("RocketChat.MainActivity", "Handling video conf intent - event: $event, rid: $rid, host: $host, callId: $callId") + + // Cancel the notification + if (notificationId != 0) { + VideoConfNotification.cancelById(this, notificationId) + } + + // Store action for JS to pick up - include all required fields + val data = mapOf( + "notificationType" to "videoconf", + "rid" to rid, + "event" to event, + "host" to host, + "callId" to callId, + "caller" to mapOf( + "_id" to callerId, + "name" to callerName + ) + ) + + val gson = GsonBuilder().create() + val jsonData = gson.toJson(data) + + android.util.Log.d("RocketChat.MainActivity", "Storing video conf action: $jsonData") + + VideoConfModule.storePendingAction(this, jsonData) + + // Clear the video conf flag to prevent re-processing + intent.removeExtra("videoConfAction") + } } override fun invokeDefaultOnBackPressed() { diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt index 0d07364246b..8b532b363bc 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt @@ -1,9 +1,7 @@ package chat.rocket.reactnative import android.app.Application -import android.content.Context import android.content.res.Configuration -import android.os.Bundle import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactHost @@ -18,17 +16,25 @@ import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage; -import com.wix.reactnativenotifications.core.AppLaunchHelper -import com.wix.reactnativenotifications.core.AppLifecycleFacade -import com.wix.reactnativenotifications.core.JsIOHelper -import com.wix.reactnativenotifications.core.notification.INotificationsApplication -import com.wix.reactnativenotifications.core.notification.IPushNotification import com.bugsnag.android.Bugsnag import expo.modules.ApplicationLifecycleDispatcher import chat.rocket.reactnative.networking.SSLPinningTurboPackage; import chat.rocket.reactnative.notification.CustomPushNotification; +import chat.rocket.reactnative.notification.VideoConfTurboPackage -open class MainApplication : Application(), ReactApplication, INotificationsApplication { +/** + * Main Application class. + * + * NOTIFICATION ARCHITECTURE: + * - JS layer uses expo-notifications for token registration and event handling + * - Native layer uses RCFirebaseMessagingService + CustomPushNotification for: + * - FCM message handling + * - Notification display with MessagingStyle + * - E2E encrypted message decryption + * - Direct reply functionality + * - Message-id-only notification loading + */ +open class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { @@ -36,6 +42,7 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl PackageList(this).packages.apply { add(SSLPinningTurboPackage()) add(WatermelonDBJSIPackage()) + add(VideoConfTurboPackage()) } override fun getJSMainModuleName(): String = "index" @@ -71,19 +78,4 @@ open class MainApplication : Application(), ReactApplication, INotificationsAppl super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } - - override fun getPushNotification( - context: Context, - bundle: Bundle, - defaultFacade: AppLifecycleFacade, - defaultAppLaunchHelper: AppLaunchHelper - ): IPushNotification { - return CustomPushNotification( - context, - bundle, - defaultFacade, - defaultAppLaunchHelper, - JsIOHelper() - ) - } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java index 18f89fb0ca4..723ce880cb3 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java @@ -1,7 +1,5 @@ package chat.rocket.reactnative.notification; -import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; - import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -25,10 +23,6 @@ import com.bumptech.glide.request.RequestOptions; import com.facebook.react.bridge.ReactApplicationContext; import com.google.gson.Gson; -import com.wix.reactnativenotifications.core.AppLaunchHelper; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.JsIOHelper; -import com.wix.reactnativenotifications.core.notification.PushNotification; import java.util.ArrayList; import java.util.Date; @@ -39,15 +33,16 @@ import java.util.concurrent.ExecutionException; import chat.rocket.reactnative.BuildConfig; +import chat.rocket.reactnative.MainActivity; import chat.rocket.reactnative.R; /** * Custom push notification handler for Rocket.Chat. * * Handles standard push notifications and End-to-End encrypted (E2E) notifications. - * For E2E notifications, waits for React Native initialization before decrypting and displaying. + * Provides MessagingStyle notifications, direct reply, and advanced processing. */ -public class CustomPushNotification extends PushNotification { +public class CustomPushNotification { private static final String TAG = "RocketChat.CustomPush"; private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig.DEBUG; @@ -59,14 +54,21 @@ public class CustomPushNotification extends PushNotification { // Constants public static final String KEY_REPLY = "KEY_REPLY"; public static final String NOTIFICATION_ID = "NOTIFICATION_ID"; + private static final String CHANNEL_ID = "rocketchatrn_channel_01"; + private static final String CHANNEL_NAME = "All"; // Instance fields - final NotificationManager notificationManager; + private final Context mContext; + private Bundle mBundle; + private final NotificationManager notificationManager; - public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, - AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { - super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); - notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + public CustomPushNotification(Context context, Bundle bundle) { + this.mContext = context; + this.mBundle = bundle; + this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // Ensure notification channel exists + createNotificationChannel(); } /** @@ -80,59 +82,66 @@ public static void setReactContext(ReactApplicationContext context) { public static void clearMessages(int notId) { notificationMessages.remove(Integer.toString(notId)); } + + /** + * Check if React Native is initialized + */ + private boolean isReactInitialized() { + return reactApplicationContext != null; + } - @Override - public void onReceived() throws InvalidNotificationException { - Bundle bundle = mNotificationProps.asBundle(); - String notId = bundle.getString("notId"); + public void onReceived() { + String notId = mBundle.getString("notId"); if (notId == null || notId.isEmpty()) { - throw new InvalidNotificationException("Missing notification ID"); + Log.w(TAG, "Missing notification ID, ignoring notification"); + return; } try { Integer.parseInt(notId); } catch (NumberFormatException e) { - throw new InvalidNotificationException("Invalid notification ID format: " + notId); + Log.w(TAG, "Invalid notification ID format: " + notId); + return; } // Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only) - if (!mAppLifecycleFacade.isReactInitialized()) { - android.util.Log.w(TAG, "React not initialized yet, waiting before processing notification..."); + if (!isReactInitialized()) { + Log.w(TAG, "React not initialized yet, waiting before processing notification..."); // Wait for React to initialize with timeout new Thread(() -> { int attempts = 0; int maxAttempts = 50; // 5 seconds total (50 * 100ms) - while (!mAppLifecycleFacade.isReactInitialized() && attempts < maxAttempts) { + while (!isReactInitialized() && attempts < maxAttempts) { try { Thread.sleep(100); // Wait 100ms attempts++; if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)"); + Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)"); } } catch (InterruptedException e) { - android.util.Log.e(TAG, "Wait interrupted", e); + Log.e(TAG, "Wait interrupted", e); Thread.currentThread().interrupt(); return; } } - if (mAppLifecycleFacade.isReactInitialized()) { - android.util.Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification"); + if (isReactInitialized()) { + Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification"); try { handleNotification(); } catch (Exception e) { - android.util.Log.e(TAG, "Failed to process notification after React initialization", e); + Log.e(TAG, "Failed to process notification after React initialization", e); } } else { - android.util.Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV"); + Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV"); try { handleNotification(); } catch (Exception e) { - android.util.Log.e(TAG, "Failed to process notification without React context", e); + Log.e(TAG, "Failed to process notification without React context", e); } } }).start(); @@ -141,23 +150,21 @@ public void onReceived() throws InvalidNotificationException { } if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "React already initialized, proceeding with notification"); + Log.d(TAG, "React already initialized, proceeding with notification"); } try { handleNotification(); } catch (Exception e) { - android.util.Log.e(TAG, "Failed to process notification on main thread", e); - throw new InvalidNotificationException("Notification processing failed: " + e.getMessage()); + Log.e(TAG, "Failed to process notification on main thread", e); } } private void handleNotification() { - Bundle received = mNotificationProps.asBundle(); - Ejson receivedEjson = safeFromJson(received.getString("ejson", "{}"), Ejson.class); + Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) { - android.util.Log.d(TAG, "Detected message-id-only notification, will fetch full content from server"); + Log.d(TAG, "Detected message-id-only notification, will fetch full content from server"); loadNotificationAndProcess(receivedEjson); return; // Exit early, notification will be processed in callback } @@ -171,30 +178,19 @@ private void loadNotificationAndProcess(Ejson ejson) { @Override public void call(@Nullable Bundle bundle) { if (bundle != null) { - android.util.Log.d(TAG, "Successfully loaded notification content from server, updating notification props"); + Log.d(TAG, "Successfully loaded notification content from server, updating notification props"); if (ENABLE_VERBOSE_LOGS) { - // BEFORE createProps - android.util.Log.d(TAG, "[BEFORE createProps] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); - android.util.Log.d(TAG, "[BEFORE createProps] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]")); - android.util.Log.d(TAG, "[BEFORE createProps] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0)); - android.util.Log.d(TAG, "[BEFORE createProps] bundle has ejson=" + (bundle.getString("ejson") != null)); + Log.d(TAG, "[BEFORE update] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, "[BEFORE update] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]")); + Log.d(TAG, "[BEFORE update] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0)); } synchronized(CustomPushNotification.this) { - mNotificationProps = createProps(bundle); - } - - if (ENABLE_VERBOSE_LOGS) { - // AFTER createProps - verify it worked - Bundle verifyBundle = mNotificationProps.asBundle(); - android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.notificationLoaded=" + verifyBundle.getBoolean("notificationLoaded", false)); - android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.title=" + (verifyBundle.getString("title") != null ? "[present]" : "[null]")); - android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps.message length=" + (verifyBundle.getString("message") != null ? verifyBundle.getString("message").length() : 0)); - android.util.Log.d(TAG, "[AFTER createProps] mNotificationProps has ejson=" + (verifyBundle.getString("ejson") != null)); + mBundle = bundle; } } else { - android.util.Log.w(TAG, "Failed to load notification content from server, will display placeholder notification"); + Log.w(TAG, "Failed to load notification content from server, will display placeholder notification"); } processNotification(); @@ -203,28 +199,26 @@ public void call(@Nullable Bundle bundle) { } private void processNotification() { - // We should re-read these values since that can be changed by notificationLoad - Bundle bundle = mNotificationProps.asBundle(); - Ejson loadedEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); - String notId = bundle.getString("notId", "1"); + Ejson loadedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); + String notId = mBundle.getString("notId", "1"); if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[processNotification] notId=" + notId); - android.util.Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); - android.util.Log.d(TAG, "[processNotification] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]")); - android.util.Log.d(TAG, "[processNotification] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0)); - android.util.Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null")); - android.util.Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null")); + Log.d(TAG, "[processNotification] notId=" + notId); + Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + mBundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, "[processNotification] bundle.title=" + (mBundle.getString("title") != null ? "[present]" : "[null]")); + Log.d(TAG, "[processNotification] bundle.message length=" + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0)); + Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null")); + Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null")); } // Handle E2E encrypted notifications if (isE2ENotification(loadedEjson)) { - handleE2ENotification(bundle, loadedEjson, notId); + handleE2ENotification(mBundle, loadedEjson, notId); return; // E2E processor will handle showing the notification } // Handle regular (non-E2E) notifications - showNotification(bundle, loadedEjson, notId); + showNotification(mBundle, loadedEjson, notId); } /** @@ -245,8 +239,7 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { if (decrypted != null) { bundle.putString("message", decrypted); - mNotificationProps = createProps(bundle); - bundle = mNotificationProps.asBundle(); + mBundle = bundle; ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); showNotification(bundle, ejson, notId); } else { @@ -266,11 +259,9 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { new E2ENotificationProcessor.NotificationCallback() { @Override public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) { - // Update props and show notification - mNotificationProps = createProps(decryptedBundle); - Bundle finalBundle = mNotificationProps.asBundle(); - Ejson finalEjson = safeFromJson(finalBundle.getString("ejson", "{}"), Ejson.class); - showNotification(finalBundle, finalEjson, notificationId); + mBundle = decryptedBundle; + Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class); + showNotification(decryptedBundle, finalEjson, notificationId); } @Override @@ -308,134 +299,174 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { String avatarUri = ejson != null ? ejson.getAvatarUri() : null; if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]")); + Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]")); } bundle.putString("avatarUri", avatarUri); // Handle special notification types - if (ejson != null && ejson.notificationType instanceof String && - ejson.notificationType.equals("videoconf")) { - notifyReceivedToJS(); + if (ejson != null && "videoconf".equals(ejson.notificationType)) { + handleVideoConfNotification(bundle, ejson); + return; } else { // Show regular notification if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); } notificationMessages.get(notId).add(bundle); if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size()); + Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size()); } postNotification(Integer.parseInt(notId)); - notifyReceivedToJS(); } } - @Override - public void onOpened() { - Bundle bundle = mNotificationProps.asBundle(); - final String notId = bundle.getString("notId", "1"); - notificationMessages.remove(notId); - digestNotification(); + /** + * Handles video conference notifications. + * Shows incoming call notification or cancels existing one based on status. + */ + private void handleVideoConfNotification(Bundle bundle, Ejson ejson) { + VideoConfNotification videoConf = new VideoConfNotification(mContext); + + Integer status = ejson.status; + String rid = ejson.rid; + // Video conf uses 'caller' field, regular messages use 'sender' + String callerId = ""; + if (ejson.caller != null && ejson.caller._id != null) { + callerId = ejson.caller._id; + } else if (ejson.sender != null && ejson.sender._id != null) { + callerId = ejson.sender._id; + } + + Log.d(TAG, "Video conf notification - status: " + status + ", rid: " + rid); + + if (status == null || status == 0) { + // Incoming call - show notification + videoConf.showIncomingCall(bundle, ejson); + } else if (status == 4) { + // Call cancelled/ended - dismiss notification + videoConf.cancelCall(rid, callerId); + } else { + Log.d(TAG, "Unknown video conf status: " + status); + } } - @Override - protected Notification.Builder getNotificationBuilder(PendingIntent intent) { - final Notification.Builder notification = new Notification.Builder(mContext); + private void postNotification(int notificationId) { + Notification.Builder notification = buildNotification(notificationId); + if (notification != null && notificationManager != null) { + notificationManager.notify(notificationId, notification.build()); + } + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH + ); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } - Bundle bundle = mNotificationProps.asBundle(); - String notId = bundle.getString("notId", "1"); - String title = bundle.getString("title"); - String message = bundle.getString("message"); - Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false); - Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); + private Notification.Builder buildNotification(int notificationId) { + String notId = Integer.toString(notificationId); + String title = mBundle.getString("title"); + String message = mBundle.getString("message"); + Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false); + Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[getNotificationBuilder] notId=" + notId); - android.util.Log.d(TAG, "[getNotificationBuilder] notificationLoaded=" + notificationLoaded); - android.util.Log.d(TAG, "[getNotificationBuilder] title=" + (title != null ? "[present]" : "[null]")); - android.util.Log.d(TAG, "[getNotificationBuilder] message length=" + (message != null ? message.length() : 0)); - android.util.Log.d(TAG, "[getNotificationBuilder] ejson=" + (ejson != null ? "present" : "null")); - android.util.Log.d(TAG, "[getNotificationBuilder] ejson.notificationType=" + (ejson != null ? ejson.notificationType : "null")); - android.util.Log.d(TAG, "[getNotificationBuilder] ejson.sender=" + (ejson != null && ejson.sender != null ? ejson.sender.username : "null")); + Log.d(TAG, "[buildNotification] notId=" + notId); + Log.d(TAG, "[buildNotification] notificationLoaded=" + notificationLoaded); + Log.d(TAG, "[buildNotification] title=" + (title != null ? "[present]" : "[null]")); + Log.d(TAG, "[buildNotification] message length=" + (message != null ? message.length() : 0)); + } + + // Create pending intent to open the app + Intent intent = new Intent(mContext, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtras(mBundle); + + PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + Notification.Builder notification; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notification = new Notification.Builder(mContext, CHANNEL_ID); + } else { + notification = new Notification.Builder(mContext); } notification .setContentTitle(title) .setContentText(message) - .setContentIntent(intent) + .setContentIntent(pendingIntent) .setPriority(Notification.PRIORITY_HIGH) .setDefaults(Notification.DEFAULT_ALL) .setAutoCancel(true); - Integer notificationId = Integer.parseInt(notId); notificationColor(notification); - notificationChannel(notification); - notificationIcons(notification, bundle); + notificationIcons(notification, mBundle); notificationDismiss(notification, notificationId); // if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully if (ejson == null || ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) { - android.util.Log.i(TAG, "[getNotificationBuilder] ✅ Rendering FULL notification style (ejson=" + (ejson != null) + ", notificationType=" + (ejson != null ? ejson.notificationType : "null") + ", notificationLoaded=" + notificationLoaded + ")"); - notificationStyle(notification, notificationId, bundle); - notificationReply(notification, notificationId, bundle); - - // message couldn't be loaded from server (Fallback notification) + Log.i(TAG, "[buildNotification] ✅ Rendering FULL notification style"); + notificationStyle(notification, notificationId, mBundle); + notificationReply(notification, notificationId, mBundle); } else { - android.util.Log.w(TAG, "[getNotificationBuilder] ⚠️ Rendering FALLBACK notification (ejson=" + (ejson != null) + ", notificationType=" + (ejson != null ? ejson.notificationType : "null") + ", notificationLoaded=" + notificationLoaded + ")"); - // iterate over the current notification ids to dismiss fallback notifications from same server - for (Map.Entry> bundleList : notificationMessages.entrySet()) { - // iterate over the notifications with this id (same host + rid) - Iterator iterator = bundleList.getValue().iterator(); - while (iterator.hasNext()) { - Bundle not = (Bundle) iterator.next(); - // get the notification info - Ejson notEjson = safeFromJson(not.getString("ejson", "{}"), Ejson.class); - // if already has a notification from same server - if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) { - String id = not.getString("notId"); - // cancel this notification - if (notificationManager != null) { - try { - notificationManager.cancel(Integer.parseInt(id)); - if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "Cancelled previous fallback notification from same server"); - } - } catch (NumberFormatException e) { - android.util.Log.e(TAG, "Invalid notification ID for cancel: " + id, e); + Log.w(TAG, "[buildNotification] ⚠️ Rendering FALLBACK notification"); + // Cancel previous fallback notifications from same server + cancelPreviousFallbackNotifications(ejson); + } + + return notification; + } + + private void cancelPreviousFallbackNotifications(Ejson ejson) { + for (Map.Entry> bundleList : notificationMessages.entrySet()) { + Iterator iterator = bundleList.getValue().iterator(); + while (iterator.hasNext()) { + Bundle not = iterator.next(); + Ejson notEjson = safeFromJson(not.getString("ejson", "{}"), Ejson.class); + if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) { + String id = not.getString("notId"); + if (notificationManager != null && id != null) { + try { + notificationManager.cancel(Integer.parseInt(id)); + if (ENABLE_VERBOSE_LOGS) { + Log.d(TAG, "Cancelled previous fallback notification from same server"); } + } catch (NumberFormatException e) { + Log.e(TAG, "Invalid notification ID for cancel: " + id, e); } } } } } - - return notification; - } - - private void notifyReceivedToJS() { - boolean isReactInitialized = mAppLifecycleFacade.isReactInitialized(); - if (isReactInitialized) { - mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); - } } private Bitmap getAvatar(String uri) { if (uri == null || uri.isEmpty()) { if (ENABLE_VERBOSE_LOGS) { - android.util.Log.w(TAG, "getAvatar called with null/empty URI"); + Log.w(TAG, "getAvatar called with null/empty URI"); } return largeIcon(); } - // Sanitize URL for logging (remove query params with tokens) - String sanitizedUri = uri; - int queryStart = uri.indexOf("?"); - if (queryStart != -1) { - sanitizedUri = uri.substring(0, queryStart) + "?[auth_params]"; - } - if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "Fetching avatar from: " + sanitizedUri); + String sanitizedUri = uri; + int queryStart = uri.indexOf("?"); + if (queryStart != -1) { + sanitizedUri = uri.substring(0, queryStart) + "?[auth_params]"; + } + Log.d(TAG, "Fetching avatar from: " + sanitizedUri); } try { @@ -446,16 +477,9 @@ private Bitmap getAvatar(String uri) { .submit(100, 100) .get(); - if (avatar != null) { - if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "Successfully loaded avatar"); - } - } else { - android.util.Log.w(TAG, "Avatar loaded but is null"); - } return avatar != null ? avatar : largeIcon(); } catch (final ExecutionException | InterruptedException e) { - android.util.Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e); + Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e); return largeIcon(); } } @@ -464,16 +488,13 @@ private Bitmap largeIcon() { final Resources res = mContext.getResources(); String packageName = mContext.getPackageName(); int largeIconResId = res.getIdentifier("ic_notification", "drawable", packageName); - Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); - return largeIconBitmap; + return BitmapFactory.decodeResource(res, largeIconResId); } private void notificationIcons(Notification.Builder notification, Bundle bundle) { final Resources res = mContext.getResources(); String packageName = mContext.getPackageName(); - int smallIconResId = res.getIdentifier("ic_notification", "drawable", packageName); - Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); notification.setSmallIcon(smallIconResId); @@ -486,28 +507,6 @@ private void notificationIcons(Notification.Builder notification, Bundle bundle) } } - private void notificationChannel(Notification.Builder notification) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String CHANNEL_ID = "rocketchatrn_channel_01"; - String CHANNEL_NAME = "All"; - - // User-visible importance level: Urgent - Makes a sound and appears as a heads-up notification - // https://developer.android.com/training/notify-user/channels#importance - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, - CHANNEL_NAME, - NotificationManager.IMPORTANCE_HIGH); - - final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null) { - notificationManager.createNotificationChannel(channel); - } else { - android.util.Log.e(TAG, "NotificationManager is null, cannot create notification channel"); - } - - notification.setChannelId(CHANNEL_ID); - } - } - private String extractMessage(String message, Ejson ejson) { if (message == null) { return ""; @@ -515,7 +514,7 @@ private String extractMessage(String message, Ejson ejson) { if (ejson != null && ejson.type != null && !ejson.type.equals("d")) { int pos = message.indexOf(":"); int start = pos == -1 ? 0 : pos + 2; - return message.substring(start, message.length()); + return message.substring(start); } return message; } @@ -530,25 +529,17 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun List bundles = notificationMessages.get(Integer.toString(notId)); if (ENABLE_VERBOSE_LOGS) { - android.util.Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null")); - if (bundles != null && bundles.size() > 0) { - Bundle firstBundle = bundles.get(0); - android.util.Log.d(TAG, "[notificationStyle] first bundle.message length=" + (firstBundle.getString("message") != null ? firstBundle.getString("message").length() : 0)); - android.util.Log.d(TAG, "[notificationStyle] first bundle.notificationLoaded=" + firstBundle.getBoolean("notificationLoaded", false)); - } + Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null")); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Notification.InboxStyle messageStyle = new Notification.InboxStyle(); if (bundles != null) { - for (int i = 0; i < bundles.size(); i++) { - Bundle data = bundles.get(i); + for (Bundle data : bundles) { String message = data.getString("message"); - messageStyle.addLine(message); } } - notification.setStyle(messageStyle); } else { Notification.MessagingStyle messageStyle; @@ -567,9 +558,7 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun messageStyle.setConversationTitle(title); if (bundles != null) { - for (int i = 0; i < bundles.size(); i++) { - Bundle data = bundles.get(i); - + for (Bundle data : bundles) { long timestamp = data.getLong("time"); String message = data.getString("message"); String senderId = data.getString("senderId"); @@ -582,18 +571,16 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun messageStyle.addMessage(m, timestamp, senderName); } else { Bitmap avatar = getAvatar(avatarUri); - String senderName = ejson != null ? ejson.senderName : "Unknown"; - Person.Builder sender = new Person.Builder() + Person.Builder senderBuilder = new Person.Builder() .setKey(senderId) .setName(senderName); if (avatar != null) { - sender.setIcon(Icon.createWithBitmap(avatar)); + senderBuilder.setIcon(Icon.createWithBitmap(avatar)); } - Person person = sender.build(); - + Person person = senderBuilder.build(); messageStyle.addMessage(m, timestamp, person); } } @@ -630,8 +617,7 @@ private void notificationReply(Notification.Builder notification, int notificati .setLabel(label) .build(); - CharSequence title = label; - Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent) + Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, label, replyPendingIntent) .addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .build(); @@ -657,11 +643,6 @@ private void notificationLoad(Ejson ejson, Callback callback) { /** * Safely parses JSON string to object with error handling. - * - * @param json JSON string to parse - * @param classOfT Target class type - * @param Type parameter - * @return Parsed object, or null if parsing fails */ private static T safeFromJson(String json, Class classOfT) { if (json == null || json.trim().isEmpty() || json.equals("{}")) { diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java index c12017dd0bc..64f3c3bac59 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java @@ -7,8 +7,6 @@ import com.ammarahmed.mmkv.SecureKeystore; import com.tencent.mmkv.MMKV; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder; import java.math.BigInteger; @@ -37,11 +35,14 @@ public class Ejson { String rid; String type; Sender sender; + Caller caller; // For video conf notifications String messageId; + String callId; // For video conf notifications String notificationType; String messageType; String senderName; String msg; + Integer status; // For video conf: 0=incoming, 4=cancelled String tmid; @@ -73,19 +74,9 @@ private synchronized void ensureMMKVInitialized() { initializationAttempted = true; - // Try to get ReactApplicationContext from available sources + // Get ReactApplicationContext from CustomPushNotification if (this.reactContext == null) { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null) { - Object runningContext = facade.getRunningReactContext(); - if (runningContext instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) runningContext; - } - } - - if (this.reactContext == null) { - this.reactContext = CustomPushNotification.reactApplicationContext; - } + this.reactContext = CustomPushNotification.reactApplicationContext; } // Initialize MMKV if context is available @@ -238,6 +229,11 @@ static class Sender { String name; } + static class Caller { + String _id; + String name; + } + static class Content { String algorithm; String ciphertext; diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java index b9fffc4bba7..f0fd33cea8f 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java @@ -8,8 +8,6 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder; import com.google.gson.Gson; import com.google.gson.JsonObject; import chat.rocket.mobilecrypto.algorithms.AESCrypto; @@ -319,10 +317,8 @@ public String decryptMessage(final Ejson ejson, final Context context) { if (context instanceof ReactApplicationContext) { this.reactContext = (ReactApplicationContext) context; } else { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) facade.getRunningReactContext(); - } + // Fallback to CustomPushNotification's static context + this.reactContext = CustomPushNotification.reactApplicationContext; } if (this.reactContext == null) { @@ -370,10 +366,8 @@ public String decryptMessage(final Ejson ejson, final Context context) { public EncryptionContent encryptMessageContent(final String message, final String id, final Ejson ejson) { try { - AppLifecycleFacade facade = AppLifecycleFacadeHolder.get(); - if (facade != null && facade.getRunningReactContext() instanceof ReactApplicationContext) { - this.reactContext = (ReactApplicationContext) facade.getRunningReactContext(); - } + // Get ReactApplicationContext from CustomPushNotification + this.reactContext = CustomPushNotification.reactApplicationContext; // Use reactContext for database access if (this.reactContext == null) { diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java index 6b6158393fc..eb528df4f3b 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java @@ -127,6 +127,7 @@ public void load(ReactApplicationContext reactApplicationContext, final Ejson ej Request request = new Request.Builder() .header("x-user-id", userId) .header("x-auth-token", userToken) + .header("User-Agent", NotificationHelper.getUserAgent()) .url(urlBuilder.addQueryParameter("id", messageId).build()) .build(); diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt new file mode 100644 index 00000000000..8eaea08cfd4 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt @@ -0,0 +1,23 @@ +package chat.rocket.reactnative.notification + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.turbomodule.core.interfaces.TurboModule + +abstract class NativeVideoConfSpec(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { + + companion object { + const val NAME = "VideoConfModule" + } + + override fun getName(): String = NAME + + @ReactMethod + abstract fun getPendingAction(promise: Promise) + + @ReactMethod + abstract fun clearPendingAction() +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java index ac2f1256301..3b53a0be419 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java @@ -1,5 +1,7 @@ package chat.rocket.reactnative.notification; +import android.os.Build; + import chat.rocket.reactnative.BuildConfig; /** @@ -23,5 +25,17 @@ public static String sanitizeUrl(String url) { } return "[workspace]"; } + + /** + * Get the User-Agent string for API requests + * Format: RC Mobile; android {systemVersion}; v{appVersion} ({buildNumber}) + * @return User-Agent string + */ + public static String getUserAgent() { + String systemVersion = Build.VERSION.RELEASE; + String appVersion = BuildConfig.VERSION_NAME; + int buildNumber = BuildConfig.VERSION_CODE; + return String.format("RC Mobile; android %s; v%s (%d)", systemVersion, appVersion, buildNumber); + } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt new file mode 100644 index 00000000000..c8aba96e6b9 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt @@ -0,0 +1,51 @@ +package chat.rocket.reactnative.notification + +import android.os.Bundle +import android.util.Log +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +/** + * Custom Firebase Messaging Service for Rocket.Chat. + * + * Handles incoming FCM messages and routes them to CustomPushNotification + * for advanced processing (E2E decryption, MessagingStyle, direct reply, etc.) + */ +class RCFirebaseMessagingService : FirebaseMessagingService() { + + companion object { + private const val TAG = "RocketChat.FCM" + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + Log.d(TAG, "FCM message received from: ${remoteMessage.from}") + + val data = remoteMessage.data + if (data.isEmpty()) { + Log.w(TAG, "FCM message has no data payload, ignoring") + return + } + + // Convert FCM data to Bundle for processing + val bundle = Bundle().apply { + data.forEach { (key, value) -> + putString(key, value) + } + } + + // Process the notification + try { + val notification = CustomPushNotification(this, bundle) + notification.onReceived() + } catch (e: Exception) { + Log.e(TAG, "Error processing FCM message", e) + } + } + + override fun onNewToken(token: String) { + Log.d(TAG, "FCM token refreshed") + // Token handling is done by expo-notifications JS layer + // which uses getDevicePushTokenAsync() + super.onNewToken(token) + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java index 428332c3bf3..345596cfd98 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java @@ -27,8 +27,6 @@ import okhttp3.RequestBody; import okhttp3.Response; -import com.wix.reactnativenotifications.core.NotificationIntentAdapter; - public class ReplyBroadcast extends BroadcastReceiver { private Context mContext; private Bundle bundle; @@ -43,7 +41,11 @@ public void onReceive(Context context, Intent intent) { } mContext = context; - bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + // Extract bundle directly from intent extras + bundle = intent.getBundleExtra("pushNotification"); + if (bundle == null) { + bundle = intent.getExtras(); + } notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); String notId = bundle.getString("notId"); @@ -79,6 +81,7 @@ protected void replyToMessage(final Ejson ejson, final int notId, final CharSequ Request request = new Request.Builder() .header("x-auth-token", ejson.token()) .header("x-user-id", ejson.userId()) + .header("User-Agent", NotificationHelper.getUserAgent()) .url(String.format("%s/api/v1/chat.sendMessage", serverURL)) .post(body) .build(); diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt new file mode 100644 index 00000000000..d5aa014fe8c --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt @@ -0,0 +1,73 @@ +package chat.rocket.reactnative.notification + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import chat.rocket.reactnative.MainActivity +import com.google.gson.GsonBuilder + +/** + * Handles video conference notification actions (accept/decline). + * Stores the action for the JS layer to process when the app opens. + */ +class VideoConfBroadcast : BroadcastReceiver() { + + companion object { + private const val TAG = "RocketChat.VideoConf" + } + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + val extras = intent.extras + + if (action == null || extras == null) { + Log.w(TAG, "Received broadcast with null action or extras") + return + } + + Log.d(TAG, "Received video conf action: $action") + + val event = when (action) { + VideoConfNotification.ACTION_ACCEPT -> "accept" + VideoConfNotification.ACTION_DECLINE -> "decline" + else -> { + Log.w(TAG, "Unknown action: $action") + return + } + } + + // Cancel the notification + val notificationId = extras.getInt("notificationId", 0) + if (notificationId != 0) { + VideoConfNotification.cancelById(context, notificationId) + } + + // Build data for JS layer + val data = mapOf( + "notificationType" to (extras.getString("notificationType") ?: "videoconf"), + "rid" to (extras.getString("rid") ?: ""), + "event" to event, + "caller" to mapOf( + "_id" to (extras.getString("callerId") ?: ""), + "name" to (extras.getString("callerName") ?: "") + ) + ) + + // Store action for the JS layer to pick up + val gson = GsonBuilder().create() + val jsonData = gson.toJson(data) + + VideoConfModule.storePendingAction(context, jsonData) + + Log.d(TAG, "Stored video conf action: $event for rid: ${extras.getString("rid")}") + + // Launch the app + val launchIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtras(extras) + putExtra("event", event) + } + context.startActivity(launchIntent) + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt new file mode 100644 index 00000000000..87059ec0cc7 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt @@ -0,0 +1,66 @@ +package chat.rocket.reactnative.notification + +import android.content.Context +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod + +/** + * Native module to expose video conference notification actions to JavaScript. + * Used to retrieve pending video conf actions when the app opens. + */ +class VideoConfModule(reactContext: ReactApplicationContext) : NativeVideoConfSpec(reactContext) { + + companion object { + private const val PREFS_NAME = "RocketChatPrefs" + private const val KEY_VIDEO_CONF_ACTION = "videoConfAction" + + /** + * Stores a video conference action. + * Called from native code when user interacts with video conf notification. + */ + @JvmStatic + fun storePendingAction(context: Context, actionJson: String) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_VIDEO_CONF_ACTION, actionJson) + .apply() + } + } + + /** + * Gets any pending video conference action. + * Returns null if no pending action. + */ + @ReactMethod + override fun getPendingAction(promise: Promise) { + try { + val prefs = reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val action = prefs.getString(KEY_VIDEO_CONF_ACTION, null) + + // Clear the action after reading + action?.let { + prefs.edit().remove(KEY_VIDEO_CONF_ACTION).apply() + } + + promise.resolve(action) + } catch (e: Exception) { + promise.reject("ERROR", e.message) + } + } + + /** + * Clears any pending video conference action. + */ + @ReactMethod + override fun clearPendingAction() { + try { + reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .remove(KEY_VIDEO_CONF_ACTION) + .apply() + } catch (e: Exception) { + // Ignore errors + } + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt new file mode 100644 index 00000000000..b783e33e1b0 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt @@ -0,0 +1,210 @@ +package chat.rocket.reactnative.notification + +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.media.AudioAttributes +import android.media.RingtoneManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.core.app.NotificationCompat +import chat.rocket.reactnative.MainActivity + +/** + * Handles video conference call notifications with call-style UI. + * Displays incoming call notifications with Accept/Decline actions. + */ +class VideoConfNotification(private val context: Context) { + + companion object { + private const val TAG = "RocketChat.VideoConf" + + const val CHANNEL_ID = "video-conf-call" + const val CHANNEL_NAME = "Video Calls" + + const val ACTION_ACCEPT = "chat.rocket.reactnative.ACTION_VIDEO_CONF_ACCEPT" + const val ACTION_DECLINE = "chat.rocket.reactnative.ACTION_VIDEO_CONF_DECLINE" + const val EXTRA_NOTIFICATION_DATA = "notification_data" + + /** + * Cancels a video call notification by notification ID. + */ + @JvmStatic + fun cancelById(context: Context, notificationId: Int) { + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + manager?.cancel(notificationId) + Log.d(TAG, "Video call notification cancelled with ID: $notificationId") + } + } + + private val notificationManager: NotificationManager? = + context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + + init { + createNotificationChannel() + } + + /** + * Creates the notification channel for video calls with high importance and ringtone sound. + */ + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Incoming video conference calls" + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + + // Set ringtone sound + val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + setSound(ringtoneUri, audioAttributes) + } + + notificationManager?.createNotificationChannel(channel) + } + } + + /** + * Displays an incoming video call notification. + * + * @param bundle The notification data bundle + * @param ejson The parsed notification payload + */ + fun showIncomingCall(bundle: Bundle, ejson: Ejson) { + val rid = ejson.rid + // Video conf uses 'caller' field, regular messages use 'sender' + val callerId: String + val callerName: String + + if (ejson.caller != null) { + callerId = ejson.caller._id ?: "" + callerName = ejson.caller.name ?: "Unknown" + } else if (ejson.sender != null) { + // Fallback to sender if caller is not present + callerId = ejson.sender._id ?: "" + callerName = ejson.sender.name ?: ejson.senderName ?: "Unknown" + } else { + callerId = "" + callerName = "Unknown" + } + + // Generate unique notification ID from rid + callerId + val notificationIdStr = (rid + callerId).replace(Regex("[^A-Za-z0-9]"), "") + val notificationId = notificationIdStr.hashCode() + + Log.d(TAG, "Showing incoming call notification from: $callerName") + + // Create intent data for actions - include all required fields for JS + val intentData = Bundle().apply { + putString("rid", rid ?: "") + putString("notificationType", "videoconf") + putString("callerId", callerId) + putString("callerName", callerName) + putString("host", ejson.host ?: "") + putString("callId", ejson.callId ?: "") + putString("ejson", bundle.getString("ejson", "{}")) + putInt("notificationId", notificationId) + } + + Log.d(TAG, "Intent data - rid: $rid, callerId: $callerId, host: ${ejson.host}, callId: ${ejson.callId}") + + // Full screen intent - opens app when notification is tapped + val fullScreenIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtras(intentData) + putExtra("event", "default") + } + + val fullScreenPendingIntent = createPendingIntent(notificationId, fullScreenIntent) + + // Accept action - directly opens MainActivity (Android 12+ blocks trampoline pattern) + val acceptIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtras(intentData) + putExtra("event", "accept") + putExtra("videoConfAction", true) + action = "${ACTION_ACCEPT}_$notificationId" // Unique action to differentiate intents + } + + val acceptPendingIntent = createPendingIntent(notificationId + 1, acceptIntent) + + // Decline action - directly opens MainActivity + val declineIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtras(intentData) + putExtra("event", "decline") + putExtra("videoConfAction", true) + action = "${ACTION_DECLINE}_$notificationId" // Unique action to differentiate intents + } + + val declinePendingIntent = createPendingIntent(notificationId + 2, declineIntent) + + // Get icons + val packageName = context.packageName + val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName) + + // Build notification + val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply { + setSmallIcon(smallIconResId) + setContentTitle("Incoming call") + setContentText("Video call from $callerName") + priority = NotificationCompat.PRIORITY_HIGH + setCategory(NotificationCompat.CATEGORY_CALL) + setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + setAutoCancel(false) + setOngoing(true) + setFullScreenIntent(fullScreenPendingIntent, true) + setContentIntent(fullScreenPendingIntent) + addAction(0, "Decline", declinePendingIntent) + addAction(0, "Accept", acceptPendingIntent) + } + + // Set sound for pre-O devices + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + builder.setSound(ringtoneUri) + } + + // Show notification + notificationManager?.notify(notificationId, builder.build()) + Log.d(TAG, "Video call notification displayed with ID: $notificationId") + } + + /** + * Creates a PendingIntent with appropriate flags for the Android version. + */ + private fun createPendingIntent(requestCode: Int, intent: Intent): PendingIntent { + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + return PendingIntent.getActivity(context, requestCode, intent, flags) + } + + /** + * Cancels a video call notification. + * + * @param rid The room ID + * @param callerId The caller's user ID + */ + fun cancelCall(rid: String, callerId: String) { + val notificationIdStr = (rid + callerId).replace(Regex("[^A-Za-z0-9]"), "") + val notificationId = notificationIdStr.hashCode() + + notificationManager?.cancel(notificationId) + Log.d(TAG, "Video call notification cancelled with ID: $notificationId") + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt new file mode 100644 index 00000000000..42e3f3eb211 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt @@ -0,0 +1,37 @@ +package chat.rocket.reactnative.notification + +import com.facebook.react.TurboReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +/** + * React Native TurboModule package for video conference notification module. + */ +class VideoConfTurboPackage : TurboReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == NativeVideoConfSpec.NAME) { + VideoConfModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + mapOf( + NativeVideoConfSpec.NAME to ReactModuleInfo( + NativeVideoConfSpec.NAME, + NativeVideoConfSpec.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // hasConstants + false, // isCxxModule + true // isTurboModule + ) + ) + } + } +} diff --git a/app/index.tsx b/app/index.tsx index 3e87e3bcaa0..6b8cd73920f 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -138,7 +138,10 @@ export default class Root extends React.Component<{}, IState> { return; } - await getInitialNotification(); + const handledVideoConf = await getInitialNotification(); + if (handledVideoConf) { + return; + } // Open app from deep linking const deepLinking = await Linking.getInitialURL(); diff --git a/app/lib/native/NativeVideoConfAndroid.ts b/app/lib/native/NativeVideoConfAndroid.ts new file mode 100644 index 00000000000..90346d97cb8 --- /dev/null +++ b/app/lib/native/NativeVideoConfAndroid.ts @@ -0,0 +1,9 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + getPendingAction(): Promise; + clearPendingAction(): void; +} + +export default TurboModuleRegistry.get('VideoConfModule'); diff --git a/app/lib/notifications/index.ts b/app/lib/notifications/index.ts index 1d7ad538494..65adab40d62 100644 --- a/app/lib/notifications/index.ts +++ b/app/lib/notifications/index.ts @@ -17,39 +17,47 @@ interface IEjson { export const onNotification = (push: INotification): void => { const identifier = String(push?.payload?.action?.identifier); + + // Handle video conf notification actions (Accept/Decline buttons) if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') { - if (push?.payload && push?.payload?.ejson) { - const notification = EJSON.parse(push?.payload?.ejson); + if (push?.payload?.ejson) { + const notification = EJSON.parse(push.payload.ejson); store.dispatch(deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' })); return; } } - if (push?.payload) { - try { - const notification = push?.payload; - if (notification.ejson) { - const { rid, name, sender, type, host, messageId }: IEjson = EJSON.parse(notification.ejson); - const types: Record = { - c: 'channel', - d: 'direct', - p: 'group', - l: 'channels' - }; - let roomName = type === SubscriptionType.DIRECT ? sender.username : name; - if (type === SubscriptionType.OMNICHANNEL) { - roomName = sender.name; - } + if (push?.payload?.ejson) { + try { + const notification = EJSON.parse(push.payload.ejson); - const params = { - host, - rid, - messageId, - path: `${types[type]}/${roomName}` - }; - store.dispatch(deepLinkingOpen(params)); + // Handle video conf notification tap (default action) - treat as accept + if (notification?.notificationType === 'videoconf') { + store.dispatch(deepLinkingClickCallPush({ ...notification, event: 'accept' })); return; } + + // Handle regular message notifications + const { rid, name, sender, type, host, messageId }: IEjson = notification; + const types: Record = { + c: 'channel', + d: 'direct', + p: 'group', + l: 'channels' + }; + let roomName = type === SubscriptionType.DIRECT ? sender.username : name; + if (type === SubscriptionType.OMNICHANNEL) { + roomName = sender.name; + } + + const params = { + host, + rid, + messageId, + path: `${types[type]}/${roomName}` + }; + store.dispatch(deepLinkingOpen(params)); + return; } catch (e) { console.warn(e); } @@ -58,12 +66,14 @@ export const onNotification = (push: INotification): void => { }; export const getDeviceToken = (): string => deviceToken; -export const setBadgeCount = (count?: number): void => setNotificationsBadgeCount(count); -export const removeNotificationsAndBadge = () => { - removeAllNotifications(); - setBadgeCount(); +export const setBadgeCount = (count?: number): void => { + setNotificationsBadgeCount(count); +}; +export const removeNotificationsAndBadge = async (): Promise => { + await removeAllNotifications(); + await setNotificationsBadgeCount(); }; -export const initializePushNotifications = (): Promise | undefined => { - setBadgeCount(); +export const initializePushNotifications = async (): Promise => { + await setNotificationsBadgeCount(); return pushNotificationConfigure(onNotification); }; diff --git a/app/lib/notifications/push.ts b/app/lib/notifications/push.ts index 639e83733ef..9dc160ff2e3 100644 --- a/app/lib/notifications/push.ts +++ b/app/lib/notifications/push.ts @@ -1,13 +1,6 @@ -import { - Notifications, - type Registered, - type RegistrationError, - type NotificationCompletion, - type Notification, - NotificationAction, - NotificationCategory -} from 'react-native-notifications'; -import { PermissionsAndroid, Platform } from 'react-native'; +import * as Notifications from 'expo-notifications'; +import * as Device from 'expo-device'; +import { Platform } from 'react-native'; import { type INotification } from '../../definitions'; import { isIOS } from '../methods/helpers'; @@ -16,18 +9,154 @@ import I18n from '../../i18n'; export let deviceToken = ''; -export const setNotificationsBadgeCount = (count = 0): void => { - if (isIOS) { - Notifications.ios.setBadgeCount(count); +export const setNotificationsBadgeCount = async (count = 0): Promise => { + try { + await Notifications.setBadgeCountAsync(count); + } catch (e) { + console.log('Failed to set badge count:', e); } }; -export const removeAllNotifications = (): void => { - Notifications.removeAllDeliveredNotifications(); +export const removeAllNotifications = async (): Promise => { + try { + await Notifications.dismissAllNotificationsAsync(); + } catch (e) { + console.log('Failed to dismiss notifications:', e); + } }; let configured = false; +/** + * Transform expo-notifications response to the INotification format expected by the app + */ +const transformNotificationResponse = (response: Notifications.NotificationResponse): INotification => { + const { notification, actionIdentifier, userText } = response; + const { trigger, content } = notification.request; + + // Get the raw data from the notification + let payload: Record = {}; + + if (trigger && 'type' in trigger && trigger.type === 'push') { + if (Platform.OS === 'android' && 'remoteMessage' in trigger && trigger.remoteMessage) { + // Android: data comes from remoteMessage.data + payload = trigger.remoteMessage.data || {}; + } else if (Platform.OS === 'ios' && 'payload' in trigger && trigger.payload) { + // iOS: data comes from payload (userInfo) + payload = trigger.payload as Record; + } + } + + // Fallback to content.data if trigger data is not available + if (Object.keys(payload).length === 0 && content.data) { + payload = content.data as Record; + } + + // Add action identifier if it's a specific action (not default tap) + if (actionIdentifier && actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) { + payload.action = { identifier: actionIdentifier }; + if (userText) { + payload.action.userText = userText; + } + } + + return { + payload: { + message: content.body || payload.message || '', + style: payload.style || '', + ejson: payload.ejson || '', + collapse_key: payload.collapse_key || '', + notId: payload.notId || notification.request.identifier || '', + msgcnt: payload.msgcnt || '', + title: content.title || payload.title || '', + from: payload.from || '', + image: payload.image || '', + soundname: payload.soundname || '', + action: payload.action + }, + identifier: notification.request.identifier + }; +}; + +/** + * Set up notification categories for iOS (actions like Reply, Accept, Decline) + */ +const setupNotificationCategories = async (): Promise => { + if (!isIOS) { + return; + } + + try { + // Message category with Reply action + await Notifications.setNotificationCategoryAsync('MESSAGE', [ + { + identifier: 'REPLY_ACTION', + buttonTitle: I18n.t('Reply'), + textInput: { + submitButtonTitle: I18n.t('Reply'), + placeholder: I18n.t('Type_message') + }, + options: { + opensAppToForeground: false + } + } + ]); + + // Video conference category with Accept/Decline actions + await Notifications.setNotificationCategoryAsync('VIDEOCONF', [ + { + identifier: 'ACCEPT_ACTION', + buttonTitle: I18n.t('accept'), + options: { + opensAppToForeground: true + } + }, + { + identifier: 'DECLINE_ACTION', + buttonTitle: I18n.t('decline'), + options: { + opensAppToForeground: true + } + } + ]); + } catch (e) { + console.log('Failed to set notification categories:', e); + } +}; + +/** + * Request notification permissions and register for push notifications + */ +const registerForPushNotifications = async (): Promise => { + if (!Device.isDevice) { + console.log('Push notifications require a physical device'); + return null; + } + + try { + // Check and request permissions + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + + if (finalStatus !== 'granted') { + console.log('Failed to get push notification permissions'); + return null; + } + + // Get the device push token (FCM for Android, APNs for iOS) + const tokenData = await Notifications.getDevicePushTokenAsync(); + return tokenData.data; + } catch (e) { + console.log('Error registering for push notifications:', e); + return null; + } +}; + export const pushNotificationConfigure = (onNotification: (notification: INotification) => void): Promise => { if (configured) { return Promise.resolve({ configured: true }); @@ -35,50 +164,37 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi configured = true; - if (isIOS) { - // init - Notifications.ios.registerRemoteNotifications(); - - const notificationAction = new NotificationAction('REPLY_ACTION', 'background', I18n.t('Reply'), true, { - buttonTitle: I18n.t('Reply'), - placeholder: I18n.t('Type_message') - }); - const acceptAction = new NotificationAction('ACCEPT_ACTION', 'foreground', I18n.t('accept'), true); - const rejectAction = new NotificationAction('DECLINE_ACTION', 'foreground', I18n.t('decline'), true); - - const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]); - const videoConfCategory = new NotificationCategory('VIDEOCONF', [acceptAction, rejectAction]); - - Notifications.setCategories([videoConfCategory, notificationCategory]); - } else if (Platform.OS === 'android' && Platform.constants.Version >= 33) { - // @ts-ignore - PermissionsAndroid.request('android.permission.POST_NOTIFICATIONS').then(permissionStatus => { - if (permissionStatus === 'granted') { - Notifications.registerRemoteNotifications(); - } else { - // TODO: Ask user to enable notifications - } - }); - } else { - Notifications.registerRemoteNotifications(); - } + // Set up how notifications should be handled when the app is in foreground + Notifications.setNotificationHandler({ + handleNotification: () => + Promise.resolve({ + shouldShowAlert: false, + shouldPlaySound: false, + shouldSetBadge: false, + shouldShowBanner: false, + shouldShowList: false + }) + }); + + // Set up notification categories for iOS + setupNotificationCategories(); - Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => { - deviceToken = event.deviceToken; + // Register for push notifications and get token + registerForPushNotifications().then(token => { + if (token) { + deviceToken = token; + } }); - Notifications.events().registerRemoteNotificationsRegistrationFailed((event: RegistrationError) => { - // TODO: Handle error - console.log(event); + // Listen for token updates + Notifications.addPushTokenListener(tokenData => { + deviceToken = tokenData.data; }); - Notifications.events().registerNotificationReceivedForeground( - (notification: Notification, completion: (response: NotificationCompletion) => void) => { - completion({ alert: false, sound: false, badge: false }); - } - ); + // Listen for notification responses (when user taps on notification) + Notifications.addNotificationResponseReceivedListener(response => { + const notification = transformNotificationResponse(response); - Notifications.events().registerNotificationOpened((notification: Notification, completion: () => void) => { if (isIOS) { const { background } = reduxStore.getState().app; if (background) { @@ -87,14 +203,13 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi } else { onNotification(notification); } - completion(); }); - Notifications.events().registerNotificationReceivedBackground( - (notification: Notification, completion: (response: any) => void) => { - completion({ alert: true, sound: true, badge: false }); - } - ); + // Get initial notification (app was opened by tapping a notification) + const lastResponse = Notifications.getLastNotificationResponse(); + if (lastResponse) { + return Promise.resolve(transformNotificationResponse(lastResponse)); + } - return Notifications.getInitialNotification(); + return Promise.resolve(null); }; diff --git a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts b/app/lib/notifications/videoConf/backgroundNotificationHandler.ts deleted file mode 100644 index ca33cc13787..00000000000 --- a/app/lib/notifications/videoConf/backgroundNotificationHandler.ts +++ /dev/null @@ -1,125 +0,0 @@ -import notifee, { AndroidCategory, AndroidFlags, AndroidImportance, AndroidVisibility, type Event } from '@notifee/react-native'; -import { getMessaging as messaging } from '@react-native-firebase/messaging'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import ejson from 'ejson'; - -import { deepLinkingClickCallPush } from '../../../actions/deepLinking'; -import i18n from '../../../i18n'; -import { colors } from '../../constants/colors'; -import { store } from '../../store/auxStore'; - -const VIDEO_CONF_CHANNEL = 'video-conf-call'; -const VIDEO_CONF_TYPE = 'videoconf'; - -interface Caller { - _id?: string; - name?: string; -} - -interface NotificationData { - notificationType?: string; - status?: number; - rid?: string; - caller?: Caller; -} - -const createChannel = () => - notifee.createChannel({ - id: VIDEO_CONF_CHANNEL, - name: 'Video Call', - lights: true, - vibration: true, - importance: AndroidImportance.HIGH, - sound: 'ringtone' - }); - -const handleBackgroundEvent = async (event: Event) => { - const { pressAction, notification } = event.detail; - const notificationData = notification?.data; - if ( - typeof notificationData?.caller === 'object' && - (notificationData.caller as Caller)?._id && - (event.type === 1 || event.type === 2) - ) { - if (store?.getState()?.app.ready) { - store.dispatch(deepLinkingClickCallPush({ ...notificationData, event: pressAction?.id })); - } else { - AsyncStorage.setItem('pushNotification', JSON.stringify({ ...notificationData, event: pressAction?.id })); - } - await notifee.cancelNotification( - `${notificationData.rid}${(notificationData.caller as Caller)._id}`.replace(/[^A-Za-z0-9]/g, '') - ); - } -}; - -const backgroundNotificationHandler = () => { - notifee.onBackgroundEvent(handleBackgroundEvent); -}; - -const displayVideoConferenceNotification = async (notification: NotificationData) => { - const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, ''); - const actions = [ - { - title: i18n.t('accept'), - pressAction: { - id: 'accept', - launchActivity: 'default' - } - }, - { - title: i18n.t('decline'), - pressAction: { - id: 'decline', - launchActivity: 'default' - } - } - ]; - - await notifee.displayNotification({ - id, - title: i18n.t('conference_call'), - body: `${i18n.t('Incoming_call_from')} ${notification.caller?.name}`, - data: notification as { [key: string]: string | number | object }, - android: { - channelId: VIDEO_CONF_CHANNEL, - category: AndroidCategory.CALL, - visibility: AndroidVisibility.PUBLIC, - importance: AndroidImportance.HIGH, - smallIcon: 'ic_notification', - color: colors.light.badgeBackgroundLevel4, - actions, - lightUpScreen: true, - loopSound: true, - sound: 'ringtone', - autoCancel: false, - ongoing: true, - pressAction: { - id: 'default', - launchActivity: 'default' - }, - flags: [AndroidFlags.FLAG_NO_CLEAR] - } - }); -}; - -const setBackgroundNotificationHandler = () => { - createChannel(); - messaging().setBackgroundMessageHandler(async message => { - if (message?.data?.ejson) { - const notification: NotificationData = ejson.parse(message?.data?.ejson as string); - if (notification?.notificationType === VIDEO_CONF_TYPE) { - if (notification.status === 0) { - await displayVideoConferenceNotification(notification); - } else if (notification.status === 4) { - const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, ''); - await notifee.cancelNotification(id); - } - } - } - - return null; - }); -}; - -setBackgroundNotificationHandler(); -backgroundNotificationHandler(); diff --git a/app/lib/notifications/videoConf/getInitialNotification.ts b/app/lib/notifications/videoConf/getInitialNotification.ts index 5e48a842ce1..25acc7cfe79 100644 --- a/app/lib/notifications/videoConf/getInitialNotification.ts +++ b/app/lib/notifications/videoConf/getInitialNotification.ts @@ -1,15 +1,62 @@ +import * as Notifications from 'expo-notifications'; +import EJSON from 'ejson'; +import { Platform } from 'react-native'; + import { deepLinkingClickCallPush } from '../../../actions/deepLinking'; -import { isAndroid } from '../../methods/helpers'; import { store } from '../../store/auxStore'; +import NativeVideoConfModule from '../../native/NativeVideoConfAndroid'; -export const getInitialNotification = async (): Promise => { - if (isAndroid) { - const notifee = require('@notifee/react-native').default; - const initialNotification = await notifee.getInitialNotification(); - if (initialNotification?.notification?.data?.notificationType === 'videoconf') { - store.dispatch( - deepLinkingClickCallPush({ ...initialNotification?.notification?.data, event: initialNotification?.pressAction?.id }) - ); +/** + * Check for pending video conference actions from native notification handling. + * @returns true if a video conf action was found and dispatched, false otherwise + */ +export const getInitialNotification = async (): Promise => { + // Android: Check native module for pending action + if (Platform.OS === 'android' && NativeVideoConfModule) { + try { + const pendingAction = await NativeVideoConfModule.getPendingAction(); + if (pendingAction) { + const data = JSON.parse(pendingAction); + if (data?.notificationType === 'videoconf') { + store.dispatch(deepLinkingClickCallPush(data)); + return true; + } + } + } catch (error) { + console.log('Error getting video conf initial notification:', error); } } + + // iOS: Check expo-notifications for last response with video conf action + if (Platform.OS === 'ios') { + try { + const lastResponse = await Notifications.getLastNotificationResponseAsync(); + if (lastResponse) { + const { actionIdentifier, notification } = lastResponse; + const { trigger } = notification.request; + let payload: Record = {}; + + if (trigger && 'type' in trigger && trigger.type === 'push' && 'payload' in trigger && trigger.payload) { + payload = trigger.payload as Record; + } + + if (payload.ejson) { + const ejsonData = EJSON.parse(payload.ejson); + if (ejsonData?.notificationType === 'videoconf') { + // Accept/Decline actions or default tap (treat as accept) + let event = 'accept'; + if (actionIdentifier === 'DECLINE_ACTION') { + event = 'decline'; + } + store.dispatch(deepLinkingClickCallPush({ ...ejsonData, event })); + return true; + } + } + } + } catch (error) { + console.log('Error getting iOS video conf initial notification:', error); + } + } + + return false; }; diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index abc28aba567..7afcadc7b92 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -223,7 +223,7 @@ const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) { if (room) { const isMasterDetail = yield select(state => state.app.isMasterDetail); yield navigateToRoom({ item: room, isMasterDetail }); - const uid = params.caller._id; + const uid = params.caller?._id; const { rid, callId, event } = params; if (event === 'accept') { yield call(notifyUser, `${uid}/video-conference`, { @@ -246,6 +246,10 @@ const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) { const handleClickCallPush = function* handleClickCallPush({ params }) { let { host } = params; + if (!host) { + return; + } + if (host.slice(-1) === '/') { host = host.slice(0, host.length - 1); } diff --git a/app/sagas/troubleshootingNotification.ts b/app/sagas/troubleshootingNotification.ts index 0717c083f19..193568890e9 100644 --- a/app/sagas/troubleshootingNotification.ts +++ b/app/sagas/troubleshootingNotification.ts @@ -1,6 +1,6 @@ import { type Action } from 'redux'; import { call, takeLatest, put } from 'typed-redux-saga'; -import notifee, { AuthorizationStatus } from '@notifee/react-native'; +import * as Notifications from 'expo-notifications'; import { TROUBLESHOOTING_NOTIFICATION } from '../actions/actionsTypes'; import { setTroubleshootingNotification } from '../actions/troubleshootingNotification'; @@ -19,8 +19,8 @@ function* init() { let defaultPushGateway = false; let pushGatewayEnabled = false; try { - const { authorizationStatus } = yield* call(notifee.getNotificationSettings); - deviceNotificationEnabled = authorizationStatus > AuthorizationStatus.DENIED; + const { status } = yield* call(Notifications.getPermissionsAsync); + deviceNotificationEnabled = status === 'granted'; } catch (e) { log(e); } diff --git a/app/views/JitsiMeetView/index.tsx b/app/views/JitsiMeetView/index.tsx index 67544b8de63..fe86c05d1c7 100644 --- a/app/views/JitsiMeetView/index.tsx +++ b/app/views/JitsiMeetView/index.tsx @@ -2,7 +2,7 @@ import CookieManager from '@react-native-cookies/cookies'; import { type RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import React, { useCallback, useEffect, useState } from 'react'; -import { ActivityIndicator, BackHandler, Linking, SafeAreaView, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, Linking, SafeAreaView, StyleSheet, View } from 'react-native'; import WebView, { type WebViewNavigation } from 'react-native-webview'; import { userAgent } from '../../lib/constants/userAgent'; @@ -52,8 +52,8 @@ const JitsiMeetView = (): React.ReactElement => { await Linking.openURL(`org.jitsi.meet://${callUrl}`); goBack(); } catch (error) { - // As the jitsi app was not opened, disable the backhandler on android - BackHandler.addEventListener('hardwareBackPress', () => true); + // Jitsi app not installed - will use WebView instead + // No need to block back button as WebView handles navigation } }, [goBack, url]); diff --git a/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx index 9e77e43c47b..0adf5702b9d 100644 --- a/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx +++ b/app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx @@ -1,4 +1,3 @@ -import notifee from '@notifee/react-native'; import React from 'react'; import { Linking } from 'react-native'; @@ -19,7 +18,7 @@ export default function DeviceNotificationSettings(): React.ReactElement { if (isIOS) { Linking.openURL('app-settings:'); } else { - notifee.openNotificationSettings(); + Linking.openSettings(); } }; diff --git a/index.js b/index.js index 4844e1da5dd..778e46478d1 100644 --- a/index.js +++ b/index.js @@ -22,9 +22,8 @@ if (process.env.USE_STORYBOOK) { LogBox.ignoreAllLogs(); - if (isAndroid) { - require('./app/lib/notifications/videoConf/backgroundNotificationHandler'); - } + // Note: Android video conference notifications are now handled natively + // in RCFirebaseMessagingService -> CustomPushNotification -> VideoConfNotification AppRegistry.registerComponent(appName, () => require('./app/index').default); } diff --git a/ios/AppDelegate.swift b/ios/AppDelegate.swift index 386a6feffd6..d5697908b38 100644 --- a/ios/AppDelegate.swift +++ b/ios/AppDelegate.swift @@ -27,8 +27,7 @@ public class AppDelegate: ExpoAppDelegate { MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: .debug) } - // Initialize notifications - RNNotifications.startMonitorNotifications() + // Configure reply notification handler (integrates with expo-notifications) ReplyNotification.configure() let delegate = ReactNativeDelegate() @@ -63,19 +62,6 @@ public class AppDelegate: ExpoAppDelegate { return result } - // Remote Notification handling - public override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - RNNotifications.didRegisterForRemoteNotifications(withDeviceToken: deviceToken) - } - - public override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - RNNotifications.didFailToRegisterForRemoteNotificationsWithError(error) - } - - public override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - RNNotifications.didReceiveBackgroundNotification(userInfo, withCompletionHandler: completionHandler) - } - // Linking API public override func application( _ app: UIApplication, diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 1d18a413e39..57cf88cf17b 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -13,11 +13,18 @@ class NotificationService: UNNotificationServiceExtension { if let bestAttemptContent = bestAttemptContent { let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)! guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else { + contentHandler(bestAttemptContent) return } rocketchat = RocketChat(server: data.host.removeTrailingSlash()) + // Handle video conference notifications + if data.notificationType == .videoconf { + self.processVideoConf(payload: data, request: request) + return + } + // If the notification has the content on the payload, show it if data.notificationType != .messageIdOnly { self.processPayload(payload: data) @@ -35,17 +42,49 @@ class NotificationService: UNNotificationServiceExtension { } // Request the content from server - self.rocketchat?.getPushWithId(data.messageId) { notification in - if let notification = notification { - self.bestAttemptContent?.title = notification.title - self.bestAttemptContent?.body = notification.text - self.processPayload(payload: notification.payload) + if let messageId = data.messageId { + self.rocketchat?.getPushWithId(messageId) { notification in + if let notification = notification { + self.bestAttemptContent?.title = notification.title + self.bestAttemptContent?.body = notification.text + self.processPayload(payload: notification.payload) + } } } } } } + func processVideoConf(payload: Payload, request: UNNotificationRequest) { + guard let bestAttemptContent = bestAttemptContent else { + return + } + + // Status 4 means call cancelled/ended - remove any existing notification + if payload.status == 4 { + if let rid = payload.rid, let callerId = payload.caller?._id { + let notificationId = "\(rid)\(callerId)".replacingOccurrences(of: "[^A-Za-z0-9]", with: "", options: .regularExpression) + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId]) + } + // Don't show anything for cancelled calls + contentHandler?(UNNotificationContent()) + return + } + + // Status 0 (or nil) means incoming call - show notification with actions + let callerName = payload.caller?.name ?? "Unknown" + + bestAttemptContent.title = NSLocalizedString("Video Call", comment: "") + bestAttemptContent.body = String(format: NSLocalizedString("Incoming call from %@", comment: ""), callerName) + bestAttemptContent.categoryIdentifier = "VIDEOCONF" + bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName("ringtone.mp3")) + if #available(iOS 15.0, *) { + bestAttemptContent.interruptionLevel = .timeSensitive + } + + contentHandler?(bestAttemptContent) + } + func processPayload(payload: Payload) { // If is a encrypted message if payload.messageType == .e2e { diff --git a/ios/Podfile b/ios/Podfile index 693b2016291..9fe51d0a279 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -36,8 +36,7 @@ end $static_framework = [ 'WatermelonDB', 'simdjson', - 'react-native-mmkv-storage', - 'react-native-notifications' + 'react-native-mmkv-storage' ] pre_install do |installer| Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b443f1d65a8..0ab9d11f83b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -27,11 +27,15 @@ PODS: - BVLinearGradient (2.6.2): - React-Core - DoubleConversion (1.1.6) + - EXApplication (7.0.8): + - ExpoModulesCore - EXAV (15.1.3): - ExpoModulesCore - ReactCommon/turbomodule/core - EXConstants (17.1.5): - ExpoModulesCore + - EXNotifications (0.32.14): + - ExpoModulesCore - Expo (53.0.7): - DoubleConversion - ExpoModulesCore @@ -67,6 +71,8 @@ PODS: - ExpoModulesCore - ZXingObjC/OneD - ZXingObjC/PDF417 + - ExpoDevice (8.0.10): + - ExpoModulesCore - ExpoDocumentPicker (13.1.4): - ExpoModulesCore - ExpoFileSystem (18.1.7): @@ -1713,8 +1719,6 @@ PODS: - Yoga - react-native-netinfo (11.3.1): - React-Core - - react-native-notifications (5.1.0): - - React-Core - react-native-restart (0.0.22): - React-Core - react-native-safe-area-context (5.4.0): @@ -2385,11 +2389,6 @@ PODS: - React-Core - RNLocalize (2.1.1): - React-Core - - RNNotifee (7.8.2): - - React-Core - - RNNotifee/NotifeeCore (= 7.8.2) - - RNNotifee/NotifeeCore (7.8.2): - - React-Core - RNReanimated (3.17.1): - DoubleConversion - glog @@ -2643,12 +2642,15 @@ DEPENDENCIES: - "BugsnagReactNative (from `../node_modules/@bugsnag/react-native`)" - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXApplication (from `../node_modules/expo-application/ios`) - EXAV (from `../node_modules/expo-av/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) + - EXNotifications (from `../node_modules/expo-notifications/ios`) - Expo (from `../node_modules/expo`) - ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoCamera (from `../node_modules/expo-camera/ios`) + - ExpoDevice (from `../node_modules/expo-device/ios`) - ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) @@ -2706,7 +2708,6 @@ DEPENDENCIES: - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" @@ -2758,7 +2759,6 @@ DEPENDENCIES: - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNKeychain (from `../node_modules/react-native-keychain`) - RNLocalize (from `../node_modules/react-native-localize`) - - "RNNotifee (from `../node_modules/@notifee/react-native`)" - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) @@ -2805,10 +2805,14 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-linear-gradient" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/expo-application/ios" EXAV: :path: "../node_modules/expo-av/ios" EXConstants: :path: "../node_modules/expo-constants/ios" + EXNotifications: + :path: "../node_modules/expo-notifications/ios" Expo: :path: "../node_modules/expo" ExpoAppleAuthentication: @@ -2817,6 +2821,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-asset/ios" ExpoCamera: :path: "../node_modules/expo-camera/ios" + ExpoDevice: + :path: "../node_modules/expo-device/ios" ExpoDocumentPicker: :path: "../node_modules/expo-document-picker/ios" ExpoFileSystem: @@ -2928,8 +2934,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-mmkv-storage" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" - react-native-notifications: - :path: "../node_modules/react-native-notifications" react-native-restart: :path: "../node_modules/react-native-restart" react-native-safe-area-context: @@ -3032,8 +3036,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-keychain" RNLocalize: :path: "../node_modules/react-native-localize" - RNNotifee: - :path: "../node_modules/@notifee/react-native" RNReanimated: :path: "../node_modules/react-native-reanimated" RNScreens: @@ -3052,12 +3054,15 @@ SPEC CHECKSUMS: BugsnagReactNative: 8150cc1facb5c69c7a5d27d614fc50b4ed03c2b8 BVLinearGradient: 7815a70ab485b7b155186dd0cc836363e0288cad DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb + EXApplication: 1e98d4b1dccdf30627f92917f4b2c5a53c330e5f EXAV: 90c33266835bf7e61dc0731fa8d82833d22d286f EXConstants: a1112af878fddfe6acc0399473ac56a07ced0f47 + EXNotifications: 52c982ff8aa4ed971534e27f20dbf532aa4082b1 Expo: bb70dfd014457bcca19f33e8c783afdc18308434 ExpoAppleAuthentication: b589b71be6bb817decf8f35e92c6281365140289 ExpoAsset: 3bc9adb7dbbf27ae82c18ca97eb988a3ae7e73b1 ExpoCamera: 105a9a963c443a3e112c51dd81290d81cd8da94a + ExpoDevice: 6327c3c200816795708885adf540d26ecab83d1a ExpoDocumentPicker: 344f16224e6a8a088f2693667a8b713160f8f57b ExpoFileSystem: 175267faf2b38511b01ac110243b13754dac57d3 ExpoFont: abbb91a911eb961652c2b0a22eef801860425ed6 @@ -3133,7 +3138,6 @@ SPEC CHECKSUMS: react-native-keyboard-controller: 9ec7ee23328c30251a399cffd8b54324a00343bf react-native-mmkv-storage: 9afc38c25213482f668c80bf2f0a50f75dda1777 react-native-netinfo: 2e3c27627db7d49ba412bfab25834e679db41e21 - react-native-notifications: 3bafa1237ae8a47569a84801f17d80242fe9f6a5 react-native-restart: f6f591aeb40194c41b9b5013901f00e6cf7d0f29 react-native-safe-area-context: 5928d84c879db2f9eb6969ca70e68f58623dbf25 react-native-slider: 605e731593322c4bb2eb48d7d64e2e4dbf7cbd77 @@ -3185,7 +3189,6 @@ SPEC CHECKSUMS: RNImageCropPicker: b219389d3a300679b396e81d501e8c8169ffa3c0 RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4 RNLocalize: ca86348d88b9a89da0e700af58d428ab3f343c4e - RNNotifee: 8768d065bf1e2f9f8f347b4bd79147431c7eacd6 RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642 RNScreens: b13e4c45f0406f33986a39c0d8da0324bff94435 RNSVG: 680e961f640e381aab730a04b2371969686ed9f7 @@ -3200,6 +3203,6 @@ SPEC CHECKSUMS: Yoga: dfabf1234ccd5ac41d1b1d43179f024366ae9831 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 4c73563b34520b90c036817cdb9ccf65fea5f5c5 +PODFILE CHECKSUM: 80f90fcad8e4b1f21b36f1dbbae085fa0ddff08c COCOAPODS: 1.15.2 diff --git a/ios/ReplyNotification.swift b/ios/ReplyNotification.swift index cffb486528e..3d3a2de7403 100644 --- a/ios/ReplyNotification.swift +++ b/ios/ReplyNotification.swift @@ -9,49 +9,110 @@ import Foundation import UserNotifications +/// Handles direct reply from iOS notifications. +/// Intercepts REPLY_ACTION responses and sends messages natively, +/// while forwarding all other notification events to expo-notifications. @objc(ReplyNotification) -class ReplyNotification: RNNotificationEventHandler { - private static let dispatchOnce: Void = { - let instance: AnyClass! = object_getClass(ReplyNotification()) - let originalMethod = class_getInstanceMethod(instance, #selector(didReceive)) - let swizzledMethod = class_getInstanceMethod(instance, #selector(replyNotification_didReceiveNotificationResponse)) - if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod { - method_exchangeImplementations(originalMethod, swizzledMethod) - } - }() +class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { + private static var shared: ReplyNotification? + private weak var originalDelegate: UNUserNotificationCenterDelegate? @objc public static func configure() { - _ = self.dispatchOnce + let instance = ReplyNotification() + shared = instance + + // Store the original delegate (expo-notifications) and set ourselves as the delegate + let center = UNUserNotificationCenter.current() + instance.originalDelegate = center.delegate + center.delegate = instance } - @objc - func replyNotification_didReceiveNotificationResponse(_ response: UNNotificationResponse, completionHandler: @escaping(() -> Void)) { + // MARK: - UNUserNotificationCenterDelegate + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + // Handle REPLY_ACTION natively if response.actionIdentifier == "REPLY_ACTION" { - if let notification = RCTConvert.unNotificationPayload(response.notification) { - if let data = (notification["ejson"] as? String)?.data(using: .utf8) { - if let payload = try? JSONDecoder().decode(Payload.self, from: data), let rid = payload.rid { - if let msg = (response as? UNTextInputNotificationResponse)?.userText { - let rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) - let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) - - rocketchat.sendMessage(rid: rid, message: msg, threadIdentifier: payload.tmid) { response in - guard let response = response, response.success else { - let content = UNMutableNotificationContent() - content.body = "Failed to reply message." - let request = UNNotificationRequest(identifier: "replyFailure", content: content, trigger: nil) - UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - return - } - UIApplication.shared.endBackgroundTask(backgroundTask) - } - } - } + handleReplyAction(response: response, completionHandler: completionHandler) + return + } + + // Forward to original delegate (expo-notifications) + if let originalDelegate = originalDelegate { + originalDelegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler) + } else { + completionHandler() + } + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + // Forward to original delegate (expo-notifications) + if let originalDelegate = originalDelegate { + originalDelegate.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler) + } else { + completionHandler([]) + } + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification? + ) { + // Forward to original delegate (expo-notifications) + if let originalDelegate = originalDelegate { + if #available(iOS 12.0, *) { + originalDelegate.userNotificationCenter?(center, openSettingsFor: notification) + } + } + } + + // MARK: - Reply Handling + + private func handleReplyAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) { + guard let textResponse = response as? UNTextInputNotificationResponse else { + completionHandler() + return + } + + let userInfo = response.notification.request.content.userInfo + + guard let ejsonString = userInfo["ejson"] as? String, + let ejsonData = ejsonString.data(using: .utf8), + let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData), + let rid = payload.rid else { + completionHandler() + return + } + + let message = textResponse.userText + let rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) + let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) + + rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in + // Ensure we're on the main thread for UI operations + DispatchQueue.main.async { + defer { + UIApplication.shared.endBackgroundTask(backgroundTask) + completionHandler() + } + + guard let response = response, response.success else { + // Show failure notification + let content = UNMutableNotificationContent() + content.body = "Failed to reply message." + let request = UNNotificationRequest(identifier: "replyFailure", content: content, trigger: nil) + UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) + return } } - } else { - let body = RNNotificationParser.parseNotificationResponse(response) - RNEventEmitter.sendEvent(RNNotificationOpened, body: body) } } } diff --git a/ios/RocketChatRN-Bridging-Header.h b/ios/RocketChatRN-Bridging-Header.h index 84cdd29326b..528f9e1aa39 100644 --- a/ios/RocketChatRN-Bridging-Header.h +++ b/ios/RocketChatRN-Bridging-Header.h @@ -4,12 +4,6 @@ #import #import -#import -#import -#import -#import -#import -#import #import #import #import diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 7f679cbc227..80fbac43faf 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -1540,8 +1540,11 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", @@ -1570,8 +1573,11 @@ name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", @@ -1627,8 +1633,11 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", @@ -1657,8 +1666,11 @@ name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", @@ -1958,8 +1970,11 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/BugsnagReactNative/Bugsnag.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", @@ -1988,8 +2003,11 @@ name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Bugsnag.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", diff --git a/ios/Shared/Models/NotificationType.swift b/ios/Shared/Models/NotificationType.swift index 95e41bed78c..0e769aa0910 100644 --- a/ios/Shared/Models/NotificationType.swift +++ b/ios/Shared/Models/NotificationType.swift @@ -11,4 +11,5 @@ import Foundation enum NotificationType: String, Codable { case message = "message" case messageIdOnly = "message-id-only" + case videoconf = "videoconf" } diff --git a/ios/Shared/Models/Payload.swift b/ios/Shared/Models/Payload.swift index 124ccf612e6..2a91f3a1b38 100644 --- a/ios/Shared/Models/Payload.swift +++ b/ios/Shared/Models/Payload.swift @@ -8,12 +8,17 @@ import Foundation +struct Caller: Codable { + let _id: String? + let name: String? +} + struct Payload: Codable { let host: String let rid: String? let type: RoomType? let sender: Sender? - let messageId: String + let messageId: String? let notificationType: NotificationType? let name: String? let messageType: MessageType? @@ -21,4 +26,9 @@ struct Payload: Codable { let senderName: String? let tmid: String? let content: EncryptedContent? + + // Video conference fields + let caller: Caller? + let callId: String? + let status: Int? } diff --git a/ios/Shared/RocketChat/API/Request.swift b/ios/Shared/RocketChat/API/Request.swift index a1f6a728c57..97c54bf8574 100644 --- a/ios/Shared/RocketChat/API/Request.swift +++ b/ios/Shared/RocketChat/API/Request.swift @@ -55,6 +55,7 @@ extension Request { request.httpMethod = method.rawValue request.httpBody = body() request.addValue(contentType, forHTTPHeaderField: "Content-Type") + request.addValue(userAgent, forHTTPHeaderField: "User-Agent") if let userId = api.credentials?.userId { request.addValue(userId, forHTTPHeaderField: "x-user-id") @@ -69,4 +70,14 @@ extension Request { return request } + + private var userAgent: String { + let osVersion = ProcessInfo.processInfo.operatingSystemVersion + let systemVersion = "\(osVersion.majorVersion).\(osVersion.minorVersion)" + + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown" + + return "RC Mobile; ios \(systemVersion); v\(appVersion) (\(buildNumber))" + } } diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index f8df411c23d..318f743be2b 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -122,7 +122,7 @@ platform :ios do build_path: "./fastlane/build", configuration: "Release", derived_data_path: "./fastlane/derived_data", - xcargs: "-parallelizeTargets -jobs 4" + xcargs: "-parallelizeTargets -jobs 4 ARCHS=arm64" ) end diff --git a/jest.setup.js b/jest.setup.js index 6374b2beb62..774f02d2527 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -136,18 +136,23 @@ jest.mock('@react-navigation/native', () => { }; }); -jest.mock('react-native-notifications', () => ({ - Notifications: { - getInitialNotification: jest.fn(() => Promise.resolve()), - registerRemoteNotifications: jest.fn(), - events: () => ({ - registerRemoteNotificationsRegistered: jest.fn(), - registerRemoteNotificationsRegistrationFailed: jest.fn(), - registerNotificationReceivedForeground: jest.fn(), - registerNotificationReceivedBackground: jest.fn(), - registerNotificationOpened: jest.fn() - }) - } +jest.mock('expo-notifications', () => ({ + getDevicePushTokenAsync: jest.fn(() => Promise.resolve({ data: 'mock-token' })), + getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })), + requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })), + setBadgeCountAsync: jest.fn(() => Promise.resolve(true)), + dismissAllNotificationsAsync: jest.fn(() => Promise.resolve()), + setNotificationHandler: jest.fn(), + setNotificationCategoryAsync: jest.fn(() => Promise.resolve()), + addNotificationReceivedListener: jest.fn(() => ({ remove: jest.fn() })), + addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })), + addPushTokenListener: jest.fn(() => ({ remove: jest.fn() })), + getLastNotificationResponse: jest.fn(() => null), + DEFAULT_ACTION_IDENTIFIER: 'expo.modules.notifications.actions.DEFAULT' +})); + +jest.mock('expo-device', () => ({ + isDevice: true })); jest.mock('@discord/bottom-sheet', () => { diff --git a/package.json b/package.json index 18036f1628e..489c43244bc 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@expo/vector-icons": "^14.1.0", "@hookform/resolvers": "^2.9.10", - "@notifee/react-native": "7.8.2", "@nozbe/watermelondb": "^0.28.1-0", "@react-native-async-storage/async-storage": "^1.22.3", "@react-native-camera-roll/camera-roll": "^7.10.0", @@ -41,7 +40,6 @@ "@react-native-firebase/analytics": "^21.12.2", "@react-native-firebase/app": "^21.12.2", "@react-native-firebase/crashlytics": "^21.12.2", - "@react-native-firebase/messaging": "^21.12.2", "@react-native-masked-view/masked-view": "^0.3.1", "@react-native-picker/picker": "^2.11.0", "@react-native/codegen": "^0.80.0", @@ -63,6 +61,7 @@ "expo-apple-authentication": "7.2.3", "expo-av": "15.1.3", "expo-camera": "16.1.5", + "expo-device": "^8.0.10", "expo-document-picker": "13.1.4", "expo-file-system": "18.1.7", "expo-haptics": "14.1.3", @@ -70,6 +69,7 @@ "expo-keep-awake": "14.1.3", "expo-local-authentication": "16.0.3", "expo-navigation-bar": "^4.2.4", + "expo-notifications": "^0.32.14", "expo-status-bar": "^2.2.3", "expo-system-ui": "^5.0.7", "expo-video-thumbnails": "9.1.2", @@ -108,7 +108,6 @@ "react-native-mime-types": "2.3.0", "react-native-mmkv-storage": "^12.0.0", "react-native-modal": "13.0.1", - "react-native-notifications": "5.1.0", "react-native-notifier": "1.6.1", "react-native-picker-select": "9.0.1", "react-native-platform-touchable": "1.1.1", diff --git a/patches/@notifee+react-native+7.8.2.patch b/patches/@notifee+react-native+7.8.2.patch deleted file mode 100644 index cdb17b7853a..00000000000 --- a/patches/@notifee+react-native+7.8.2.patch +++ /dev/null @@ -1,41 +0,0 @@ ---- a/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java -+++ b/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java -@@ -238,7 +238,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm - @ReactMethod - public void requestPermission(Promise promise) { - // For Android 12 and below, we return the notification settings -- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { -+ if (Build.VERSION.SDK_INT < 33) { - Notifee.getInstance() - .getNotificationSettings( - (e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle)); -@@ -265,7 +265,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm - (e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle)); - - activity.requestPermissions( -- new String[] {Manifest.permission.POST_NOTIFICATIONS}, -+ new String[] {"android.permission.POST_NOTIFICATIONS"}, - Notifee.REQUEST_CODE_NOTIFICATION_PERMISSION, - this); - } -diff --git a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m -index cf8020d..3a1e080 100644 ---- a/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m -+++ b/node_modules/@notifee/react-native/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m -@@ -179,11 +179,11 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center - - _notificationOpenedAppID = notifeeNotification[@"id"]; - -- // handle notification outside of notifee -- if (notifeeNotification == nil) { -- notifeeNotification = -- [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request]; -- } -+ // disable notifee handler on ios devices -+ // if (notifeeNotification == nil) { -+ // notifeeNotification = -+ // [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request]; -+ // } - - if (notifeeNotification != nil) { - if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) { diff --git a/patches/@react-native-firebase+messaging+21.12.2.patch b/patches/@react-native-firebase+messaging+21.12.2.patch deleted file mode 100644 index 34905325f15..00000000000 --- a/patches/@react-native-firebase+messaging+21.12.2.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml b/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml -index 2f6741b..d4f4abb 100644 ---- a/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml -+++ b/node_modules/@react-native-firebase/messaging/android/src/main/AndroidManifest.xml -@@ -9,12 +9,12 @@ - - -- - - - -- -+ --> - --@import UserNotifications; -+#import - - @interface RCTConvert (UIMutableUserNotificationAction) - + (UIMutableUserNotificationAction *)UIMutableUserNotificationAction:(id)json; -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h -index 4bc5292..71df0bc 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.h -@@ -4,7 +4,7 @@ typedef void (^RCTPromiseResolveBlock)(id result); - typedef void (^RCTResponseSenderBlock)(NSArray *response); - typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error); - --@import UserNotifications; -+#import - - @interface RNNotificationCenter : NSObject - -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m -index afd5c73..ec4dd85 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenter.m -@@ -48,14 +48,15 @@ - } - - - (void)setCategories:(NSArray *)json { -- NSMutableSet* categories = nil; -+ NSMutableSet* categories = [NSMutableSet new]; - -- if ([json count] > 0) { -- categories = [NSMutableSet new]; -- for (NSDictionary* categoryJson in json) { -- [categories addObject:[RCTConvert UNMutableUserNotificationCategory:categoryJson]]; -+ for (NSDictionary* categoryJson in json) { -+ UNNotificationCategory *category = [RCTConvert UNMutableUserNotificationCategory:categoryJson]; -+ if (category) { -+ [categories addObject:category]; - } - } -+ - [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:categories]; - } - -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h b/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h -index 77a67dd..eaf0043 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationCenterListener.h -@@ -1,5 +1,5 @@ - #import --@import UserNotifications; -+#import - #import "RNNotificationEventHandler.h" - - @interface RNNotificationCenterListener : NSObject -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h -index a07c6e9..8e3ca6a 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.h -@@ -1,5 +1,5 @@ - #import --@import UserNotifications; -+#import - #import "RNNotificationsStore.h" - #import "RNEventEmitter.h" - -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h -index 7aa2bfb..c1c019c 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.h -@@ -1,5 +1,5 @@ - #import --@import UserNotifications; -+#import - - @interface RNNotificationParser : NSObject - -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m -index 62f3043..840acda 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationParser.m -@@ -12,7 +12,18 @@ - } - - + (NSDictionary *)parseNotificationResponse:(UNNotificationResponse *)response { -- NSDictionary* responseDict = @{@"notification": [RCTConvert UNNotificationPayload:response.notification], @"identifier": response.notification.request.identifier, @"action": [self parseNotificationResponseAction:response]}; -+ NSMutableDictionary *notificationPayload = [[RCTConvert UNNotificationPayload:response.notification] mutableCopy]; -+ -+ NSDictionary *responseAction = [self parseNotificationResponseAction:response]; -+ -+ if (responseAction != nil) { -+ [notificationPayload setObject:responseAction forKey:@"action"]; -+ } -+ -+ NSDictionary *responseDict = @{ -+ @"notification": [notificationPayload copy], -+ @"identifier": response.notification.request.identifier -+ }; - - return responseDict; - } -diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h b/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h -index 4f8a171..7e4f9ca 100644 ---- a/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h -+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationsStore.h -@@ -1,6 +1,6 @@ - #import - #import --@import UserNotifications; -+#import - - @interface RNNotificationsStore : NSObject - diff --git a/react-native.config.js b/react-native.config.js index 685aec05f4b..5af09eb7525 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,9 +1,3 @@ module.exports = { - dependencies: { - '@react-native-firebase/messaging': { - platforms: { - ios: null - } - } - } + dependencies: {} }; diff --git a/yarn.lock b/yarn.lock index 42818383dc4..a9dc56dfc17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2750,6 +2750,26 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@~54.0.3": + version "54.0.3" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-54.0.3.tgz#2b9ffd68a48e3b51299cdbe3ee777b9f5163fc03" + integrity sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw== + dependencies: + "@expo/config-types" "^54.0.9" + "@expo/json-file" "~10.0.7" + "@expo/plist" "^0.4.7" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.5" + getenv "^2.0.0" + glob "^13.0.0" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + "@expo/config-types@^53.0.3": version "53.0.3" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.3.tgz#d083d9b095972e89eee96c41d085feb5b92d2749" @@ -2760,6 +2780,11 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.4.tgz#fe64fac734531ae883d18529b32586c23ffb1ceb" integrity sha512-0s+9vFx83WIToEr0Iwy4CcmiUXa5BgwBmEjylBB2eojX5XAMm9mJvw9KpjAb8m7zq2G0Q6bRbeufkzgbipuNQg== +"@expo/config-types@^54.0.9": + version "54.0.9" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-54.0.9.tgz#b9279c47fe249b774fbd3358b6abddea08f1bcec" + integrity sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw== + "@expo/config@~11.0.7", "@expo/config@~11.0.8": version "11.0.8" resolved "https://registry.yarnpkg.com/@expo/config/-/config-11.0.8.tgz#658538d4321cf6edf6741f8b8506fda0046d5e94" @@ -2798,6 +2823,25 @@ slugify "^1.3.4" sucrase "3.35.0" +"@expo/config@~12.0.11": + version "12.0.11" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-12.0.11.tgz#b9f1cecd6bac4c2101bb8489b918556dc100434f" + integrity sha512-bGKNCbHirwgFlcOJHXpsAStQvM0nU3cmiobK0o07UkTfcUxl9q9lOQQh2eoMGqpm6Vs1IcwBpYye6thC3Nri/w== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~54.0.3" + "@expo/config-types" "^54.0.9" + "@expo/json-file" "^10.0.7" + deepmerge "^4.3.1" + getenv "^2.0.0" + glob "^13.0.0" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + resolve-workspace-root "^2.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "~3.35.1" + "@expo/devcert@^1.1.2": version "1.1.4" resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.1.4.tgz#d98807802a541847cc42791a606bfdc26e641277" @@ -2827,6 +2871,17 @@ dotenv-expand "~11.0.6" getenv "^1.0.0" +"@expo/env@~2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@expo/env/-/env-2.0.8.tgz#2aea906eed3d297b2e19608dc1a800fba0a3fe03" + integrity sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA== + dependencies: + chalk "^4.0.0" + debug "^4.3.4" + dotenv "~16.4.5" + dotenv-expand "~11.0.6" + getenv "^2.0.0" + "@expo/fingerprint@0.12.4": version "0.12.4" resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.12.4.tgz#d4cc4de50e7b6d4e03b0d38850d1e4a136b74c8c" @@ -2858,6 +2913,30 @@ temp-dir "~2.0.0" unique-string "~2.0.0" +"@expo/image-utils@^0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.8.8.tgz#db5d460fd0c7101b10e9d027ffbe42f9cf115248" + integrity sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA== + dependencies: + "@expo/spawn-async" "^1.7.2" + chalk "^4.0.0" + getenv "^2.0.0" + jimp-compact "0.16.1" + parse-png "^2.1.0" + resolve-from "^5.0.0" + resolve-global "^1.0.0" + semver "^7.6.0" + temp-dir "~2.0.0" + unique-string "~2.0.0" + +"@expo/json-file@^10.0.7", "@expo/json-file@~10.0.7": + version "10.0.8" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-10.0.8.tgz#05e524d1ecc0011db0a6d66b525ea2f58cfe6d43" + integrity sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + "@expo/json-file@^9.1.4", "@expo/json-file@~9.1.4": version "9.1.4" resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.4.tgz#e719d092c08afb3234643f9285e57c6a24989327" @@ -2920,6 +2999,15 @@ base64-js "^1.2.3" xmlbuilder "^15.1.1" +"@expo/plist@^0.4.7": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.4.8.tgz#e014511a4a5008cf2b832b91caa8e9f2704127cc" + integrity sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.2.3" + xmlbuilder "^15.1.1" + "@expo/prebuild-config@^9.0.5": version "9.0.5" resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-9.0.5.tgz#b8b864b5e19489a1f66442ae30d5d7295f658297" @@ -3433,6 +3521,23 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@ide/backoff@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176" + integrity sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g== + +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -3825,11 +3930,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@notifee/react-native@7.8.2": - version "7.8.2" - resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.2.tgz#72d3199ae830b4128ddaef3c1c2f11604759c9c4" - integrity sha512-VG4IkWJIlOKqXwa3aExC3WFCVCGCC9BA55Ivg0SMRfEs+ruvYy/zlLANcrVGiPtgkUEryXDhA8SXx9+JcO8oLA== - "@nozbe/simdjson@3.9.4": version "3.9.4" resolved "https://registry.yarnpkg.com/@nozbe/simdjson/-/simdjson-3.9.4.tgz#64bb522c54cd22e40ff4a64d8f8e8e9285b123b6" @@ -4281,11 +4381,6 @@ dependencies: stacktrace-js "^2.0.2" -"@react-native-firebase/messaging@^21.12.2": - version "21.12.2" - resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-21.12.2.tgz#9d8ec5dd21c562a6b5c5d3699fadde419c9f2a75" - integrity sha512-t/MQgqclINESO/1yCbgXTHJxAdxIzAnjtgTw6bj/po/1JRGroT+HG3VDpep9a3Z35S4f6eF90AE5zZzcKu8IjQ== - "@react-native-masked-view/masked-view@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.3.1.tgz#5bd76f17004a6ccbcec03856893777ee91f23d29" @@ -6123,6 +6218,17 @@ asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +assert@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -6406,6 +6512,11 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" +badgin@^1.1.5: + version "1.2.3" + resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.3.tgz#994b5f519827d7d5422224825b2c8faea2bc43ad" + integrity sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -6595,6 +6706,16 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- es-errors "^1.3.0" function-bind "^1.1.2" +call-bind@^1.0.0, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -6606,16 +6727,6 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" @@ -8322,6 +8433,11 @@ expo-apple-authentication@7.2.3: resolved "https://registry.yarnpkg.com/expo-apple-authentication/-/expo-apple-authentication-7.2.3.tgz#524817c1b2c0b165343039d183ee91c4674be5fe" integrity sha512-2izNn8qhUUM/gXMxA2byOn4AymUpmhaZlnGZy1vpndT0dMXd3T/Wk8j67rEB0+JhQY11iEQGXBG8cfro7LV0dA== +expo-application@~7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-7.0.8.tgz#320af0d6c39b331456d3bc833b25763c702d23db" + integrity sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q== + expo-asset@~11.1.5: version "11.1.5" resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.1.5.tgz#5cad3d781c9d0edec31b9b3adbba574eb4d5dd3e" @@ -8350,6 +8466,21 @@ expo-constants@~17.1.5: "@expo/config" "~11.0.7" "@expo/env" "~1.0.5" +expo-constants@~18.0.11: + version "18.0.11" + resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-18.0.11.tgz#522680c14f0a54cfd968d581961848c6397d44ae" + integrity sha512-xnfrfZ7lHjb+03skhmDSYeFF7OU2K3Xn/lAeP+7RhkV2xp2f5RCKtOUYajCnYeZesvMrsUxOsbGOP2JXSOH3NA== + dependencies: + "@expo/config" "~12.0.11" + "@expo/env" "~2.0.8" + +expo-device@^8.0.10: + version "8.0.10" + resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-8.0.10.tgz#88be854d6de5568392ed814b44dad0e19d1d50f8" + integrity sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA== + dependencies: + ua-parser-js "^0.7.33" + expo-document-picker@13.1.4: version "13.1.4" resolved "https://registry.yarnpkg.com/expo-document-picker/-/expo-document-picker-13.1.4.tgz#f78a91e31dfac8ff26ea065bdc015ce4938eb1cc" @@ -8429,6 +8560,19 @@ expo-navigation-bar@^4.2.4: react-native-edge-to-edge "1.6.0" react-native-is-edge-to-edge "^1.1.6" +expo-notifications@^0.32.14: + version "0.32.14" + resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.32.14.tgz#8f2161207e81272aac2fe4f1981b2ff7c8ef5529" + integrity sha512-IRxzsd94+c1sim7R9OWdICPINmL4iwsLWcG3n6FKgzZal2ZZbBym2/m/k5yv3NQORUpytqB373WBJDZvaPCtgw== + dependencies: + "@expo/image-utils" "^0.8.8" + "@ide/backoff" "^1.0.0" + abort-controller "^3.0.0" + assert "^2.0.0" + badgin "^1.1.5" + expo-application "~7.0.8" + expo-constants "~18.0.11" + expo-status-bar@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-2.2.3.tgz#09385a866732328e0af3b4588c4f349a15fd7cd0" @@ -8936,6 +9080,11 @@ getenv@^1.0.0: resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31" integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg== +getenv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0" + integrity sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ== + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -8967,6 +9116,15 @@ glob@^10.3.10, glob@^10.4.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" + integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== + dependencies: + minimatch "^10.1.1" + minipass "^7.1.2" + path-scurry "^2.0.0" + glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -8979,6 +9137,13 @@ glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg== + dependencies: + ini "^1.3.4" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -9384,7 +9549,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -9562,6 +9727,14 @@ is-map@^2.0.3: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-negative-zero@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" @@ -10784,6 +10957,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +lru-cache@^11.0.0: + version "11.2.4" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" + integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== + lru-cache@^4.1.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -11352,6 +11530,13 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" +minimatch@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" + integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -11639,6 +11824,14 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -11995,6 +12188,14 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10" + integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -12567,11 +12768,6 @@ react-native-modal@13.0.1: prop-types "^15.6.2" react-native-animatable "1.3.3" -react-native-notifications@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/react-native-notifications/-/react-native-notifications-5.1.0.tgz#8cba105fd57ab9d5df9d27284acf1e2b4f3d7ea3" - integrity sha512-laqDSDlCvEASmJR6cXpqaryK855ejQd07vrfYERzhv68YDOoSkKy/URExRP4vAfAOVqHhix80tLbNUcfvZk2VQ== - react-native-notifier@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/react-native-notifier/-/react-native-notifier-1.6.1.tgz#eec07bdebed6c22cd22f5167555b7762e4119552" @@ -13105,6 +13301,13 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-global@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" + integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== + dependencies: + global-dirs "^0.1.1" + resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" @@ -14039,6 +14242,19 @@ sucrase@3.35.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" +sucrase@~3.35.1: + version "3.35.1" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1" + integrity sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + tinyglobby "^0.2.11" + ts-interface-checker "^0.1.9" + sudo-prompt@^8.2.0: version "8.2.5" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.2.5.tgz#cc5ef3769a134bb94b24a631cc09628d4d53603e" @@ -14242,7 +14458,7 @@ tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tinyglobby@^0.2.14: +tinyglobby@^0.2.11, tinyglobby@^0.2.14: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== @@ -14535,7 +14751,7 @@ typical@^5.2.0: resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== -ua-parser-js@1.0.2: +ua-parser-js@1.0.2, ua-parser-js@^0.7.33: version "1.0.2" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== @@ -14729,7 +14945,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -util@^0.12.4: +util@^0.12.4, util@^0.12.5: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==