From f7aef7f6fab62a04d08a7515c312caf649992751 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 6 Jan 2026 14:52:27 -0300 Subject: [PATCH 01/17] feat: Avatar on push notifications (#6853) --- .../notification/CustomPushNotification.java | 92 ++--- .../reactnative/notification/Ejson.java | 74 +++- .../reactnative/notification/Encryption.java | 97 ++++- .../notification/NotificationHelper.java | 50 ++- .../notification/VideoConfBroadcast.kt | 2 + .../notification/VideoConfNotification.kt | 22 + app/lib/notifications/index.ts | 24 +- ios/NotificationService/Info.plist | 11 + .../NotificationService.entitlements | 2 + .../NotificationService.swift | 378 +++++++++++++----- ios/ReplyNotification.swift | 23 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 340 ++++++---------- ios/RocketChatRN/Info.plist | 4 + ios/RocketChatRN/RocketChatRN.entitlements | 2 + ios/Shared/Extensions/Bundle+Extensions.swift | 9 + ios/Shared/Models/Payload.swift | 1 + ios/Shared/RocketChat/API/Request.swift | 12 +- 17 files changed, 747 insertions(+), 396 deletions(-) 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 cad28734ea4..6865aff023e 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 @@ -18,9 +18,6 @@ import androidx.annotation.Nullable; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; -import com.bumptech.glide.request.RequestOptions; import com.facebook.react.bridge.ReactApplicationContext; import com.google.gson.Gson; @@ -30,9 +27,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import chat.rocket.reactnative.BuildConfig; import chat.rocket.reactnative.MainActivity; @@ -61,7 +55,7 @@ public class CustomPushNotification { // Instance fields private final Context mContext; - private Bundle mBundle; + private volatile Bundle mBundle; private final NotificationManager notificationManager; public CustomPushNotification(Context context, Bundle bundle) { @@ -300,9 +294,6 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { bundle.putString("senderId", hasSender ? ejson.sender._id : "1"); String avatarUri = ejson != null ? ejson.getAvatarUri() : null; - if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]")); - } bundle.putString("avatarUri", avatarUri); // Handle special notification types @@ -379,10 +370,27 @@ private Notification.Builder buildNotification(int notificationId) { Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false); Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); + // Determine the correct title based on notification type + String notificationTitle = title; + if (ejson != null && ejson.type != null) { + if ("p".equals(ejson.type) || "c".equals(ejson.type)) { + // For groups/channels, use room name if available, otherwise fall back to title + notificationTitle = (ejson.name != null && !ejson.name.isEmpty()) ? ejson.name : title; + } else if ("d".equals(ejson.type)) { + // For direct messages, use title (sender name from server) + notificationTitle = title; + } else if ("l".equals(ejson.type)) { + // For omnichannel, use sender name if available, otherwise fall back to title + notificationTitle = (ejson.sender != null && ejson.sender.name != null && !ejson.sender.name.isEmpty()) + ? ejson.sender.name : title; + } + } + if (ENABLE_VERBOSE_LOGS) { 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] notificationTitle=" + (notificationTitle != null ? "[present]" : "[null]")); Log.d(TAG, "[buildNotification] message length=" + (message != null ? message.length() : 0)); } @@ -406,7 +414,7 @@ private Notification.Builder buildNotification(int notificationId) { } notification - .setContentTitle(title) + .setContentTitle(notificationTitle) .setContentText(message) .setContentIntent(pendingIntent) .setPriority(Notification.PRIORITY_HIGH) @@ -455,37 +463,7 @@ private void cancelPreviousFallbackNotifications(Ejson ejson) { } private Bitmap getAvatar(String uri) { - if (uri == null || uri.isEmpty()) { - if (ENABLE_VERBOSE_LOGS) { - Log.w(TAG, "getAvatar called with null/empty URI"); - } - return largeIcon(); - } - - if (ENABLE_VERBOSE_LOGS) { - 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 { - // Use a 3-second timeout to avoid blocking the FCM service for too long - // FCM has a 10-second limit, so we need to fail fast and use fallback icon - Bitmap avatar = Glide.with(mContext) - .asBitmap() - .apply(RequestOptions.bitmapTransform(new RoundedCorners(10))) - .load(uri) - .submit(100, 100) - .get(3, TimeUnit.SECONDS); - - return avatar != null ? avatar : largeIcon(); - } catch (final ExecutionException | InterruptedException | TimeoutException e) { - Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e); - return largeIcon(); - } + return NotificationHelper.fetchAvatarBitmap(mContext, uri, largeIcon()); } private Bitmap largeIcon() { @@ -506,7 +484,10 @@ private void notificationIcons(Notification.Builder notification, Bundle bundle) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { String avatarUri = ejson != null ? ejson.getAvatarUri() : null; if (avatarUri != null) { - notification.setLargeIcon(getAvatar(avatarUri)); + Bitmap avatar = getAvatar(avatarUri); + if (avatar != null) { + notification.setLargeIcon(avatar); + } } } } @@ -517,8 +498,11 @@ 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); + if (pos == -1) { + return message; + } + int start = pos + 2; + return start <= message.length() ? message.substring(start) : ""; } return message; } @@ -559,7 +543,23 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun } String title = bundle.getString("title"); - messageStyle.setConversationTitle(title); + // Determine the correct conversation title based on notification type + Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); + String conversationTitle = title; + if (bundleEjson != null && bundleEjson.type != null) { + if ("p".equals(bundleEjson.type) || "c".equals(bundleEjson.type)) { + // For groups/channels, use room name if available, otherwise fall back to title + conversationTitle = (bundleEjson.name != null && !bundleEjson.name.isEmpty()) ? bundleEjson.name : title; + } else if ("d".equals(bundleEjson.type)) { + // For direct messages, use title (sender name from server) + conversationTitle = title; + } else if ("l".equals(bundleEjson.type)) { + // For omnichannel, use sender name if available, otherwise fall back to title + conversationTitle = (bundleEjson.sender != null && bundleEjson.sender.name != null && !bundleEjson.sender.name.isEmpty()) + ? bundleEjson.sender.name : title; + } + } + messageStyle.setConversationTitle(conversationTitle); if (bundles != null) { for (Bundle data : bundles) { 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 036d03f24f6..91e72386f94 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 @@ -6,6 +6,8 @@ import com.tencent.mmkv.MMKV; import java.math.BigInteger; +import java.net.URLEncoder; +import java.io.UnsupportedEncodingException; import chat.rocket.reactnative.BuildConfig; import chat.rocket.reactnative.storage.MMKVKeyManager; @@ -40,6 +42,7 @@ public class Ejson { String notificationType; String messageType; String senderName; + String name; // Room name for groups/channels String msg; Integer status; // For video conf: 0=incoming, 4=cancelled @@ -57,15 +60,14 @@ private MMKV getMMKV() { return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE); } - public String getAvatarUri() { - if (sender == null || sender.username == null || sender.username.isEmpty()) { - Log.w(TAG, "Cannot generate avatar URI: sender or username is null"); - return null; - } - + /** + * Helper method to build avatar URI from avatar path. + * Validates server URL and credentials, then constructs the full URI. + */ + private String buildAvatarUri(String avatarPath, String errorContext) { String server = serverURL(); if (server == null || server.isEmpty()) { - Log.w(TAG, "Cannot generate avatar URI: serverURL is null"); + Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null"); return null; } @@ -73,17 +75,64 @@ public String getAvatarUri() { String uid = userId(); if (userToken.isEmpty() || uid.isEmpty()) { - Log.w(TAG, "Cannot generate avatar URI: missing auth credentials (token=" + !userToken.isEmpty() + ", uid=" + !uid.isEmpty() + ")"); + Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: missing auth credentials"); return null; } - String uri = server + "/avatar/" + sender.username + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid; + return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid; + } + + public String getAvatarUri() { + String avatarPath; + + // For DMs, show sender's avatar; for groups/channels, show room avatar + if ("d".equals(type)) { + // Direct message: use sender's avatar + if (sender == null || sender.username == null || sender.username.isEmpty()) { + Log.w(TAG, "Cannot generate avatar URI: sender or username is null"); + return null; + } + try { + avatarPath = "/avatar/" + URLEncoder.encode(sender.username, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Failed to encode username", e); + return null; + } + } else { + // Group/Channel/Livechat: use room avatar + if (rid == null || rid.isEmpty()) { + Log.w(TAG, "Cannot generate avatar URI: rid is null for non-DM"); + return null; + } + try { + avatarPath = "/avatar/room/" + URLEncoder.encode(rid, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Failed to encode rid", e); + return null; + } + } - if (BuildConfig.DEBUG) { - Log.d(TAG, "Generated avatar URI for user: " + sender.username); + return buildAvatarUri(avatarPath, ""); + } + + /** + * Generates avatar URI for video conference caller. + * Returns null if caller username is not available (username is required for avatar endpoint). + */ + public String getCallerAvatarUri() { + // Check if caller exists and has username (required - /avatar/{userId} endpoint doesn't exist) + if (caller == null || caller.username == null || caller.username.isEmpty()) { + Log.w(TAG, "Cannot generate caller avatar URI: caller or username is null"); + return null; } - return uri; + try { + String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8"); + return buildAvatarUri(avatarPath, "caller"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Failed to encode caller username", e); + return null; + } } public String token() { @@ -194,6 +243,7 @@ static class Sender { static class Caller { String _id; String name; + String username; } static class Content { 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 8eafd8d1fff..f6384100613 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 @@ -188,14 +188,23 @@ public Room readRoom(final Ejson ejson, Context context) { } cursor.moveToFirst(); - String e2eKey = cursor.getString(cursor.getColumnIndex("e2e_key")); - Boolean encrypted = cursor.getInt(cursor.getColumnIndex("encrypted")) > 0; + int e2eKeyColumnIndex = cursor.getColumnIndex("e2e_key"); + int encryptedColumnIndex = cursor.getColumnIndex("encrypted"); + + if (e2eKeyColumnIndex == -1) { + Log.e(TAG, "e2e_key column not found in subscriptions table"); + cursor.close(); + return null; + } + + String e2eKey = cursor.getString(e2eKeyColumnIndex); + Boolean encrypted = encryptedColumnIndex != -1 && cursor.getInt(encryptedColumnIndex) > 0; cursor.close(); return new Room(e2eKey, encrypted); } catch (Exception e) { - Log.e("[ENCRYPTION]", "Error reading room", e); + Log.e(TAG, "Error reading room", e); return null; } finally { @@ -236,7 +245,31 @@ public String readUserKey(final Ejson ejson) throws Exception { return null; } - PrivateKey privKey = gson.fromJson(privateKey, PrivateKey.class); + PrivateKey privKey; + try { + // First, try parsing as direct JSON object + privKey = gson.fromJson(privateKey, PrivateKey.class); + } catch (com.google.gson.JsonSyntaxException e) { + // If that fails, it might be a JSON-encoded string (double-encoded) + // Try parsing as a string first, then parse that string as JSON + try { + String decoded = gson.fromJson(privateKey, String.class); + privKey = gson.fromJson(decoded, PrivateKey.class); + } catch (Exception e2) { + Log.e(TAG, "Failed to parse private key", e2); + throw new Exception("Failed to parse private key: " + e2.getMessage(), e2); + } + } + + if (privKey == null) { + return null; + } + + // Validate that required fields are present + if (privKey.n == null || privKey.e == null || privKey.d == null) { + Log.e(TAG, "PrivateKey missing required fields (n, e, or d)"); + return null; + } WritableMap jwk = Arguments.createMap(); jwk.putString("n", privKey.n); @@ -252,9 +285,19 @@ public String readUserKey(final Ejson ejson) throws Exception { } public RoomKeyResult decryptRoomKey(final String e2eKey, final Ejson ejson) throws Exception { + if (e2eKey == null || e2eKey.isEmpty()) { + return null; + } + // Parse using prefixed base64 - PrefixedData parsed = decodePrefixedBase64(e2eKey); - keyId = parsed.prefix; + PrefixedData parsed; + try { + parsed = decodePrefixedBase64(e2eKey); + keyId = parsed.prefix; + } catch (Exception e) { + Log.e(TAG, "Failed to decode prefixed base64", e); + throw e; + } // Decrypt the session key String userKey = readUserKey(ejson); @@ -263,22 +306,54 @@ public RoomKeyResult decryptRoomKey(final String e2eKey, final Ejson ejson) thro } String base64EncryptedData = Base64.encodeToString(parsed.data, Base64.NO_WRAP); - String decrypted = RSACrypto.INSTANCE.decrypt(base64EncryptedData, userKey); + String decrypted; + try { + decrypted = RSACrypto.INSTANCE.decrypt(base64EncryptedData, userKey); + if (decrypted == null) { + return null; + } + } catch (Exception e) { + Log.e(TAG, "RSA decryption failed", e); + throw e; + } // Parse sessionKey to determine v1 vs v2 from "alg" field - JsonObject sessionKey = gson.fromJson(decrypted, JsonObject.class); + JsonObject sessionKey; + try { + sessionKey = gson.fromJson(decrypted, JsonObject.class); + if (sessionKey == null) { + return null; + } + } catch (com.google.gson.JsonSyntaxException e) { + Log.e(TAG, "Failed to parse decrypted session key as JSON", e); + throw new Exception("Failed to parse decrypted session key as JSON: " + e.getMessage(), e); + } + + if (!sessionKey.has("k")) { + return null; + } + String k = sessionKey.get("k").getAsString(); - byte[] decoded = Base64.decode(k, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); + byte[] decoded; + try { + decoded = Base64.decode(k, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); + } catch (Exception e) { + Log.e(TAG, "Failed to decode 'k' from base64", e); + throw e; + } + String decryptedKey = CryptoUtils.INSTANCE.bytesToHex(decoded); // Determine format from "alg" field + String algorithm; if (sessionKey.has("alg") && "A256GCM".equals(sessionKey.get("alg").getAsString())) { algorithm = "rc.v2.aes-sha2"; - return new RoomKeyResult(decryptedKey, "rc.v2.aes-sha2"); } else { algorithm = "rc.v1.aes-sha2"; - return new RoomKeyResult(decryptedKey, "rc.v1.aes-sha2"); } + this.algorithm = algorithm; + + return new RoomKeyResult(decryptedKey, algorithm); } private String decryptContent(Ejson.Content content, String e2eKey) throws Exception { 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 3b53a0be419..db21a2fcb46 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,6 +1,18 @@ package chat.rocket.reactnative.notification; -import android.os.Build; +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import chat.rocket.reactnative.BuildConfig; @@ -32,10 +44,44 @@ public static String sanitizeUrl(String url) { * @return User-Agent string */ public static String getUserAgent() { - String systemVersion = Build.VERSION.RELEASE; + String systemVersion = android.os.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); } + + /** + * Fetches avatar bitmap from URI using Glide. + * Uses a 3-second timeout to avoid blocking the FCM service for too long. + * + * @param context The application context + * @param uri The avatar URI to fetch + * @param fallbackIcon Optional fallback bitmap (null if no fallback desired) + * @return Avatar bitmap, or fallbackIcon if fetch fails, or null if no fallback provided + */ + public static Bitmap fetchAvatarBitmap(Context context, String uri, @Nullable Bitmap fallbackIcon) { + if (uri == null || uri.isEmpty()) { + return fallbackIcon; + } + + try { + // Use a 3-second timeout to avoid blocking the FCM service for too long + // FCM has a 10-second limit, so we need to fail fast and use fallback icon + Bitmap avatar = Glide.with(context) + .asBitmap() + .apply(RequestOptions.bitmapTransform(new RoundedCorners(10))) + .load(uri) + .submit(100, 100) + .get(3, TimeUnit.SECONDS); + + return avatar != null ? avatar : fallbackIcon; + } catch (final ExecutionException | InterruptedException | TimeoutException e) { + Log.e("NotificationHelper", "Failed to fetch avatar", e); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return fallbackIcon; + } + } } 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 index d5aa014fe8c..94f2c9f7682 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt @@ -48,6 +48,8 @@ class VideoConfBroadcast : BroadcastReceiver() { "notificationType" to (extras.getString("notificationType") ?: "videoconf"), "rid" to (extras.getString("rid") ?: ""), "event" to event, + "host" to (extras.getString("host") ?: ""), + "callId" to (extras.getString("callId") ?: ""), "caller" to mapOf( "_id" to (extras.getString("callerId") ?: ""), "name" to (extras.getString("callerName") ?: "") 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 index b783e33e1b0..a2f7bb5a30b 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.media.AudioAttributes import android.media.RingtoneManager +import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.util.Log @@ -155,6 +156,14 @@ class VideoConfNotification(private val context: Context) { val packageName = context.packageName val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName) + // Fetch caller avatar + val avatarUri = ejson.getCallerAvatarUri() + val avatarBitmap = if (avatarUri != null) { + getAvatar(avatarUri) + } else { + null + } + // Build notification val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply { setSmallIcon(smallIconResId) @@ -169,6 +178,11 @@ class VideoConfNotification(private val context: Context) { setContentIntent(fullScreenPendingIntent) addAction(0, "Decline", declinePendingIntent) addAction(0, "Accept", acceptPendingIntent) + + // Set large icon (avatar) if available + if (avatarBitmap != null) { + setLargeIcon(avatarBitmap) + } } // Set sound for pre-O devices @@ -194,6 +208,14 @@ class VideoConfNotification(private val context: Context) { return PendingIntent.getActivity(context, requestCode, intent, flags) } + /** + * Fetches avatar bitmap from URI using Glide. + * Returns null if fetch fails or times out, in which case notification will display without avatar. + */ + private fun getAvatar(uri: String): Bitmap? { + return NotificationHelper.fetchAvatarBitmap(context, uri, null) + } + /** * Cancels a video call notification. * diff --git a/app/lib/notifications/index.ts b/app/lib/notifications/index.ts index 65adab40d62..5f13b44e6cb 100644 --- a/app/lib/notifications/index.ts +++ b/app/lib/notifications/index.ts @@ -21,9 +21,15 @@ export const onNotification = (push: INotification): void => { // Handle video conf notification actions (Accept/Decline buttons) if (identifier === 'ACCEPT_ACTION' || identifier === 'DECLINE_ACTION') { if (push?.payload?.ejson) { - const notification = EJSON.parse(push.payload.ejson); - store.dispatch(deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' })); - return; + try { + const notification = EJSON.parse(push.payload.ejson); + store.dispatch( + deepLinkingClickCallPush({ ...notification, event: identifier === 'ACCEPT_ACTION' ? 'accept' : 'decline' }) + ); + return; + } catch (e) { + console.warn('Failed to parse video conf notification:', e); + } } } @@ -38,6 +44,10 @@ export const onNotification = (push: INotification): void => { } // Handle regular message notifications + if (!notification?.rid || !notification?.type || !notification?.host) { + store.dispatch(appInit()); + return; + } const { rid, name, sender, type, host, messageId }: IEjson = notification; const types: Record = { c: 'channel', @@ -45,9 +55,11 @@ export const onNotification = (push: INotification): void => { p: 'group', l: 'channels' }; - let roomName = type === SubscriptionType.DIRECT ? sender.username : name; - if (type === SubscriptionType.OMNICHANNEL) { - roomName = sender.name; + let roomName = name; + if (type === SubscriptionType.DIRECT) { + roomName = sender?.username ?? name; + } else if (type === SubscriptionType.OMNICHANNEL) { + roomName = sender?.name ?? name; } const params = { diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 9d959502934..24b65a6e595 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -33,5 +33,16 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService + NSUserActivityTypes + + INSendMessageIntent + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + diff --git a/ios/NotificationService/NotificationService.entitlements b/ios/NotificationService/NotificationService.entitlements index 4bfdb2c2b03..8d6bb8f66f9 100644 --- a/ios/NotificationService/NotificationService.entitlements +++ b/ios/NotificationService/NotificationService.entitlements @@ -2,6 +2,8 @@ + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.ios.chat.rocket diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index d337a0061df..00d8ac9549c 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -1,133 +1,331 @@ import UserNotifications +import Intents class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? + var finalContent: UNNotificationContent? var rocketchat: RocketChat? + // MARK: - Notification Lifecycle + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - 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) - return - } - - // Merge missing content notifications - UNUserNotificationCenter.current().getDeliveredNotifications { deliveredNotifications in - let identifiersToRemove = deliveredNotifications.filter { - $0.request.content.body == "You have a new message" - }.map { $0.request.identifier } - - if identifiersToRemove.count > 0 { - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiersToRemove) - } - - // Request the content from server - 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 - - // Update ejson with full payload from server for correct navigation - if let payloadData = try? JSONEncoder().encode(notification.payload), - let payloadString = String(data: payloadData, encoding: .utf8) { - self.bestAttemptContent?.userInfo["ejson"] = payloadString - } - - self.processPayload(payload: notification.payload) - } else { - // Server returned no notification, deliver as-is - if let bestAttemptContent = self.bestAttemptContent { - self.contentHandler?(bestAttemptContent) - } - } - } - } else { - // No messageId available, deliver the notification as-is - if let bestAttemptContent = self.bestAttemptContent { - self.contentHandler?(bestAttemptContent) - } - } - } + guard let bestAttemptContent = bestAttemptContent, + let ejsonString = bestAttemptContent.userInfo["ejson"] as? String, + let ejson = ejsonString.data(using: .utf8), + let payload = try? JSONDecoder().decode(Payload.self, from: ejson) else { + contentHandler(request.content) + return + } + + rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) + + if payload.notificationType == .videoconf { + processVideoConf(payload: payload) + } else if payload.notificationType == .messageIdOnly { + fetchMessageContent(payload: payload) + } else { + processPayload(payload: payload) } } - func processVideoConf(payload: Payload, request: UNNotificationRequest) { - guard let bestAttemptContent = bestAttemptContent else { - return - } + // MARK: - Processors + + func processVideoConf(payload: Payload) { + guard let bestAttemptContent = bestAttemptContent else { return } - // Status 4 means call cancelled/ended - remove any existing notification + // Handle Cancelled Calls 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 + // 1. Setup Basic Content 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 - } + bestAttemptContent.interruptionLevel = .timeSensitive - contentHandler?(bestAttemptContent) + // 2. Fetch Avatar & Activate Intent + fetchCallerAvatarData(from: payload) { [weak self] avatarData in + guard let self = self else { return } + + self.activateCommunicationIntent( + senderName: callerName, + senderUsername: payload.caller?.username ?? "", + avatarData: avatarData, + conversationId: payload.rid ?? "", + isGroup: false, + groupName: nil + ) + + self.contentHandler?(self.finalContent ?? bestAttemptContent) + } } func processPayload(payload: Payload) { - // If is a encrypted message - if payload.messageType == .e2e { - if let rid = payload.rid { - let decryptedMessage: String? - - if let content = payload.content, (content.algorithm == "rc.v1.aes-sha2" || content.algorithm == "rc.v2.aes-sha2") { - decryptedMessage = rocketchat?.decryptContent(rid: rid, content: content) - } else if let msg = payload.msg, !msg.isEmpty { - // Fallback to msg field - decryptedMessage = rocketchat?.decryptContent(rid: rid, content: EncryptedContent(algorithm: "rc.v1.aes-sha2", ciphertext: msg, kid: nil, iv: nil)) - } else { - decryptedMessage = nil + guard let bestAttemptContent = bestAttemptContent else { return } + + // 1. Setup Basic Content (Title/Body) + let senderName = payload.sender?.name ?? payload.senderName ?? "Unknown" + let senderUsername = payload.sender?.username ?? payload.senderName ?? "" + + bestAttemptContent.title = senderName + + if let roomType = payload.type { + if roomType == .group || roomType == .channel { + bestAttemptContent.title = payload.name ?? senderName + // Strip sender prefix if present + if let body = bestAttemptContent.body as? String { + let prefix = "\(senderUsername): " + if body.hasPrefix(prefix) { + bestAttemptContent.body = String(body.dropFirst(prefix.count)) + } else { + // Try with sender name (display name) as fallback + let senderNamePrefix = "\(senderName): " + if body.hasPrefix(senderNamePrefix) { + bestAttemptContent.body = String(body.dropFirst(senderNamePrefix.count)) + } + } } - - if let decryptedMessage = decryptedMessage { - bestAttemptContent?.body = decryptedMessage - if let roomType = payload.type, roomType == .group, let sender = payload.senderName { - bestAttemptContent?.body = "\(sender): \(decryptedMessage)" + } else if roomType == .livechat { + bestAttemptContent.title = payload.sender?.name ?? senderName + } + } + + // Handle Decryption (E2E) + if payload.messageType == .e2e, let rid = payload.rid { + if let decrypted = decryptMessage(payload: payload, rid: rid) { + bestAttemptContent.body = decrypted + } + } + + // 2. Fetch Avatar & Activate Intent + fetchAvatarData(from: payload) { [weak self] avatarData in + guard let self = self else { return } + + let isGroup = (payload.type == .group || payload.type == .channel) + + self.activateCommunicationIntent( + senderName: senderName, + senderUsername: senderUsername, + avatarData: avatarData, + conversationId: payload.rid ?? "", + isGroup: isGroup, + groupName: payload.name + ) + + self.contentHandler?(self.finalContent ?? bestAttemptContent) + } + } + + // MARK: - Shared Intent Logic + + /// Shared method to create INPerson, INSendMessageIntent, and update the notification + private func activateCommunicationIntent(senderName: String, senderUsername: String, avatarData: Data?, conversationId: String, isGroup: Bool, groupName: String?) { + guard let bestAttemptContent = bestAttemptContent else { return } + + // 1. Create Sender + var senderImage: INImage? + if let data = avatarData { + senderImage = INImage(imageData: data) + } + + let sender = INPerson( + personHandle: INPersonHandle(value: senderUsername, type: .unknown), + nameComponents: nil, + displayName: senderName, + image: senderImage, + contactIdentifier: nil, + customIdentifier: nil + ) + + // 2. Handle Group Logic + var recipients: [INPerson]? + var speakableGroupName: INSpeakableString? + + if isGroup { + speakableGroupName = (groupName != nil) ? INSpeakableString(spokenPhrase: groupName!) : nil + // Dummy recipient required for iOS to treat as group conversation + let dummy = INPerson( + personHandle: INPersonHandle(value: "placeholder", type: .unknown), + nameComponents: nil, + displayName: nil, + image: nil, + contactIdentifier: nil, + customIdentifier: "recipient_\(conversationId)" + ) + recipients = [dummy] + } + + // 3. Create Intent + let intent = INSendMessageIntent( + recipients: recipients, + outgoingMessageType: .outgoingMessageText, + content: bestAttemptContent.body, + speakableGroupName: speakableGroupName, + conversationIdentifier: conversationId, + serviceName: nil, + sender: sender, + attachments: nil + ) + + if isGroup { + intent.setImage(senderImage, forParameterNamed: \.speakableGroupName) + } + + // 4. Donate & Update + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + interaction.donate(completion: nil) + + do { + self.finalContent = try bestAttemptContent.updating(from: intent) + } catch { + self.finalContent = bestAttemptContent + } + } + + // MARK: - Helpers + + private func fetchMessageContent(payload: Payload) { + UNUserNotificationCenter.current().getDeliveredNotifications { [weak self] deliveredNotifications in + guard let self = self else { return } + + let identifiersToRemove = deliveredNotifications.filter { + $0.request.content.body == "You have a new message" + }.map { $0.request.identifier } + + if identifiersToRemove.count > 0 { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiersToRemove) + } + + // Request the content from server + if let messageId = payload.messageId { + self.rocketchat?.getPushWithId(messageId) { notification in + if let notification = notification { + // Set body first, processPayload will strip sender prefix for groups/channels + self.bestAttemptContent?.body = notification.text + + // Update ejson with full payload from server for correct navigation + if let payloadData = try? JSONEncoder().encode(notification.payload), + let payloadString = String(data: payloadData, encoding: .utf8) { + self.bestAttemptContent?.userInfo["ejson"] = payloadString + } + + self.processPayload(payload: notification.payload) + } else { + // Server returned no notification, deliver as-is + if let bestAttemptContent = self.bestAttemptContent { + self.contentHandler?(bestAttemptContent) + } } } + } else { + // No messageId available, deliver the notification as-is + if let bestAttemptContent = self.bestAttemptContent { + self.contentHandler?(bestAttemptContent) + } } } + } + + private func decryptMessage(payload: Payload, rid: String) -> String? { + if let content = payload.content, (content.algorithm == "rc.v1.aes-sha2" || content.algorithm == "rc.v2.aes-sha2") { + return rocketchat?.decryptContent(rid: rid, content: content) + } else if let msg = payload.msg, !msg.isEmpty { + return rocketchat?.decryptContent(rid: rid, content: EncryptedContent(algorithm: "rc.v1.aes-sha2", ciphertext: msg, kid: nil, iv: nil)) + } + return nil + } + + // MARK: - Avatar Fetching + + /// Fetches avatar image data from a given avatar path + private func fetchAvatarDataFromPath(avatarPath: String, server: String, credentials: Credentials, completion: @escaping (Data?) -> Void) { + // URL-encode credentials to prevent malformed URLs if they contain special characters + guard let encodedToken = credentials.userToken.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let encodedUserId = credentials.userId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + completion(nil) + return + } + + let fullPath = "\(avatarPath)?format=png&size=100&rc_token=\(encodedToken)&rc_uid=\(encodedUserId)" + guard let avatarURL = URL(string: server + fullPath) else { + completion(nil) + return + } + + // Create URLSessionConfiguration with proper timeouts for notification service extension + // timeoutIntervalForResource ensures total download time is limited (not just inactivity) + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 3 // Inactivity timeout + config.timeoutIntervalForResource = 3 // Total download timeout (critical for notification extensions) + let session = URLSession(configuration: config) + + var request = URLRequest(url: avatarURL) + request.httpMethod = "GET" + request.addValue(Bundle.userAgent, forHTTPHeaderField: "User-Agent") + + let task = session.dataTask(with: request) { data, response, error in + guard error == nil, + let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200, + let data = data else { + completion(nil) + return + } + completion(data) + } + task.resume() + } + + /// Fetches avatar image data for video conference caller + func fetchCallerAvatarData(from payload: Payload, completion: @escaping (Data?) -> Void) { + let server = payload.host.removeTrailingSlash() + guard let credentials = Storage().getCredentials(server: server), + let username = payload.caller?.username, + let encoded = username.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + completion(nil) + return + } + fetchAvatarDataFromPath(avatarPath: "/avatar/\(encoded)", server: server, credentials: credentials, completion: completion) + } + + /// Fetches avatar image data - sender's avatar for DMs, room avatar for groups/channels + func fetchAvatarData(from payload: Payload, completion: @escaping (Data?) -> Void) { + let server = payload.host.removeTrailingSlash() + guard let credentials = Storage().getCredentials(server: server) else { + completion(nil) + return + } - if let bestAttemptContent = bestAttemptContent { - contentHandler?(bestAttemptContent) + let avatarPath: String + if payload.type == .direct { + guard let username = payload.sender?.username, + let encoded = username.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + completion(nil) + return + } + avatarPath = "/avatar/\(encoded)" + } else { + guard let rid = payload.rid else { + completion(nil) + return + } + avatarPath = "/avatar/room/\(rid)" } + + fetchAvatarDataFromPath(avatarPath: avatarPath, server: server, credentials: credentials, completion: completion) } } diff --git a/ios/ReplyNotification.swift b/ios/ReplyNotification.swift index 4c3ea7c2183..f0318ddc8bd 100644 --- a/ios/ReplyNotification.swift +++ b/ios/ReplyNotification.swift @@ -88,26 +88,43 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { let ejsonData = ejsonString.data(using: .utf8), let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData), let rid = payload.rid else { + // Show failure notification to user + let content = UNMutableNotificationContent() + content.body = "Failed to send reply. Invalid notification data." + let request = UNNotificationRequest(identifier: "replyPayloadFailure", content: content, trigger: nil) + UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) completionHandler() return } let message = textResponse.userText let rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) - let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) + + var backgroundTask: UIBackgroundTaskIdentifier = .invalid + backgroundTask = UIApplication.shared.beginBackgroundTask { + // Expiration handler - called if system needs to reclaim resources + if backgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid + } + completionHandler() + } 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) + if backgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = .invalid + } completionHandler() } guard let response = response, response.success else { // Show failure notification let content = UNMutableNotificationContent() - content.body = "Failed to reply message." + content.body = "Failed to send reply." let request = UNNotificationRequest(identifier: "replyFailure", content: content, trigger: nil) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) return diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index cca807529bd..a096771b4c8 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 0745F30D29A18A45DDDF8568 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5DA03EEFD8CEA0E9578CEFA /* Pods_defaults_NotificationService.framework */; }; + 059517F8FF85756835127879 /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */; }; 0C6E2DE448364EA896869ADF /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B37C79D9BD0742CE936B6982 /* libc++.tbd */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 1E01C81C2511208400FEF824 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E01C81B2511208400FEF824 /* URL+Extensions.swift */; }; @@ -359,14 +359,14 @@ 7ACFE7DA2DDE48760090D9BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACFE7D82DDE48760090D9BC /* AppDelegate.swift */; }; 7AE10C0628A59530003593CB /* Inter.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7AE10C0528A59530003593CB /* Inter.ttf */; }; 7AE10C0828A59530003593CB /* Inter.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7AE10C0528A59530003593CB /* Inter.ttf */; }; - 815F9657A87D16E93AD8451E /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CF268DB43E067211CC4AB1D8 /* Pods_defaults_Rocket_Chat.framework */; }; 85160EB6C143E0493FE5F014 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194D9A8897F4A486C2C6F89A /* ExpoModulesProvider.swift */; }; - 8BC28DFD84976599F4DD0E1F /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9D354D4BED64C03F5586CC /* Pods_defaults_RocketChatRN.framework */; }; A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; A48B46D92D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; A48B46DA2D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; + AFE1A0329E79D5FDE4B09ECF /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */; }; BC404914E86821389EEB543D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */; }; DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7E862283664608B3894E34 /* libWatermelonDB.a */; }; + FF53AD18205526A976C47AA5 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -465,6 +465,7 @@ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; 06BB44DD4855498082A744AD /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 13B07F961A680F5B00A75B9A /* Rocket.Chat Experimental.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rocket.Chat Experimental.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RocketChatRN/Images.xcassets; sourceTree = ""; }; @@ -612,9 +613,10 @@ 1EFEB5972493B6640072EDC0 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 1EFEB5992493B6640072EDC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EFEB5A12493B67D0072EDC0 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-Rocket.Chat/ExpoModulesProvider.swift"; sourceTree = ""; }; + 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 45D5C142B655F8EFD006792C /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-RocketChatRN/ExpoModulesProvider.swift"; sourceTree = ""; }; - 4C9D354D4BED64C03F5586CC /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60B2A6A31FC4588700BD58E5 /* RocketChatRN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RocketChatRN.entitlements; path = RocketChatRN/RocketChatRN.entitlements; sourceTree = ""; }; 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 65B9A7192AFC24190088956F /* ringtone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringtone.mp3; sourceTree = ""; }; @@ -622,14 +624,13 @@ 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MMKVKeyManager.mm; sourceTree = ""; }; 66C2701E2EBBCB780062725F /* SecureStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; 66C2701F2EBBCB780062725F /* SecureStorage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; - 7065C6880465E9A8735AA5EF /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; + 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; 7A006F13229C83B600803143 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7A0129D22C6E8B5900F84A97 /* ShareRocketChatRN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareRocketChatRN.swift; sourceTree = ""; }; 7A0D62D1242AB187006D5C06 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 7A14FCEC257FEB3A005BDCD4 /* Experimental.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Experimental.xcassets; sourceTree = ""; }; 7A14FCF3257FEB59005BDCD4 /* Official.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Official.xcassets; sourceTree = ""; }; 7A610CD127ECE38100B8ABDD /* custom.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = custom.ttf; sourceTree = ""; }; - 7A6B8ACA1953C727CACE14EB /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; 7A8B30742BCD9D3F00146A40 /* SSLPinning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSLPinning.h; sourceTree = ""; }; 7A8B30752BCD9D3F00146A40 /* SSLPinning.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SSLPinning.mm; sourceTree = ""; }; 7AAA749C23043B1D00F1ADE9 /* RocketChatRN-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RocketChatRN-Bridging-Header.h"; sourceTree = ""; }; @@ -641,17 +642,16 @@ 7AE10C0528A59530003593CB /* Inter.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Inter.ttf; sourceTree = ""; }; 9B215A42CFB843397273C7EA /* SecureStorage.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; 9B215A44CFB843397273C7EC /* MMKVBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = MMKVBridge.mm; path = Shared/RocketChat/MMKVBridge.mm; sourceTree = ""; }; + A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A48B46D72D3FFBD200945489 /* A11yFlowModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = A11yFlowModule.h; sourceTree = ""; }; A48B46D82D3FFBD200945489 /* A11yFlowModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = A11yFlowModule.m; sourceTree = ""; }; + A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; B179038FDD7AAF285047814B /* SecureStorage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; B37C79D9BD0742CE936B6982 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; - B5DA03EEFD8CEA0E9578CEFA /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BA7E862283664608B3894E34 /* libWatermelonDB.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libWatermelonDB.a; sourceTree = ""; }; - CC5834318D0A8AF03D8124DB /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; - CF268DB43E067211CC4AB1D8 /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E023B58716C64D2BFB8C0681 /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; - E06AA2822D8D24C3AA3C8711 /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; - F35C8301F7A5B8286AC64516 /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; + BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; + D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -672,7 +672,7 @@ 7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */, 24A2AEF2383D44B586D31C01 /* libz.tbd in Frameworks */, DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */, - 8BC28DFD84976599F4DD0E1F /* Pods_defaults_RocketChatRN.framework in Frameworks */, + AFE1A0329E79D5FDE4B09ECF /* Pods_defaults_RocketChatRN.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -694,7 +694,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0745F30D29A18A45DDDF8568 /* Pods_defaults_NotificationService.framework in Frameworks */, + FF53AD18205526A976C47AA5 /* Pods_defaults_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -715,7 +715,7 @@ 7AAB3E3D257E6A6E00707CF6 /* JavaScriptCore.framework in Frameworks */, 7AAB3E3E257E6A6E00707CF6 /* libz.tbd in Frameworks */, 7AAB3E3F257E6A6E00707CF6 /* libWatermelonDB.a in Frameworks */, - 815F9657A87D16E93AD8451E /* Pods_defaults_Rocket_Chat.framework in Frameworks */, + 059517F8FF85756835127879 /* Pods_defaults_Rocket_Chat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1143,12 +1143,12 @@ 7AC2B09613AA7C3FEBAC9F57 /* Pods */ = { isa = PBXGroup; children = ( - 7A6B8ACA1953C727CACE14EB /* Pods-defaults-NotificationService.debug.xcconfig */, - E06AA2822D8D24C3AA3C8711 /* Pods-defaults-NotificationService.release.xcconfig */, - E023B58716C64D2BFB8C0681 /* Pods-defaults-Rocket.Chat.debug.xcconfig */, - 7065C6880465E9A8735AA5EF /* Pods-defaults-Rocket.Chat.release.xcconfig */, - CC5834318D0A8AF03D8124DB /* Pods-defaults-RocketChatRN.debug.xcconfig */, - F35C8301F7A5B8286AC64516 /* Pods-defaults-RocketChatRN.release.xcconfig */, + A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */, + D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */, + 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */, + 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */, + 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */, + D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1247,9 +1247,9 @@ 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */, B37C79D9BD0742CE936B6982 /* libc++.tbd */, 06BB44DD4855498082A744AD /* libz.tbd */, - B5DA03EEFD8CEA0E9578CEFA /* Pods_defaults_NotificationService.framework */, - CF268DB43E067211CC4AB1D8 /* Pods_defaults_Rocket_Chat.framework */, - 4C9D354D4BED64C03F5586CC /* Pods_defaults_RocketChatRN.framework */, + A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */, + BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */, + 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */, ); name = Frameworks; sourceTree = ""; @@ -1269,8 +1269,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RocketChatRN" */; buildPhases = ( - C0B975AF6ED607297F8F55F4 /* [CP] Check Pods Manifest.lock */, - 7AA5C63E23E30D110005C4A7 /* Start Packager */, + D17D219AF77F48D35A0D7171 /* [CP] Check Pods Manifest.lock */, 589729E8381BA997CD19EF19 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, @@ -1282,8 +1281,8 @@ 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */, 407D3EDE3DABEE15D27BD87D /* ShellScript */, 9C104B12BEE385F7555E641F /* [Expo] Configure project */, - 69EE0EAB4655CCB0698B6026 /* [CP] Embed Pods Frameworks */, - 4EF35507D275D88665224EED /* [CP] Copy Pods Resources */, + 4CCE5B7235CA003F286BD050 /* [CP] Embed Pods Frameworks */, + 69520DF942793F987B1AA05B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1351,7 +1350,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1EFEB5A02493B6640072EDC0 /* Build configuration list for PBXNativeTarget "NotificationService" */; buildPhases = ( - EBDF1B5B8303C6FF72717B0B /* [CP] Check Pods Manifest.lock */, + 23A5F6CB83957B93A7EA1C97 /* [CP] Check Pods Manifest.lock */, 86A998705576AFA7CE938617 /* [Expo] Configure project */, 1EFEB5912493B6640072EDC0 /* Sources */, 1EFEB5922493B6640072EDC0 /* Frameworks */, @@ -1371,19 +1370,15 @@ isa = PBXNativeTarget; buildConfigurationList = 7AAB3E4F257E6A6E00707CF6 /* Build configuration list for PBXNativeTarget "Rocket.Chat" */; buildPhases = ( - C32210C70D1F9214A2DE8E19 /* [CP] Check Pods Manifest.lock */, - 7AAB3E13257E6A6E00707CF6 /* Start Packager */, + AEF2010F70C9B50729A6B50C /* [CP] Check Pods Manifest.lock */, 84028E94C77DEBDD5200728D /* [Expo] Configure project */, 7AAB3E14257E6A6E00707CF6 /* Sources */, 7AAB3E32257E6A6E00707CF6 /* Frameworks */, 7AAB3E41257E6A6E00707CF6 /* Resources */, - 7AAB3E46257E6A6E00707CF6 /* Bundle React Native code and images */, 7AAB3E48257E6A6E00707CF6 /* Embed App Extensions */, - 7AAB3E4B257E6A6E00707CF6 /* ShellScript */, 1ED1ECE32B8699DD00F6620C /* Embed Watch Content */, - 7A10288726B1D15200E47EF8 /* Upload source maps to Bugsnag */, - F55B2F4877AB3302D8608673 /* [CP] Embed Pods Frameworks */, - 7B5EE97580C4626E59AEA53C /* [CP] Copy Pods Resources */, + 6E2B7753E1396AE4448B35AE /* [CP] Embed Pods Frameworks */, + 3070587F81C003406057D006 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1574,31 +1569,35 @@ shellPath = /bin/sh; shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; }; - 407D3EDE3DABEE15D27BD87D /* ShellScript */ = { + 23A5F6CB83957B93A7EA1C97 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 4EF35507D275D88665224EED /* [CP] Copy Pods Resources */ = { + 3070587F81C003406057D006 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh", + "${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", @@ -1665,29 +1664,28 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 589729E8381BA997CD19EF19 /* [Expo] Configure project */ = { + 407D3EDE3DABEE15D27BD87D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-RocketChatRN/expo-configure-project.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 69EE0EAB4655CCB0698B6026 /* [CP] Embed Pods Frameworks */ = { + 4CCE5B7235CA003F286BD050 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1705,118 +1703,32 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 7A10288726B1D15200E47EF8 /* Upload source maps to Bugsnag */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", - ); - name = "Upload source maps to Bugsnag"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; - }; - 7AA5C63E23E30D110005C4A7 /* Start Packager */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Start Packager"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; - }; - 7AAB3E13257E6A6E00707CF6 /* Start Packager */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Start Packager"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; - }; - 7AAB3E46257E6A6E00707CF6 /* Bundle React Native code and images */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 12; - files = ( - ); - inputPaths = ( - ); - name = "Bundle React Native code and images"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export EXTRA_PACKAGER_ARGS=\"--sourcemap-output $TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\"\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; - }; - 7AAB3E4B257E6A6E00707CF6 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; - }; - 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */ = { + 589729E8381BA997CD19EF19 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", ); - name = "Upload source maps to Bugsnag"; + name = "[Expo] Configure project"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-RocketChatRN/expo-configure-project.sh\"\n"; }; - 7B5EE97580C4626E59AEA53C /* [CP] Copy Pods Resources */ = { + 69520DF942793F987B1AA05B /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh", + "${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", @@ -1883,7 +1795,45 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6E2B7753E1396AE4448B35AE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$TARGET_BUILD_DIR/$INFOPLIST_PATH", + ); + name = "Upload source maps to Bugsnag"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; showEnvVarsInLog = 0; }; 84028E94C77DEBDD5200728D /* [Expo] Configure project */ = { @@ -1943,6 +1893,28 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-NotificationService/expo-configure-project.sh\"\n"; }; + AEF2010F70C9B50729A6B50C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; B4801301A00C50FA3AD72CF9 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2019,7 +1991,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh\"\n"; showEnvVarsInLog = 0; }; - C0B975AF6ED607297F8F55F4 /* [CP] Check Pods Manifest.lock */ = { + D17D219AF77F48D35A0D7171 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2041,68 +2013,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C32210C70D1F9214A2DE8E19 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - EBDF1B5B8303C6FF72717B0B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - F55B2F4877AB3302D8608673 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -2492,7 +2402,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CC5834318D0A8AF03D8124DB /* Pods-defaults-RocketChatRN.debug.xcconfig */; + baseConfigurationReference = 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2557,7 +2467,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F35C8301F7A5B8286AC64516 /* Pods-defaults-RocketChatRN.release.xcconfig */; + baseConfigurationReference = D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2972,7 +2882,7 @@ }; 1EFEB59D2493B6640072EDC0 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7A6B8ACA1953C727CACE14EB /* Pods-defaults-NotificationService.debug.xcconfig */; + baseConfigurationReference = A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -3024,7 +2934,7 @@ }; 1EFEB59E2493B6640072EDC0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E06AA2822D8D24C3AA3C8711 /* Pods-defaults-NotificationService.release.xcconfig */; + baseConfigurationReference = D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -3075,7 +2985,7 @@ }; 7AAB3E50257E6A6E00707CF6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E023B58716C64D2BFB8C0681 /* Pods-defaults-Rocket.Chat.debug.xcconfig */; + baseConfigurationReference = 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -3140,7 +3050,7 @@ }; 7AAB3E51257E6A6E00707CF6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7065C6880465E9A8735AA5EF /* Pods-defaults-Rocket.Chat.release.xcconfig */; + baseConfigurationReference = 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index dbee5af49dc..b2d1a400cba 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -108,5 +108,9 @@ UIViewControllerBasedStatusBarAppearance + NSUserActivityTypes + + INSendMessageIntent + diff --git a/ios/RocketChatRN/RocketChatRN.entitlements b/ios/RocketChatRN/RocketChatRN.entitlements index 3c6b134acbc..c7f9f603a67 100644 --- a/ios/RocketChatRN/RocketChatRN.entitlements +++ b/ios/RocketChatRN/RocketChatRN.entitlements @@ -12,6 +12,8 @@ applinks:go.rocket.chat + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.ios.chat.rocket diff --git a/ios/Shared/Extensions/Bundle+Extensions.swift b/ios/Shared/Extensions/Bundle+Extensions.swift index 8873e7b2c93..a3cf7297de3 100644 --- a/ios/Shared/Extensions/Bundle+Extensions.swift +++ b/ios/Shared/Extensions/Bundle+Extensions.swift @@ -12,4 +12,13 @@ extension Bundle { return string } + + /// Returns User-Agent string for API requests: "RC Mobile; ios {version}; v{appVersion} ({build})" + static 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/Shared/Models/Payload.swift b/ios/Shared/Models/Payload.swift index 2a91f3a1b38..7e64586569b 100644 --- a/ios/Shared/Models/Payload.swift +++ b/ios/Shared/Models/Payload.swift @@ -11,6 +11,7 @@ import Foundation struct Caller: Codable { let _id: String? let name: String? + let username: String? } struct Payload: Codable { diff --git a/ios/Shared/RocketChat/API/Request.swift b/ios/Shared/RocketChat/API/Request.swift index 97c54bf8574..f28d7e1ee81 100644 --- a/ios/Shared/RocketChat/API/Request.swift +++ b/ios/Shared/RocketChat/API/Request.swift @@ -55,7 +55,7 @@ extension Request { request.httpMethod = method.rawValue request.httpBody = body() request.addValue(contentType, forHTTPHeaderField: "Content-Type") - request.addValue(userAgent, forHTTPHeaderField: "User-Agent") + request.addValue(Bundle.userAgent, forHTTPHeaderField: "User-Agent") if let userId = api.credentials?.userId { request.addValue(userId, forHTTPHeaderField: "x-user-id") @@ -70,14 +70,4 @@ 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))" - } } From b990e603c64835534ff95d1c738d87933f56be26 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:58:30 +0530 Subject: [PATCH 02/17] feat: render timestamp markdown (#6266) --- app/containers/markdown/Markdown.stories.tsx | 12 + .../__snapshots__/Markdown.test.tsx.snap | 531 ++++++++++++++++++ app/containers/markdown/components/Inline.tsx | 3 + .../markdown/components/Timestamp.tsx | 65 +++ package.json | 4 +- 5 files changed, 613 insertions(+), 2 deletions(-) create mode 100644 app/containers/markdown/components/Timestamp.tsx diff --git a/app/containers/markdown/Markdown.stories.tsx b/app/containers/markdown/Markdown.stories.tsx index cb1cb64fcde..16472f10e44 100644 --- a/app/containers/markdown/Markdown.stories.tsx +++ b/app/containers/markdown/Markdown.stories.tsx @@ -156,3 +156,15 @@ export const Lists = () => ( /> ); + +export const Timestamp = () => ( + + + + + + + + + +); diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 043ce490978..0ff288d776a 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -4327,3 +4327,534 @@ exports[`Story Snapshots: Text should match snapshot 1`] = ` `; + +exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` + + + + + + t: + + + 12:00 PM + + + + + + + + + T: + + + 12:00:00 PM + + + + + + + + + d: + + + 01/01/2025 + + + + + + + + + D: + + + Wednesday, Jan 01, 2025 + + + + + + + + + f: + + + Wednesday, Jan 01, 2025 12:00 PM + + + + + + + + + F: + + + Wednesday, Jan 01, 2025 12:00:00 PM + + + + + + + + + R: + + + a year ago + + + + + +`; diff --git a/app/containers/markdown/components/Inline.tsx b/app/containers/markdown/components/Inline.tsx index 4a3d096f1d2..4811bcabdcc 100644 --- a/app/containers/markdown/components/Inline.tsx +++ b/app/containers/markdown/components/Inline.tsx @@ -10,6 +10,7 @@ import Plain from './Plain'; import InlineCode from './InlineCode'; import Image from './Image'; import MarkdownContext from '../contexts/MarkdownContext'; +import Timestamp from './Timestamp'; // import { InlineKaTeX, KaTeX } from './Katex'; interface IParagraphProps { @@ -70,6 +71,8 @@ const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | nul case 'INLINE_KATEX': // return ; return {block.value}; + case 'TIMESTAMP': + return ; default: return null; } diff --git a/app/containers/markdown/components/Timestamp.tsx b/app/containers/markdown/components/Timestamp.tsx new file mode 100644 index 00000000000..31731c322a3 --- /dev/null +++ b/app/containers/markdown/components/Timestamp.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Text } from 'react-native'; + +import dayjs from '../../../lib/dayjs'; +import { useTheme } from '../../../theme'; +import { LISTENER } from '../../Toast'; +import EventEmitter from '../../../lib/methods/helpers/events'; +import sharedStyles from '../../../views/Styles'; + +interface ITimestampProps { + value: { timestamp: string; format: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R' }; +} + +const Timestamp = ({ value }: ITimestampProps): React.ReactElement => { + const { colors } = useTheme(); + + const formatDate = React.useMemo(() => { + const timestamp = parseInt(value.timestamp) * 1000; + + if (value.format === 't') { + return dayjs(timestamp).format('hh:mm A'); + } + + if (value.format === 'T') { + return dayjs(timestamp).format('hh:mm:ss A'); + } + + if (value.format === 'd') { + return dayjs(timestamp).format('MM/DD/YYYY'); + } + + if (value.format === 'D') { + return dayjs(timestamp).format('dddd, MMM DD, YYYY'); + } + + if (value.format === 'f') { + return dayjs(timestamp).format('dddd, MMM DD, YYYY hh:mm A'); + } + + if (value.format === 'F') { + return dayjs(timestamp).format('dddd, MMM DD, YYYY hh:mm:ss A'); + } + + if (value.format === 'R') { + return dayjs(timestamp).fromNow(); + } + + return 'Invalid Date'; + }, [value]); + + const handlePress = React.useCallback(() => { + const message = dayjs(parseInt(value.timestamp) * 1000).format('dddd, MMM DD, YYYY hh:mm A'); + EventEmitter.emit(LISTENER, { message }); + }, [value.timestamp]); + + return ( + + {` ${formatDate} `} + + ); +}; + +export default Timestamp; diff --git a/package.json b/package.json index d1d72997c88..45c7b5e50e1 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "packageManager": "yarn@1.22.22", "scripts": { "start": "react-native start", - "test": "jest", - "test-update": "jest --updateSnapshot", + "test": "TZ=UTC jest", + "test-update": "TZ=UTC jest --updateSnapshot", "lint": "eslint . && tsc", "prettier-lint": "prettier --write . && yarn lint", "ios": "npx react-native run-ios", From 1fe0bf7a24368cc97c42503aa7ea2672e1931f42 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 8 Jan 2026 17:03:07 -0300 Subject: [PATCH 03/17] fix(Android): Push notification on killed apps and deep link (#6888) --- .../chat/rocket/reactnative/MainActivity.kt | 56 +----- .../rocket/reactnative/MainApplication.kt | 13 +- .../notification/CustomPushNotification.java | 139 ++++----------- .../E2ENotificationProcessor.java | 160 ------------------ .../reactnative/notification/Ejson.java | 7 - .../NativePushNotificationSpec.kt | 24 +++ .../notification/NotificationIntentHandler.kt | 154 +++++++++++++++++ .../notification/PushNotificationModule.kt | 78 +++++++++ .../PushNotificationTurboPackage.kt | 38 +++++ .../native/NativePushNotificationAndroid.ts | 9 + app/lib/notifications/index.ts | 48 +++++- app/lib/notifications/push.ts | 60 +++++++ app/sagas/state.js | 5 + 13 files changed, 452 insertions(+), 339 deletions(-) delete mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java create mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt create mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt create mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt create mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt create mode 100644 app/lib/native/NativePushNotificationAndroid.ts 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 502ae26784f..e4ab65ceba1 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt @@ -9,9 +9,7 @@ import android.os.Bundle import com.zoontek.rnbootsplash.RNBootSplash 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 +import chat.rocket.reactnative.notification.NotificationIntentHandler class MainActivity : ReactActivity() { @@ -32,56 +30,16 @@ class MainActivity : ReactActivity() { RNBootSplash.init(this, R.style.BootTheme) super.onCreate(null) - // Handle video conf action from notification - intent?.let { handleVideoConfIntent(it) } + // Handle notification intents + intent?.let { NotificationIntentHandler.handleIntent(this, 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") - } + setIntent(intent) + + // Handle notification intents when activity is already running + NotificationIntentHandler.handleIntent(this, intent) } 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 89622d5d0a4..5ac2e25f483 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt @@ -5,11 +5,8 @@ import android.content.res.Configuration import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactHost -import com.facebook.react.ReactInstanceEventListener import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage -import com.facebook.react.bridge.ReactContext -import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost @@ -21,8 +18,8 @@ import expo.modules.ApplicationLifecycleDispatcher import chat.rocket.reactnative.networking.SSLPinningTurboPackage; import chat.rocket.reactnative.storage.MMKVKeyManager; import chat.rocket.reactnative.storage.SecureStoragePackage; -import chat.rocket.reactnative.notification.CustomPushNotification; import chat.rocket.reactnative.notification.VideoConfTurboPackage +import chat.rocket.reactnative.notification.PushNotificationTurboPackage /** * Main Application class. @@ -45,6 +42,7 @@ open class MainApplication : Application(), ReactApplication { add(SSLPinningTurboPackage()) add(WatermelonDBJSIPackage()) add(VideoConfTurboPackage()) + add(PushNotificationTurboPackage()) add(SecureStoragePackage()) } @@ -71,13 +69,6 @@ open class MainApplication : Application(), ReactApplication { // Load the native entry point for the New Architecture load() - // Register listener to set React context when initialized - reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener { - override fun onReactContextInitialized(context: ReactContext) { - CustomPushNotification.setReactContext(context as ReactApplicationContext) - } - }) - ApplicationLifecycleDispatcher.onApplicationCreate(this) } 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 6865aff023e..d468c7f91f4 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 @@ -18,7 +18,6 @@ import androidx.annotation.Nullable; -import com.facebook.react.bridge.ReactApplicationContext; import com.google.gson.Gson; import java.util.ArrayList; @@ -43,7 +42,6 @@ public class CustomPushNotification { private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig.DEBUG; // Shared state - public static volatile ReactApplicationContext reactApplicationContext; private static final Gson gson = new Gson(); private static final Map> notificationMessages = new ConcurrentHashMap<>(); @@ -67,25 +65,10 @@ public CustomPushNotification(Context context, Bundle bundle) { createNotificationChannel(); } - /** - * Sets the React application context when React Native initializes. - * Called from MainApplication when React context is ready. - */ - public static void setReactContext(ReactApplicationContext context) { - 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; - } - public void onReceived() { String notId = mBundle.getString("notId"); @@ -101,58 +84,12 @@ public void onReceived() { return; } - // Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only) - 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 (!isReactInitialized() && attempts < maxAttempts) { - try { - Thread.sleep(100); // Wait 100ms - attempts++; - - if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)"); - } - } catch (InterruptedException e) { - Log.e(TAG, "Wait interrupted", e); - Thread.currentThread().interrupt(); - return; - } - } - - if (isReactInitialized()) { - Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification"); - try { - handleNotification(); - } catch (Exception e) { - Log.e(TAG, "Failed to process notification after React initialization", e); - } - } else { - Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV"); - try { - handleNotification(); - } catch (Exception e) { - Log.e(TAG, "Failed to process notification without React context", e); - } - } - }).start(); - - return; // Exit early, notification will be processed in the thread - } - - if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "React already initialized, proceeding with notification"); - } - + // Process notification immediately - no need to wait for React Native + // MMKV is initialized at app startup, so all notification types can work without React try { handleNotification(); } catch (Exception e) { - Log.e(TAG, "Failed to process notification on main thread", e); + Log.e(TAG, "Failed to process notification", e); } } @@ -210,7 +147,7 @@ private void processNotification() { // Handle E2E encrypted notifications if (isE2ENotification(loadedEjson)) { handleE2ENotification(mBundle, loadedEjson, notId); - return; // E2E processor will handle showing the notification + return; // handleE2ENotification will decrypt and show the notification } // Handle regular (non-E2E) notifications @@ -225,54 +162,30 @@ private boolean isE2ENotification(Ejson ejson) { } /** - * Handles E2E encrypted notifications by delegating to the async processor. + * Handles E2E encrypted notifications by decrypting immediately using regular Android Context. + * No longer waits for React Native initialization. */ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { - // Check if React context is immediately available - if (reactApplicationContext != null) { - // Fast path: decrypt immediately - String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext); - - if (decrypted != null) { - bundle.putString("message", decrypted); + // Decrypt immediately using regular Android Context (mContext) + // This works without React Native initialization + String decrypted = Encryption.shared.decryptMessage(ejson, mContext); + + if (decrypted != null) { + bundle.putString("message", decrypted); + synchronized(this) { mBundle = bundle; - ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); - showNotification(bundle, ejson, notId); - } else { - Log.w(TAG, "E2E decryption failed for notification"); } - return; - } - - // Slow path: wait for React context asynchronously - Log.i(TAG, "Waiting for React context to decrypt E2E notification"); - - E2ENotificationProcessor processor = new E2ENotificationProcessor( - // Context provider - () -> reactApplicationContext, - - // Callback - new E2ENotificationProcessor.NotificationCallback() { - @Override - public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) { - mBundle = decryptedBundle; - Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class); - showNotification(decryptedBundle, finalEjson, notificationId); - } - - @Override - public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "E2E decryption failed for notification"); - } - - @Override - public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "Timeout waiting for React context for E2E notification"); - } + showNotification(bundle, ejson, notId); + } else { + Log.w(TAG, "E2E decryption failed for notification, showing fallback notification"); + // Show fallback notification so user knows a message arrived + // Use a placeholder message since we can't decrypt + bundle.putString("message", "Encrypted message"); + synchronized(this) { + mBundle = bundle; } - ); - - processor.processAsync(bundle, ejson, notId); + showNotification(bundle, ejson, notId); + } } /** @@ -296,6 +209,12 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { String avatarUri = ejson != null ? ejson.getAvatarUri() : null; bundle.putString("avatarUri", avatarUri); + // Ensure mBundle is updated with all modifications before building notification + // This ensures buildNotification() sees the complete bundle with all fields (including ejson) + synchronized(this) { + mBundle = bundle; + } + // Handle special notification types if (ejson != null && "videoconf".equals(ejson.notificationType)) { handleVideoConfNotification(bundle, ejson); diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java b/android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java deleted file mode 100644 index 532464bf257..00000000000 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java +++ /dev/null @@ -1,160 +0,0 @@ -package chat.rocket.reactnative.notification; - -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import com.facebook.react.bridge.ReactApplicationContext; - -import java.util.Date; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Handles asynchronous processing of End-to-End encrypted push notifications. - * - * When an E2E notification arrives before React Native is initialized, this processor - * waits for the React context to become available, decrypts the message, and then - * triggers the notification display. - * - * Thread-safe and handles timeout scenarios gracefully. - */ -public class E2ENotificationProcessor { - private static final String TAG = "RocketChat.E2E.Async"; - - // Configuration constants - private static final int POLLING_INTERVAL_MS = 100; // Check every 100ms - private static final int MAX_WAIT_TIME_MS = 3000; // Wait up to 3 seconds - private static final int MAX_ATTEMPTS = MAX_WAIT_TIME_MS / POLLING_INTERVAL_MS; - - private final Handler mainHandler; - private final ReactContextProvider contextProvider; - private final NotificationCallback callback; - - /** - * Interface to provide React context. - */ - public interface ReactContextProvider { - ReactApplicationContext getReactContext(); - } - - /** - * Callback interface for notification processing results. - */ - public interface NotificationCallback { - void onDecryptionComplete(Bundle decryptedBundle, Ejson ejson, String notId); - void onDecryptionFailed(Bundle originalBundle, Ejson ejson, String notId); - void onTimeout(Bundle originalBundle, Ejson ejson, String notId); - } - - /** - * Creates a new E2E notification processor. - * - * @param contextProvider Provider for React context - * @param callback Callback for processing results - */ - public E2ENotificationProcessor(ReactContextProvider contextProvider, NotificationCallback callback) { - this.mainHandler = new Handler(Looper.getMainLooper()); - this.contextProvider = contextProvider; - this.callback = callback; - } - - /** - * Processes an E2E encrypted notification asynchronously. - * - * This method returns immediately. The notification will be decrypted and shown - * once React context becomes available, or after a timeout. - * - * @param bundle The notification bundle - * @param ejson The parsed notification data - * @param notId The notification ID - */ - public void processAsync(final Bundle bundle, final Ejson ejson, final String notId) { - final AtomicInteger attempts = new AtomicInteger(0); - - final Runnable pollForContextRunnable = new Runnable() { - @Override - public void run() { - int currentAttempt = attempts.incrementAndGet(); - ReactApplicationContext reactContext = contextProvider.getReactContext(); - - if (reactContext != null) { - // Context is available - decrypt in background thread - Log.i(TAG, "React context available after " + currentAttempt + " attempts"); - decryptAndNotify(reactContext, bundle, ejson, notId); - - } else if (currentAttempt < MAX_ATTEMPTS) { - // Context not ready - poll again - mainHandler.postDelayed(this, POLLING_INTERVAL_MS); - - } else { - // Timeout - give up - Log.w(TAG, "Timeout waiting for React context after " + MAX_WAIT_TIME_MS + "ms"); - handleTimeout(bundle, ejson, notId); - } - } - }; - - // Start polling - mainHandler.post(pollForContextRunnable); - } - - /** - * Decrypts the message in a background thread and invokes the callback on the main thread. - */ - private void decryptAndNotify(final ReactApplicationContext reactContext, - final Bundle bundle, - final Ejson ejson, - final String notId) { - // Decrypt in background thread to avoid blocking - new Thread(() -> { - try { - String decrypted = Encryption.shared.decryptMessage(ejson, reactContext); - - if (decrypted != null) { - bundle.putString("message", decrypted); - - // Call directly on background thread - notification building needs background thread for image loading - try { - callback.onDecryptionComplete(bundle, ejson, notId); - } catch (Exception e) { - Log.e(TAG, "Error in decryption callback", e); - } - - } else { - Log.w(TAG, "Decryption returned null - failed to decrypt"); - handleDecryptionFailure(bundle, ejson, notId); - } - - } catch (Exception e) { - Log.e(TAG, "Exception during decryption", e); - handleDecryptionFailure(bundle, ejson, notId); - } - }, "E2E-Decrypt-" + notId).start(); - } - - /** - * Handles decryption failure by invoking the callback on the current thread. - */ - private void handleDecryptionFailure(final Bundle bundle, final Ejson ejson, final String notId) { - try { - callback.onDecryptionFailed(bundle, ejson, notId); - } catch (Exception e) { - Log.e(TAG, "Error in failure callback", e); - } - } - - /** - * Handles timeout by invoking the callback on the main thread. - */ - private void handleTimeout(final Bundle bundle, final Ejson ejson, final String notId) { - mainHandler.post(() -> { - try { - callback.onTimeout(bundle, ejson, notId); - } catch (Exception e) { - Log.e(TAG, "Error in timeout callback", e); - } - }); - } -} - 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 91e72386f94..c7c35a1d4c6 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 @@ -2,7 +2,6 @@ import android.util.Log; -import com.facebook.react.bridge.Callback; import com.tencent.mmkv.MMKV; import java.math.BigInteger; @@ -12,12 +11,6 @@ import chat.rocket.reactnative.BuildConfig; import chat.rocket.reactnative.storage.MMKVKeyManager; -class RNCallback implements Callback { - public void invoke(Object... args) { - - } -} - class Utils { static public String toHex(String arg) { try { diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt new file mode 100644 index 00000000000..488a51687ab --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt @@ -0,0 +1,24 @@ +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 NativePushNotificationSpec(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext), TurboModule { + + companion object { + const val NAME = "PushNotificationModule" + } + + override fun getName(): String = NAME + + @ReactMethod + abstract fun getPendingNotification(promise: Promise) + + @ReactMethod + abstract fun clearPendingNotification() +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt new file mode 100644 index 00000000000..9cedae3ea2d --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt @@ -0,0 +1,154 @@ +package chat.rocket.reactnative.notification + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import com.google.gson.GsonBuilder + +/** + * Handles notification Intent processing from MainActivity. + * Extracts notification data from Intents and stores it for React Native to process. + */ +class NotificationIntentHandler { + + companion object { + private const val TAG = "RocketChat.NotificationIntentHandler" + + /** + * Handles a notification Intent from MainActivity. + * Processes both video conf and regular notification intents. + */ + @JvmStatic + fun handleIntent(context: Context, intent: Intent) { + // Handle video conf action first + if (handleVideoConfIntent(context, intent)) { + return + } + + // Handle regular notification tap + handleNotificationIntent(context, intent) + } + + /** + * Handles video conference notification Intent. + * @return true if this was a video conf intent, false otherwise + */ + @JvmStatic + private fun handleVideoConfIntent(context: Context, intent: Intent): Boolean { + if (!intent.getBooleanExtra("videoConfAction", false)) { + return false + } + + val notificationId = intent.getIntExtra("notificationId", 0) + val event = intent.getStringExtra("event") ?: return true + + 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") ?: "" + + // Cancel the notification + if (notificationId != 0) { + VideoConfNotification.cancelById(context, 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) + + VideoConfModule.storePendingAction(context, jsonData) + + // Clear the video conf flag to prevent re-processing + intent.removeExtra("videoConfAction") + + return true + } + + /** + * Handles regular notification tap (non-video conf). + * Extracts Intent extras and stores them for React Native to pick up. + */ + @JvmStatic + private fun handleNotificationIntent(context: Context, intent: Intent) { + val extras = intent.extras ?: return + + // Check if this Intent has notification data (ejson) + val ejson = extras.getString("ejson") + if (ejson.isNullOrEmpty()) { + return + } + + try { + // Extract all notification data from Intent extras + // Only include serializable types to avoid JSON serialization errors + val notificationData = mutableMapOf() + + // Copy all extras to the notification data map, filtering out non-serializable types + extras.keySet().forEach { key -> + try { + when (val value = extras.get(key)) { + is String -> notificationData[key] = value + is Int -> notificationData[key] = value + is Boolean -> notificationData[key] = value + is Long -> notificationData[key] = value + is Float -> notificationData[key] = value + is Double -> notificationData[key] = value + is Byte -> notificationData[key] = value + is Char -> notificationData[key] = value + is Short -> notificationData[key] = value + // Skip complex types that can't be serialized (Bundle, Parcelable, etc.) + is Bundle -> { + // Skip Bundle objects - they're not JSON serializable + Log.w(TAG, "Skipping Bundle extra: $key") + } + null -> { + // Skip null values + } + else -> { + // For other types, try to convert to String only if it's a simple type + // Skip complex objects that might not serialize properly + val stringValue = value.toString() + // Only include if it's a reasonable string representation (not object reference) + if (!stringValue.startsWith("android.") && !stringValue.contains("@")) { + notificationData[key] = stringValue + } else { + Log.w(TAG, "Skipping non-serializable extra: $key (type: ${value.javaClass.simpleName})") + } + } + } + } catch (e: Exception) { + Log.w(TAG, "Error processing extra $key: ${e.message}") + } + } + + // Convert to JSON and store for React Native + val gson = GsonBuilder().create() + val jsonData = gson.toJson(notificationData) + + // Store notification data with error handling + try { + PushNotificationModule.storePendingNotification(context, jsonData) + } catch (e: Exception) { + Log.e(TAG, "Failed to store pending notification: ${e.message}", e) + } + } catch (e: Exception) { + Log.e(TAG, "Error handling notification intent: ${e.message}", e) + } + } + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt new file mode 100644 index 00000000000..543558c1a8d --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt @@ -0,0 +1,78 @@ +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 push notification Intent data to JavaScript. + * Used to retrieve pending notification data when the app opens from a notification tap. + */ +class PushNotificationModule(reactContext: ReactApplicationContext) : NativePushNotificationSpec(reactContext) { + + companion object { + private const val PREFS_NAME = "RocketChatPrefs" + private const val KEY_PENDING_NOTIFICATION = "pendingNotification" + + /** + * Stores notification Intent data from a notification tap. + * Called from MainActivity when receiving a notification Intent. + * @throws Exception if storage fails + */ + @JvmStatic + fun storePendingNotification(context: Context, notificationJson: String) { + try { + val success = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_PENDING_NOTIFICATION, notificationJson) + .commit() // Use commit() instead of apply() to ensure synchronous write and catch errors + + if (!success) { + android.util.Log.e("RocketChat.PushNotificationModule", "Failed to store pending notification: commit() returned false") + throw RuntimeException("Failed to store pending notification") + } + } catch (e: Exception) { + android.util.Log.e("RocketChat.PushNotificationModule", "Error storing pending notification: ${e.message}", e) + throw e + } + } + } + + /** + * Gets any pending notification data from a notification tap. + * Returns null if no pending notification. + */ + @ReactMethod + override fun getPendingNotification(promise: Promise) { + try { + val prefs = reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val notification = prefs.getString(KEY_PENDING_NOTIFICATION, null) + + // Clear the notification after reading + notification?.let { + prefs.edit().remove(KEY_PENDING_NOTIFICATION).apply() + } + + promise.resolve(notification) + } catch (e: Exception) { + promise.reject("ERROR", e.message) + } + } + + /** + * Clears any pending notification data. + */ + @ReactMethod + override fun clearPendingNotification() { + try { + reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .remove(KEY_PENDING_NOTIFICATION) + .apply() + } catch (e: Exception) { + // Ignore errors + } + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt new file mode 100644 index 00000000000..2c30630f3b9 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt @@ -0,0 +1,38 @@ +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 push notification module. + */ +class PushNotificationTurboPackage : TurboReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == NativePushNotificationSpec.NAME) { + PushNotificationModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + mapOf( + NativePushNotificationSpec.NAME to ReactModuleInfo( + NativePushNotificationSpec.NAME, + NativePushNotificationSpec.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // hasConstants + false, // isCxxModule + true // isTurboModule + ) + ) + } + } +} + diff --git a/app/lib/native/NativePushNotificationAndroid.ts b/app/lib/native/NativePushNotificationAndroid.ts new file mode 100644 index 00000000000..0c764bebb76 --- /dev/null +++ b/app/lib/native/NativePushNotificationAndroid.ts @@ -0,0 +1,9 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + getPendingNotification(): Promise; + clearPendingNotification(): void; +} + +export default TurboModuleRegistry.get('PushNotificationModule'); diff --git a/app/lib/notifications/index.ts b/app/lib/notifications/index.ts index 5f13b44e6cb..47707cf964c 100644 --- a/app/lib/notifications/index.ts +++ b/app/lib/notifications/index.ts @@ -1,4 +1,5 @@ import EJSON from 'ejson'; +import { Platform } from 'react-native'; import { appInit } from '../../actions/app'; import { deepLinkingClickCallPush, deepLinkingOpen } from '../../actions/deepLinking'; @@ -28,7 +29,7 @@ export const onNotification = (push: INotification): void => { ); return; } catch (e) { - console.warn('Failed to parse video conf notification:', e); + console.warn('[notifications/index.ts] Failed to parse video conf notification:', e); } } } @@ -71,8 +72,10 @@ export const onNotification = (push: INotification): void => { store.dispatch(deepLinkingOpen(params)); return; } catch (e) { - console.warn(e); + console.warn('[notifications/index.ts] Failed to parse ejson:', e); } + } else { + console.warn('[notifications/index.ts] No ejson in payload, dispatching appInit'); } store.dispatch(appInit()); }; @@ -89,3 +92,44 @@ export const initializePushNotifications = async (): Promise => { + if (Platform.OS === 'android') { + try { + const NativePushNotificationModule = require('../native/NativePushNotificationAndroid').default; + if (NativePushNotificationModule) { + const pendingNotification = await NativePushNotificationModule.getPendingNotification(); + if (pendingNotification) { + try { + const notificationData = JSON.parse(pendingNotification); + const notification: INotification = { + payload: { + message: notificationData.message || '', + style: notificationData.style || '', + ejson: notificationData.ejson || '', + collapse_key: notificationData.collapse_key || '', + notId: notificationData.notId || '', + msgcnt: notificationData.msgcnt || '', + title: notificationData.title || '', + from: notificationData.from || '', + image: notificationData.image || '', + soundname: notificationData.soundname || '', + action: notificationData.action + }, + identifier: notificationData.notId || '' + }; + onNotification(notification); + } catch (e) { + console.warn('[notifications/index.ts] Failed to parse pending notification:', e); + } + } + } + } catch (e) { + console.warn('[notifications/index.ts] Error checking pending notification:', e); + } + } +}; diff --git a/app/lib/notifications/push.ts b/app/lib/notifications/push.ts index c5725ec6f4c..beae1c10db6 100644 --- a/app/lib/notifications/push.ts +++ b/app/lib/notifications/push.ts @@ -7,6 +7,7 @@ import { isIOS } from '../methods/helpers'; import { store as reduxStore } from '../store/auxStore'; import { registerPushToken } from '../services/restApi'; import I18n from '../../i18n'; +import NativePushNotificationModule from '../native/NativePushNotificationAndroid'; export let deviceToken = ''; @@ -214,6 +215,65 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi }); // Get initial notification (app was opened by tapping a notification) + // First check native module for stored notification data (Android - when notification was created natively) + if (Platform.OS === 'android' && NativePushNotificationModule) { + return NativePushNotificationModule.getPendingNotification() + .then(pendingNotification => { + if (pendingNotification) { + try { + // Parse the stored notification data + const notificationData = JSON.parse(pendingNotification); + + // Transform to INotification format + const transformed: INotification = { + payload: { + message: notificationData.message || '', + style: notificationData.style || '', + ejson: notificationData.ejson || '', + collapse_key: notificationData.collapse_key || '', + notId: notificationData.notId || '', + msgcnt: notificationData.msgcnt || '', + title: notificationData.title || '', + from: notificationData.from || '', + image: notificationData.image || '', + soundname: notificationData.soundname || '', + action: notificationData.action + }, + identifier: notificationData.notId || '' + }; + + return transformed; + } catch (parseError) { + console.error('[push.ts] Error parsing notification data:', parseError); + return null; + } + } + return null; + }) + .catch(e => { + console.error('[push.ts] Error getting pending notification from native module:', e); + return null; + }) + .then(nativeNotification => { + if (nativeNotification) { + return nativeNotification; + } + + // Fallback to expo-notifications (for iOS or if native module doesn't have data) + const lastResponse = Notifications.getLastNotificationResponse(); + if (lastResponse) { + return transformNotificationResponse(lastResponse); + } + + return null; + }) + .catch(e => { + console.error('[push.ts] Error in promise chain:', e); + return null; + }); + } + + // Fallback to expo-notifications (for iOS or if native module doesn't have data) const lastResponse = Notifications.getLastNotificationResponse(); if (lastResponse) { return Promise.resolve(transformNotificationResponse(lastResponse)); diff --git a/app/sagas/state.js b/app/sagas/state.js index 2797f1da767..25b183b1e67 100644 --- a/app/sagas/state.js +++ b/app/sagas/state.js @@ -7,6 +7,7 @@ import { APP_STATE } from '../actions/actionsTypes'; import { RootEnum } from '../definitions'; import { checkAndReopen } from '../lib/services/connect'; import { setUserPresenceOnline, setUserPresenceAway } from '../lib/services/restApi'; +import { checkPendingNotification } from '../lib/notifications'; const appHasComeBackToForeground = function* appHasComeBackToForeground() { const appRoot = yield select(state => state.app.root); @@ -28,6 +29,10 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() { try { yield localAuthenticate(server.server); checkAndReopen(); + // Check for pending notification when app comes to foreground (Android - notification tap while in background) + checkPendingNotification().catch((e) => { + log('[state.js] Error checking pending notification:', e); + }); return yield setUserPresenceOnline(); } catch (e) { log(e); From c21d91e69091b689aa69f57bfcc34fc51195f941 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 9 Jan 2026 08:33:07 -0300 Subject: [PATCH 04/17] fix: push notification titles and avatars for discussions and threads (#6900) --- .../notification/CustomPushNotification.java | 44 +++++------------- .../reactnative/notification/Ejson.java | 11 ++--- .../notification/VideoConfNotification.kt | 4 +- .../NotificationService.swift | 45 +++++++++---------- 4 files changed, 40 insertions(+), 64 deletions(-) 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 d468c7f91f4..22339abd691 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 @@ -95,7 +95,7 @@ public void onReceived() { private void handleNotification() { Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); - + if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) { Log.d(TAG, "Detected message-id-only notification, will fetch full content from server"); loadNotificationAndProcess(receivedEjson); @@ -202,8 +202,12 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { boolean hasSender = ejson != null && ejson.sender != null; String title = bundle.getString("title"); + String displaySenderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty()) + ? ejson.senderName + : (hasSender ? ejson.sender.username : title); + bundle.putLong("time", new Date().getTime()); - bundle.putString("username", hasSender ? ejson.sender.username : title); + bundle.putString("username", displaySenderName); bundle.putString("senderId", hasSender ? ejson.sender._id : "1"); String avatarUri = ejson != null ? ejson.getAvatarUri() : null; @@ -291,19 +295,6 @@ private Notification.Builder buildNotification(int notificationId) { // Determine the correct title based on notification type String notificationTitle = title; - if (ejson != null && ejson.type != null) { - if ("p".equals(ejson.type) || "c".equals(ejson.type)) { - // For groups/channels, use room name if available, otherwise fall back to title - notificationTitle = (ejson.name != null && !ejson.name.isEmpty()) ? ejson.name : title; - } else if ("d".equals(ejson.type)) { - // For direct messages, use title (sender name from server) - notificationTitle = title; - } else if ("l".equals(ejson.type)) { - // For omnichannel, use sender name if available, otherwise fall back to title - notificationTitle = (ejson.sender != null && ejson.sender.name != null && !ejson.sender.name.isEmpty()) - ? ejson.sender.name : title; - } - } if (ENABLE_VERBOSE_LOGS) { Log.d(TAG, "[buildNotification] notId=" + notId); @@ -465,19 +456,6 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun // Determine the correct conversation title based on notification type Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); String conversationTitle = title; - if (bundleEjson != null && bundleEjson.type != null) { - if ("p".equals(bundleEjson.type) || "c".equals(bundleEjson.type)) { - // For groups/channels, use room name if available, otherwise fall back to title - conversationTitle = (bundleEjson.name != null && !bundleEjson.name.isEmpty()) ? bundleEjson.name : title; - } else if ("d".equals(bundleEjson.type)) { - // For direct messages, use title (sender name from server) - conversationTitle = title; - } else if ("l".equals(bundleEjson.type)) { - // For omnichannel, use sender name if available, otherwise fall back to title - conversationTitle = (bundleEjson.sender != null && bundleEjson.sender.name != null && !bundleEjson.sender.name.isEmpty()) - ? bundleEjson.sender.name : title; - } - } messageStyle.setConversationTitle(conversationTitle); if (bundles != null) { @@ -489,15 +467,17 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun Ejson ejson = safeFromJson(data.getString("ejson", "{}"), Ejson.class); String m = extractMessage(message, ejson); + String displaySenderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty()) + ? ejson.senderName + : (ejson != null && ejson.sender != null ? ejson.sender.username : title); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - String senderName = ejson != null ? ejson.senderName : "Unknown"; - messageStyle.addMessage(m, timestamp, senderName); + messageStyle.addMessage(m, timestamp, displaySenderName); } else { Bitmap avatar = getAvatar(avatarUri); - String senderName = ejson != null ? ejson.senderName : "Unknown"; Person.Builder senderBuilder = new Person.Builder() .setKey(senderId) - .setName(senderName); + .setName(displaySenderName); if (avatar != null) { senderBuilder.setIcon(Icon.createWithBitmap(avatar)); 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 c7c35a1d4c6..e9998ff8231 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 @@ -67,20 +67,18 @@ private String buildAvatarUri(String avatarPath, String errorContext) { String userToken = token(); String uid = userId(); - if (userToken.isEmpty() || uid.isEmpty()) { - Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: missing auth credentials"); - return null; + String finalUri = server + avatarPath + "?format=png&size=100"; + if (!userToken.isEmpty() && !uid.isEmpty()) { + finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid; } - return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid; + return finalUri; } public String getAvatarUri() { String avatarPath; - // For DMs, show sender's avatar; for groups/channels, show room avatar if ("d".equals(type)) { - // Direct message: use sender's avatar if (sender == null || sender.username == null || sender.username.isEmpty()) { Log.w(TAG, "Cannot generate avatar URI: sender or username is null"); return null; @@ -92,7 +90,6 @@ public String getAvatarUri() { return null; } } else { - // Group/Channel/Livechat: use room avatar if (rid == null || rid.isEmpty()) { Log.w(TAG, "Cannot generate avatar URI: rid is null for non-DM"); return null; 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 index a2f7bb5a30b..92846f46ac8 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt @@ -91,11 +91,11 @@ class VideoConfNotification(private val context: Context) { if (ejson.caller != null) { callerId = ejson.caller._id ?: "" - callerName = ejson.caller.name ?: "Unknown" + callerName = ejson.senderName ?: 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" + callerName = ejson.senderName ?: ejson.sender.name ?: "Unknown" } else { callerId = "" callerName = "Unknown" diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 00d8ac9549c..eb3f5b37e73 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -14,22 +14,21 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - guard let bestAttemptContent = bestAttemptContent, - let ejsonString = bestAttemptContent.userInfo["ejson"] as? String, - let ejson = ejsonString.data(using: .utf8), - let payload = try? JSONDecoder().decode(Payload.self, from: ejson) else { - contentHandler(request.content) - return - } - - rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) - - if payload.notificationType == .videoconf { - processVideoConf(payload: payload) - } else if payload.notificationType == .messageIdOnly { - fetchMessageContent(payload: payload) + if let bestAttemptContent = bestAttemptContent, + let ejsonString = bestAttemptContent.userInfo["ejson"] as? String, + let ejson = ejsonString.data(using: .utf8), + let payload = try? JSONDecoder().decode(Payload.self, from: ejson) { + rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) + + if payload.notificationType == .videoconf { + processVideoConf(payload: payload) + } else if payload.notificationType == .messageIdOnly { + fetchMessageContent(payload: payload) + } else { + processPayload(payload: payload) + } } else { - processPayload(payload: payload) + contentHandler(request.content) } } @@ -49,7 +48,7 @@ class NotificationService: UNNotificationServiceExtension { } // 1. Setup Basic Content - let callerName = payload.caller?.name ?? "Unknown" + let callerName = payload.senderName ?? payload.caller?.name ?? "Unknown" bestAttemptContent.title = NSLocalizedString("Video Call", comment: "") bestAttemptContent.body = String(format: NSLocalizedString("Incoming call from %@", comment: ""), callerName) bestAttemptContent.categoryIdentifier = "VIDEOCONF" @@ -77,14 +76,15 @@ class NotificationService: UNNotificationServiceExtension { guard let bestAttemptContent = bestAttemptContent else { return } // 1. Setup Basic Content (Title/Body) - let senderName = payload.sender?.name ?? payload.senderName ?? "Unknown" + let senderName = payload.senderName ?? payload.sender?.name ?? "Unknown" let senderUsername = payload.sender?.username ?? payload.senderName ?? "" - bestAttemptContent.title = senderName + if bestAttemptContent.title.isEmpty { + bestAttemptContent.title = senderName + } if let roomType = payload.type { if roomType == .group || roomType == .channel { - bestAttemptContent.title = payload.name ?? senderName // Strip sender prefix if present if let body = bestAttemptContent.body as? String { let prefix = "\(senderUsername): " @@ -98,8 +98,6 @@ class NotificationService: UNNotificationServiceExtension { } } } - } else if roomType == .livechat { - bestAttemptContent.title = payload.sender?.name ?? senderName } } @@ -122,7 +120,7 @@ class NotificationService: UNNotificationServiceExtension { avatarData: avatarData, conversationId: payload.rid ?? "", isGroup: isGroup, - groupName: payload.name + groupName: bestAttemptContent.title ) self.contentHandler?(self.finalContent ?? bestAttemptContent) @@ -214,7 +212,8 @@ class NotificationService: UNNotificationServiceExtension { if let messageId = payload.messageId { self.rocketchat?.getPushWithId(messageId) { notification in if let notification = notification { - // Set body first, processPayload will strip sender prefix for groups/channels + // Set title and body first, processPayload will strip sender prefix for groups/channels + self.bestAttemptContent?.title = notification.title self.bestAttemptContent?.body = notification.text // Update ejson with full payload from server for correct navigation From ede25690e45f02addba8d2440f73be0ee4a8ee17 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 9 Jan 2026 13:41:06 -0300 Subject: [PATCH 05/17] fix(iOS): official target missing build phases (#6905) --- ios/Podfile.lock | 2 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 251 +++++++++++---------- 2 files changed, 136 insertions(+), 117 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 34cc3e5cd90..99a6053f60c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3157,7 +3157,7 @@ SPEC CHECKSUMS: React-timing: 2d07431f1c1203c5b0aaa6dc7b5f503704519218 React-utils: 67cf7dcfc18aa4c56bec19e11886033bb057d9fa ReactAppDependencyProvider: bf62814e0fde923f73fc64b7e82d76c63c284da9 - ReactCodegen: 8885059f55a205c667f52d6e35877137631116f2 + ReactCodegen: 2f22969ab54e1aace69c9b5d3085e0a3b405a9a6 ReactCommon: 177fca841e97b2c0e288e86097b8be04c6e7ae36 RNBootSplash: 1280eeb18d887de0a45bb4923d4fc56f25c8b99c RNCAsyncStorage: edb872909c88d8541c0bfade3f86cd7784a7c6b3 diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index a096771b4c8..29cc9563241 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -7,7 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 059517F8FF85756835127879 /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */; }; + 020BAAEF21339D1538B55D15 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 537F37501C643FE86F92916E /* Pods_defaults_NotificationService.framework */; }; + 05F9D701BF644C25192B8E79 /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A46AABC73B7E9703E69AF850 /* Pods_defaults_RocketChatRN.framework */; }; 0C6E2DE448364EA896869ADF /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B37C79D9BD0742CE936B6982 /* libc++.tbd */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 1E01C81C2511208400FEF824 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E01C81B2511208400FEF824 /* URL+Extensions.swift */; }; @@ -363,10 +364,9 @@ A2C6E2DD38F8BEE19BFB2E1D /* SecureStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B215A42CFB843397273C7EA /* SecureStorage.m */; }; A48B46D92D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; A48B46DA2D3FFBD200945489 /* A11yFlowModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A48B46D82D3FFBD200945489 /* A11yFlowModule.m */; }; - AFE1A0329E79D5FDE4B09ECF /* Pods_defaults_RocketChatRN.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */; }; + ACCF5C382F186B5D43B2C952 /* Pods_defaults_Rocket_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85094D3182EA9331CC444733 /* Pods_defaults_Rocket_Chat.framework */; }; BC404914E86821389EEB543D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */; }; DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA7E862283664608B3894E34 /* libWatermelonDB.a */; }; - FF53AD18205526A976C47AA5 /* Pods_defaults_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -465,7 +465,6 @@ /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; - 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; 06BB44DD4855498082A744AD /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 13B07F961A680F5B00A75B9A /* Rocket.Chat Experimental.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Rocket.Chat Experimental.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RocketChatRN/Images.xcassets; sourceTree = ""; }; @@ -613,10 +612,11 @@ 1EFEB5972493B6640072EDC0 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 1EFEB5992493B6640072EDC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EFEB5A12493B67D0072EDC0 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; - 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; 391C4F7AA7023CD41EEBD106 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-Rocket.Chat/ExpoModulesProvider.swift"; sourceTree = ""; }; - 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 44A0BDC6158008BF9FF04F0A /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; 45D5C142B655F8EFD006792C /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-defaults-RocketChatRN/ExpoModulesProvider.swift"; sourceTree = ""; }; + 482E711ACFA5E2C4281835BF /* Pods-defaults-RocketChatRN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.debug.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.debug.xcconfig"; sourceTree = ""; }; + 537F37501C643FE86F92916E /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60B2A6A31FC4588700BD58E5 /* RocketChatRN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RocketChatRN.entitlements; path = RocketChatRN/RocketChatRN.entitlements; sourceTree = ""; }; 65AD38362BFBDF4A00271B39 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 65B9A7192AFC24190088956F /* ringtone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringtone.mp3; sourceTree = ""; }; @@ -624,7 +624,7 @@ 66C2701A2EBBCB570062725F /* MMKVKeyManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MMKVKeyManager.mm; sourceTree = ""; }; 66C2701E2EBBCB780062725F /* SecureStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; 66C2701F2EBBCB780062725F /* SecureStorage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; - 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; + 6CE8C7A54627937DB698E839 /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; 7A006F13229C83B600803143 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7A0129D22C6E8B5900F84A97 /* ShareRocketChatRN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareRocketChatRN.swift; sourceTree = ""; }; 7A0D62D1242AB187006D5C06 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; @@ -640,18 +640,18 @@ 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 7ACFE7D82DDE48760090D9BC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AE10C0528A59530003593CB /* Inter.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Inter.ttf; sourceTree = ""; }; + 85094D3182EA9331CC444733 /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 92B416D2ADF070E0E0C782A0 /* Pods-defaults-Rocket.Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.debug.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.debug.xcconfig"; sourceTree = ""; }; 9B215A42CFB843397273C7EA /* SecureStorage.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SecureStorage.m; sourceTree = ""; }; 9B215A44CFB843397273C7EC /* MMKVBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = MMKVBridge.mm; path = Shared/RocketChat/MMKVBridge.mm; sourceTree = ""; }; - A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A46AABC73B7E9703E69AF850 /* Pods_defaults_RocketChatRN.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_RocketChatRN.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A48B46D72D3FFBD200945489 /* A11yFlowModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = A11yFlowModule.h; sourceTree = ""; }; A48B46D82D3FFBD200945489 /* A11yFlowModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = A11yFlowModule.m; sourceTree = ""; }; - A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.debug.xcconfig"; sourceTree = ""; }; B179038FDD7AAF285047814B /* SecureStorage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SecureStorage.h; sourceTree = ""; }; B37C79D9BD0742CE936B6982 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; BA7E862283664608B3894E34 /* libWatermelonDB.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libWatermelonDB.a; sourceTree = ""; }; - BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_defaults_Rocket_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; - D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService.release.xcconfig"; sourceTree = ""; }; + CD1F8BBE6FE382D33AB37935 /* Pods-defaults-Rocket.Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-Rocket.Chat.release.xcconfig"; path = "Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat.release.xcconfig"; sourceTree = ""; }; + F6BFACDCE2AB06F4936B3E03 /* Pods-defaults-RocketChatRN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-defaults-RocketChatRN.release.xcconfig"; path = "Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -672,7 +672,7 @@ 7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */, 24A2AEF2383D44B586D31C01 /* libz.tbd in Frameworks */, DD2BA30A89E64F189C2C24AC /* libWatermelonDB.a in Frameworks */, - AFE1A0329E79D5FDE4B09ECF /* Pods_defaults_RocketChatRN.framework in Frameworks */, + 05F9D701BF644C25192B8E79 /* Pods_defaults_RocketChatRN.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -694,7 +694,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FF53AD18205526A976C47AA5 /* Pods_defaults_NotificationService.framework in Frameworks */, + 020BAAEF21339D1538B55D15 /* Pods_defaults_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -715,7 +715,7 @@ 7AAB3E3D257E6A6E00707CF6 /* JavaScriptCore.framework in Frameworks */, 7AAB3E3E257E6A6E00707CF6 /* libz.tbd in Frameworks */, 7AAB3E3F257E6A6E00707CF6 /* libWatermelonDB.a in Frameworks */, - 059517F8FF85756835127879 /* Pods_defaults_Rocket_Chat.framework in Frameworks */, + ACCF5C382F186B5D43B2C952 /* Pods_defaults_Rocket_Chat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1143,12 +1143,12 @@ 7AC2B09613AA7C3FEBAC9F57 /* Pods */ = { isa = PBXGroup; children = ( - A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */, - D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */, - 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */, - 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */, - 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */, - D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */, + 6CE8C7A54627937DB698E839 /* Pods-defaults-NotificationService.debug.xcconfig */, + 44A0BDC6158008BF9FF04F0A /* Pods-defaults-NotificationService.release.xcconfig */, + 92B416D2ADF070E0E0C782A0 /* Pods-defaults-Rocket.Chat.debug.xcconfig */, + CD1F8BBE6FE382D33AB37935 /* Pods-defaults-Rocket.Chat.release.xcconfig */, + 482E711ACFA5E2C4281835BF /* Pods-defaults-RocketChatRN.debug.xcconfig */, + F6BFACDCE2AB06F4936B3E03 /* Pods-defaults-RocketChatRN.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1247,9 +1247,9 @@ 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */, B37C79D9BD0742CE936B6982 /* libc++.tbd */, 06BB44DD4855498082A744AD /* libz.tbd */, - A10597851D3193C3E990372A /* Pods_defaults_NotificationService.framework */, - BABABB66676C60622674D15E /* Pods_defaults_Rocket_Chat.framework */, - 42C409D947C9DBB25E0204FD /* Pods_defaults_RocketChatRN.framework */, + 537F37501C643FE86F92916E /* Pods_defaults_NotificationService.framework */, + 85094D3182EA9331CC444733 /* Pods_defaults_Rocket_Chat.framework */, + A46AABC73B7E9703E69AF850 /* Pods_defaults_RocketChatRN.framework */, ); name = Frameworks; sourceTree = ""; @@ -1269,8 +1269,8 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RocketChatRN" */; buildPhases = ( - D17D219AF77F48D35A0D7171 /* [CP] Check Pods Manifest.lock */, - 589729E8381BA997CD19EF19 /* [Expo] Configure project */, + 8A4915EBB9B9EA919C35752B /* [CP] Check Pods Manifest.lock */, + 06C10D4F29CD7532492AD29E /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, @@ -1279,10 +1279,8 @@ 1E1EA8082326CCE300E22452 /* ShellScript */, 1ED0389C2B507B4F00C007D4 /* Embed Watch Content */, 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */, - 407D3EDE3DABEE15D27BD87D /* ShellScript */, - 9C104B12BEE385F7555E641F /* [Expo] Configure project */, - 4CCE5B7235CA003F286BD050 /* [CP] Embed Pods Frameworks */, - 69520DF942793F987B1AA05B /* [CP] Copy Pods Resources */, + BCA373A96A91C30B09231381 /* [CP] Embed Pods Frameworks */, + E57DE3ACCEF8E313DFF4D411 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1350,12 +1348,12 @@ isa = PBXNativeTarget; buildConfigurationList = 1EFEB5A02493B6640072EDC0 /* Build configuration list for PBXNativeTarget "NotificationService" */; buildPhases = ( - 23A5F6CB83957B93A7EA1C97 /* [CP] Check Pods Manifest.lock */, + 664B079CA244E1ABB144A1C9 /* [CP] Check Pods Manifest.lock */, 86A998705576AFA7CE938617 /* [Expo] Configure project */, 1EFEB5912493B6640072EDC0 /* Sources */, 1EFEB5922493B6640072EDC0 /* Frameworks */, 1EFEB5932493B6640072EDC0 /* Resources */, - B4801301A00C50FA3AD72CF9 /* [CP] Copy Pods Resources */, + 6BC31F4F1CF6E74A7683D2D4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1370,15 +1368,18 @@ isa = PBXNativeTarget; buildConfigurationList = 7AAB3E4F257E6A6E00707CF6 /* Build configuration list for PBXNativeTarget "Rocket.Chat" */; buildPhases = ( - AEF2010F70C9B50729A6B50C /* [CP] Check Pods Manifest.lock */, - 84028E94C77DEBDD5200728D /* [Expo] Configure project */, + 7E2F5D729E377A4A5D1E96B4 /* [CP] Check Pods Manifest.lock */, + 6319FBDD06EF0030B48AD389 /* [Expo] Configure project */, 7AAB3E14257E6A6E00707CF6 /* Sources */, 7AAB3E32257E6A6E00707CF6 /* Frameworks */, 7AAB3E41257E6A6E00707CF6 /* Resources */, + 7A55BE3B2F11316900D8744D /* Bundle React Native code and images */, 7AAB3E48257E6A6E00707CF6 /* Embed App Extensions */, + 7A55BE3C2F1131C000D8744D /* ShellScript */, 1ED1ECE32B8699DD00F6620C /* Embed Watch Content */, - 6E2B7753E1396AE4448B35AE /* [CP] Embed Pods Frameworks */, - 3070587F81C003406057D006 /* [CP] Copy Pods Resources */, + 7A55BE3D2F11320C00D8744D /* Upload source maps to Bugsnag */, + F1B660D44BA706C4AA9FC60C /* [CP] Embed Pods Frameworks */, + 441DA981C582EB474E37526F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1552,8 +1553,9 @@ shellPath = /bin/sh; shellScript = "export EXTRA_PACKAGER_ARGS=\"--sourcemap-output $TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\"\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; - 1E1EA8082326CCE300E22452 /* ShellScript */ = { + 06C10D4F29CD7532492AD29E /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1561,15 +1563,16 @@ ); inputPaths = ( ); + name = "[Expo] Configure project"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-RocketChatRN/expo-configure-project.sh\"\n"; }; - 23A5F6CB83957B93A7EA1C97 /* [CP] Check Pods Manifest.lock */ = { + 1E1EA8082326CCE300E22452 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1577,21 +1580,16 @@ inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; }; - 3070587F81C003406057D006 /* [CP] Copy Pods Resources */ = { + 441DA981C582EB474E37526F /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1667,68 +1665,54 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 407D3EDE3DABEE15D27BD87D /* ShellScript */ = { + 6319FBDD06EF0030B48AD389 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 4CCE5B7235CA003F286BD050 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( + inputFileListPaths = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); - name = "[CP] Embed Pods Frameworks"; + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-Rocket.Chat/expo-configure-project.sh\"\n"; }; - 589729E8381BA997CD19EF19 /* [Expo] Configure project */ = { + 664B079CA244E1ABB144A1C9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[Expo] Configure project"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-RocketChatRN/expo-configure-project.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 69520DF942793F987B1AA05B /* [CP] Copy Pods Resources */ = { + 6BC31F4F1CF6E74A7683D2D4 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh", + "${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", @@ -1795,28 +1779,45 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 6E2B7753E1396AE4448B35AE /* [CP] Embed Pods Frameworks */ = { + 7A55BE3B2F11316900D8744D /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); - name = "[CP] Embed Pods Frameworks"; + name = "Bundle React Native code and images"; + outputFileListPaths = ( + ); outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "export EXTRA_PACKAGER_ARGS=\"--sourcemap-output $TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\"\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; - 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */ = { + 7A55BE3C2F1131C000D8744D /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n"; + }; + 7A55BE3D2F11320C00D8744D /* Upload source maps to Bugsnag */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1836,45 +1837,49 @@ shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; showEnvVarsInLog = 0; }; - 84028E94C77DEBDD5200728D /* [Expo] Configure project */ = { + 7AAE9EB32891A0D20024F559 /* Upload source maps to Bugsnag */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( + "$TARGET_BUILD_DIR/$INFOPLIST_PATH", ); - name = "[Expo] Configure project"; + name = "Upload source maps to Bugsnag"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-Rocket.Chat/expo-configure-project.sh\"\n"; + shellScript = "SOURCE_MAP=\"$TMPDIR/$(md5 -qs \"$CONFIGURATION_BUILD_DIR\")-main.jsbundle.map\" ../node_modules/@bugsnag/react-native/bugsnag-react-native-xcode.sh\n"; + showEnvVarsInLog = 0; }; - 86A998705576AFA7CE938617 /* [Expo] Configure project */ = { + 7E2F5D729E377A4A5D1E96B4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[Expo] Configure project"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-NotificationService/expo-configure-project.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 9C104B12BEE385F7555E641F /* [Expo] Configure project */ = { + 86A998705576AFA7CE938617 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -1893,7 +1898,7 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-defaults-NotificationService/expo-configure-project.sh\"\n"; }; - AEF2010F70C9B50729A6B50C /* [CP] Check Pods Manifest.lock */ = { + 8A4915EBB9B9EA919C35752B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1908,20 +1913,38 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-Rocket.Chat-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-defaults-RocketChatRN-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B4801301A00C50FA3AD72CF9 /* [CP] Copy Pods Resources */ = { + BCA373A96A91C30B09231381 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh", + "${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E57DE3ACCEF8E313DFF4D411 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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", @@ -1988,29 +2011,25 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-NotificationService/Pods-defaults-NotificationService-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-resources.sh\"\n"; showEnvVarsInLog = 0; }; - D17D219AF77F48D35A0D7171 /* [CP] Check Pods Manifest.lock */ = { + F1B660D44BA706C4AA9FC60C /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-defaults-RocketChatRN-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-Rocket.Chat/Pods-defaults-Rocket.Chat-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -2402,7 +2421,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 25B6129FD3765EC5B5D0F3F3 /* Pods-defaults-RocketChatRN.debug.xcconfig */; + baseConfigurationReference = 482E711ACFA5E2C4281835BF /* Pods-defaults-RocketChatRN.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2467,7 +2486,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D37FD1CB4DF877266A49B72B /* Pods-defaults-RocketChatRN.release.xcconfig */; + baseConfigurationReference = F6BFACDCE2AB06F4936B3E03 /* Pods-defaults-RocketChatRN.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -2882,7 +2901,7 @@ }; 1EFEB59D2493B6640072EDC0 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A775A4A535C3A4DF9E009CB5 /* Pods-defaults-NotificationService.debug.xcconfig */; + baseConfigurationReference = 6CE8C7A54627937DB698E839 /* Pods-defaults-NotificationService.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -2934,7 +2953,7 @@ }; 1EFEB59E2493B6640072EDC0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D97BDAB3F63F70EF45D2FD6B /* Pods-defaults-NotificationService.release.xcconfig */; + baseConfigurationReference = 44A0BDC6158008BF9FF04F0A /* Pods-defaults-NotificationService.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(EMBEDDED_CONTENT_CONTAINS_SWIFT)"; CLANG_ANALYZER_NONNULL = YES; @@ -2985,7 +3004,7 @@ }; 7AAB3E50257E6A6E00707CF6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0547DC0CF78D9F082A8B0BB5 /* Pods-defaults-Rocket.Chat.debug.xcconfig */; + baseConfigurationReference = 92B416D2ADF070E0E0C782A0 /* Pods-defaults-Rocket.Chat.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; @@ -3000,7 +3019,7 @@ DEVELOPMENT_TEAM = S6UPZG7ZR3; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -3050,7 +3069,7 @@ }; 7AAB3E51257E6A6E00707CF6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6B6D45FCB4C3A2DAC625E0C5 /* Pods-defaults-Rocket.Chat.release.xcconfig */; + baseConfigurationReference = CD1F8BBE6FE382D33AB37935 /* Pods-defaults-Rocket.Chat.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APPLICATION_EXTENSION_API_ONLY = NO; From 41823b80bd868595b9fa19444f789fc8f58f8f4e Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:04:13 +0530 Subject: [PATCH 06/17] fix: pass tmid to show typing status in threads (#6894) --- app/actions/room.ts | 10 ++++++++-- .../MessageComposer/components/ComposerInput.tsx | 2 +- app/lib/services/restApi.ts | 4 ++-- app/sagas/room.js | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/actions/room.ts b/app/actions/room.ts index 1ac3b0f4907..a4811411344 100644 --- a/app/actions/room.ts +++ b/app/actions/room.ts @@ -39,9 +39,14 @@ interface IForwardRoom extends Action { rid: string; } +type IUserTypingArgs = { + tmid?: string; +}; + interface IUserTyping extends Action { rid: string; status: boolean; + args?: IUserTypingArgs; } export interface IRoomHistoryRequest extends Action { @@ -109,11 +114,12 @@ export function removedRoom(): Action { }; } -export function userTyping(rid: string, status = true): IUserTyping { +export function userTyping(rid: string, status = true, args?: IUserTypingArgs): IUserTyping { return { type: ROOM.USER_TYPING, rid, - status + status, + args }; } diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 178ca3b58b1..d1835ef4abd 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -361,7 +361,7 @@ export const ComposerInput = memo( const handleTyping = (isTyping: boolean) => { if (sharing || !rid) return; - dispatch(userTyping(rid, isTyping)); + dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); }; return ( diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index f38c096c315..95ad3f86c7e 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -950,14 +950,14 @@ export const addUsersToRoom = (rid: string): Promise => { return sdk.methodCallWrapper('addUsersToRoom', { rid, users }); }; -export const emitTyping = (room: IRoom, typing = true) => { +export const emitTyping = (room: IRoom, typing = true, args: { tmid?: string } = {}) => { const { login, settings, server } = reduxStore.getState(); const { UI_Use_Real_Name } = settings; const { version: serverVersion } = server; const { user } = login; const name = UI_Use_Real_Name ? user.name : user.username; if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '4.0.0')) { - return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : []); + return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : [], args); } return sdk.methodCall('stream-notify-room', `${room}/typing`, name, typing); }; diff --git a/app/sagas/room.js b/app/sagas/room.js index 71bd73c717a..9fb160695f0 100644 --- a/app/sagas/room.js +++ b/app/sagas/room.js @@ -51,10 +51,10 @@ const clearInactiveTyping = function* clearInactiveTyping({ rid }) { yield clearUserTyping({ rid, status: false }); }; -const watchUserTyping = function* watchUserTyping({ rid, status }) { +const watchUserTyping = function* watchUserTyping({ rid, status, args }) { try { if (status) { - yield emitTyping(rid, status); + yield emitTyping(rid, status, args); if (inactiveTypingTask) { yield cancel(inactiveTypingTask); } From ad951f9555e9418e8fc12b11cd5557226cd9e148 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:09:52 +0530 Subject: [PATCH 07/17] fix: timestamp bug fixes (#6910) --- .../markdown/components/Timestamp.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/containers/markdown/components/Timestamp.tsx b/app/containers/markdown/components/Timestamp.tsx index 31731c322a3..da760661b3c 100644 --- a/app/containers/markdown/components/Timestamp.tsx +++ b/app/containers/markdown/components/Timestamp.tsx @@ -14,44 +14,44 @@ interface ITimestampProps { const Timestamp = ({ value }: ITimestampProps): React.ReactElement => { const { colors } = useTheme(); - const formatDate = React.useMemo(() => { - const timestamp = parseInt(value.timestamp) * 1000; + const timestampMs = React.useMemo(() => parseInt(value.timestamp, 10) * 1000, [value.timestamp]); + const formatDate = React.useMemo(() => { if (value.format === 't') { - return dayjs(timestamp).format('hh:mm A'); + return dayjs(timestampMs).format('hh:mm A'); } if (value.format === 'T') { - return dayjs(timestamp).format('hh:mm:ss A'); + return dayjs(timestampMs).format('hh:mm:ss A'); } if (value.format === 'd') { - return dayjs(timestamp).format('MM/DD/YYYY'); + return dayjs(timestampMs).format('MM/DD/YYYY'); } if (value.format === 'D') { - return dayjs(timestamp).format('dddd, MMM DD, YYYY'); + return dayjs(timestampMs).format('dddd, MMM DD, YYYY'); } if (value.format === 'f') { - return dayjs(timestamp).format('dddd, MMM DD, YYYY hh:mm A'); + return dayjs(timestampMs).format('dddd, MMM DD, YYYY hh:mm A'); } if (value.format === 'F') { - return dayjs(timestamp).format('dddd, MMM DD, YYYY hh:mm:ss A'); + return dayjs(timestampMs).format('dddd, MMM DD, YYYY hh:mm:ss A'); } if (value.format === 'R') { - return dayjs(timestamp).fromNow(); + return dayjs(timestampMs).fromNow(); } return 'Invalid Date'; - }, [value]); + }, [timestampMs, value.format]); const handlePress = React.useCallback(() => { - const message = dayjs(parseInt(value.timestamp) * 1000).format('dddd, MMM DD, YYYY hh:mm A'); + const message = dayjs(timestampMs).format('dddd, MMM DD, YYYY hh:mm A'); EventEmitter.emit(LISTENER, { message }); - }, [value.timestamp]); + }, [timestampMs]); return ( Date: Fri, 16 Jan 2026 13:13:41 +0530 Subject: [PATCH 08/17] fix: chat fails to load older messages when scrolling to the top (#6861) --- app/lib/methods/loadMessagesForRoom.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/lib/methods/loadMessagesForRoom.ts b/app/lib/methods/loadMessagesForRoom.ts index 747b8b2aad2..10aa131ec5e 100644 --- a/app/lib/methods/loadMessagesForRoom.ts +++ b/app/lib/methods/loadMessagesForRoom.ts @@ -9,7 +9,6 @@ import updateMessages from './updateMessages'; import { generateLoadMoreId } from './helpers/generateLoadMoreId'; const COUNT = 50; -const COUNT_LIMIT = COUNT * 10; async function load({ rid: roomId, latest, t }: { rid: string; latest?: Date; t: RoomTypes }): Promise { const apiType = roomTypeToApiType(t); @@ -21,11 +20,11 @@ async function load({ rid: roomId, latest, t }: { rid: string; latest?: Date; t: let mainMessagesCount = 0; async function fetchBatch(lastTs?: string): Promise { - if (allMessages.length >= COUNT_LIMIT) { + if (allMessages.length >= COUNT) { return; } - const params = { roomId, count: COUNT, ...(lastTs && { latest: lastTs }) }; + const params = { roomId, showThreadMessages: false, count: COUNT, ...(lastTs && { latest: lastTs }) }; let data; switch (apiType) { @@ -77,7 +76,7 @@ export function loadMessagesForRoom(args: { if (data?.length) { const lastMessage = data[data.length - 1]; const lastMessageRecord = await getMessageById(lastMessage._id as string); - if (!lastMessageRecord && (data.length === COUNT || data.length >= COUNT_LIMIT)) { + if (!lastMessageRecord && data.length === COUNT) { const loadMoreMessage = { _id: generateLoadMoreId(lastMessage._id as string), rid: lastMessage.rid, From c52af0481d8dcbf3071789b023c75df27d2472b7 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:25:35 +0530 Subject: [PATCH 09/17] fix: Keep connecting label on header while logging in (#6920) --- app/views/RoomsListView/components/Header.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/RoomsListView/components/Header.tsx b/app/views/RoomsListView/components/Header.tsx index 6d56a9582f0..3f6ff738f90 100644 --- a/app/views/RoomsListView/components/Header.tsx +++ b/app/views/RoomsListView/components/Header.tsx @@ -36,6 +36,7 @@ const RoomsListHeaderView = ({ search, searchEnabled }: { search: (text: string) const connecting = useAppSelector(state => state.meteor.connecting || state.server.loading); const connected = useAppSelector(state => state.meteor.connected); + const isLoggingIn = useAppSelector(state => state.login.isFetching); const isFetching = useAppSelector(state => state.rooms.isFetching); const serverName = useAppSelector(state => state.settings.Site_Name as string); const server = useAppSelector(state => state.server.server); @@ -55,7 +56,7 @@ const RoomsListHeaderView = ({ search, searchEnabled }: { search: (text: string) let subtitle; if (supportedVersionsStatus === 'expired') { subtitle = 'Cannot connect'; - } else if (connecting) { + } else if (connecting || isLoggingIn) { subtitle = I18n.t('Connecting'); } else if (isFetching) { subtitle = I18n.t('Updating'); From 7b483e6296eb8019fe156d8022d20cb9b27eaafc Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:56:18 +0530 Subject: [PATCH 10/17] chore: changelog in beta release (#6899) --- .github/actions/upload-android/action.yml | 28 ++++++++++++++- .github/actions/upload-ios/action.yml | 8 ++++- .github/workflows/build-develop.yml | 13 ++++--- .github/workflows/build-official-android.yml | 4 +-- .github/workflows/generate-changelog.yml | 38 ++++++++++++++++++++ android/fastlane/Fastfile | 6 +++- ios/fastlane/Fastfile | 28 ++++++++++++--- 7 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/generate-changelog.yml diff --git a/.github/actions/upload-android/action.yml b/.github/actions/upload-android/action.yml index 7f1fb418089..0452a2832b7 100644 --- a/.github/actions/upload-android/action.yml +++ b/.github/actions/upload-android/action.yml @@ -53,6 +53,33 @@ runs: echo "${{ inputs.FASTLANE_GOOGLE_SERVICE_ACCOUNT }}" | base64 --decode > service_account.json shell: bash + - uses: actions/download-artifact@v4 + if: ${{ inputs.trigger == 'develop' }} + with: + name: release-changelog + path: . + + - name: Prepare Play Store changelog metadata + if: ${{ inputs.trigger == 'develop' }} + run: | + mkdir -p android/fastlane/metadata/android/en-US/changelogs + + if [ -f changelog.txt ]; then + char_count=$(wc -m < changelog.txt) + + if [ "$char_count" -gt 500 ]; then + cut -c1-497 changelog.txt > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" + printf "..." >> "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" + else + cat changelog.txt > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" + fi + else + printf "Internal improvements and bug fixes" > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" + fi + shell: bash + env: + BUILD_VERSION: ${{ inputs.BUILD_VERSION }} + - name: Fastlane Play Store Upload working-directory: android run: | @@ -65,7 +92,6 @@ runs: if [[ ${{ inputs.trigger }} == "develop" ]] && [[ ${{ inputs.type }} == 'official' ]]; then bundle exec fastlane android official_open_testing fi - shell: bash - name: Leave a comment on PR diff --git a/.github/actions/upload-ios/action.yml b/.github/actions/upload-ios/action.yml index 68f5d4539d4..4deefc781a2 100644 --- a/.github/actions/upload-ios/action.yml +++ b/.github/actions/upload-ios/action.yml @@ -109,6 +109,12 @@ runs: yarn pod-install shell: bash + - uses: actions/download-artifact@v4 + if: ${{ inputs.type == 'official' && inputs.trigger == 'develop' }} + with: + name: release-changelog + path: . + - name: Fastlane Submit to TestFlight working-directory: ios run: | @@ -157,4 +163,4 @@ runs: message="**iOS Build Available**"$'\n\n'"$app_name $VERSION_NAME.$BUILD_VERSION" gh pr comment "$PR_NUMBER" --body "$message" - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/build-develop.yml b/.github/workflows/build-develop.yml index 48ffc62e564..b8be5d7ef33 100644 --- a/.github/workflows/build-develop.yml +++ b/.github/workflows/build-develop.yml @@ -14,11 +14,16 @@ jobs: if: ${{ github.repository == 'RocketChat/Rocket.Chat.ReactNative' }} uses: ./.github/workflows/eslint.yml + generate-changelog: + name: Generate Release Changelog + needs: [run-eslint-and-test] + uses: ./.github/workflows/generate-changelog.yml + android-build-experimental-store: name: Build Android Experimental if: ${{ github.repository == 'RocketChat/Rocket.Chat.ReactNative' }} uses: ./.github/workflows/build-android.yml - needs: [run-eslint-and-test] + needs: [run-eslint-and-test, generate-changelog] secrets: inherit with: type: experimental @@ -28,7 +33,7 @@ jobs: name: Build Android Official if: ${{ github.repository == 'RocketChat/Rocket.Chat.ReactNative' }} uses: ./.github/workflows/build-official-android.yml - needs: [run-eslint-and-test] + needs: [run-eslint-and-test, generate-changelog] secrets: inherit with: type: official @@ -38,7 +43,7 @@ jobs: name: Build iOS Experimental if: ${{ github.repository == 'RocketChat/Rocket.Chat.ReactNative' }} uses: ./.github/workflows/build-ios.yml - needs: [run-eslint-and-test] + needs: [run-eslint-and-test, generate-changelog] secrets: inherit with: type: experimental @@ -48,7 +53,7 @@ jobs: name: Build iOS Official if: ${{ github.repository == 'RocketChat/Rocket.Chat.ReactNative' }} uses: ./.github/workflows/build-official-ios.yml - needs: [run-eslint-and-test] + needs: [run-eslint-and-test, generate-changelog] secrets: inherit with: type: official diff --git a/.github/workflows/build-official-android.yml b/.github/workflows/build-official-android.yml index 76d195085b0..226c50ff26a 100644 --- a/.github/workflows/build-official-android.yml +++ b/.github/workflows/build-official-android.yml @@ -72,7 +72,7 @@ jobs: upload-android: name: Upload runs-on: ubuntu-latest - needs: [upload-hold] + needs: [build-android, upload-hold] if: ${{ inputs.type == 'official' && (always() && (needs.upload-hold.result == 'success' || needs.upload-hold.result == 'skipped')) }} steps: - name: Checkout Repository @@ -85,7 +85,7 @@ jobs: trigger: ${{ inputs.trigger }} FASTLANE_GOOGLE_SERVICE_ACCOUNT: ${{ secrets.FASTLANE_GOOGLE_SERVICE_ACCOUNT }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_VERSION: ${{ needs.upload-hold.outputs.BUILD_VERSION }} + BUILD_VERSION: ${{ needs.build-android.outputs.BUILD_VERSION }} upload-internal: name: Internal Sharing diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 00000000000..af729951373 --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,38 @@ +name: Generate Release Changelog + +on: + workflow_call: + +jobs: + generate-changelog: + name: Generate changelog + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate changelog + shell: bash + run: | + LATEST_RELEASE_TAG=$(git tag --sort=-creatordate | head -n 1) + + if [ -z "$LATEST_RELEASE_TAG" ]; then + echo "- Improvements and bug fixes" > changelog.txt + exit 0 + fi + + git log "$LATEST_RELEASE_TAG"..HEAD --pretty=format:"- %s" --no-merges > changelog.txt + + if [ ! -s changelog.txt ]; then + echo "- Improvements and bug fixes" > changelog.txt + fi + + - name: Upload changelog artifact + uses: actions/upload-artifact@v4 + with: + name: release-changelog + path: changelog.txt + retention-days: 15 diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index b89435389ce..1448a1173f0 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -78,7 +78,11 @@ platform :android do upload_to_play_store( package_name: 'chat.rocket.android', track: 'beta', - aab: 'app/build/outputs/bundle/officialRelease/app-official-release.aab' + aab: 'app/build/outputs/bundle/officialRelease/app-official-release.aab', + skip_upload_metadata: true, + skip_upload_changelogs: false, + skip_upload_images: true, + skip_upload_screenshots: true ) end end diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 80a43c7f587..6f52b0e2f6b 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -29,17 +29,35 @@ platform :ios do desc "Submit a new Beta Build to Apple TestFlight" lane :beta do |options| + changelog_path = File.expand_path('../../changelog.txt', __dir__) + + changelog = if File.exist?(changelog_path) + content = File.read(changelog_path) + content.length > 4000 ? content[0, 3997] + "..." : content + end + api_key = app_store_connect_api_key( key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"], issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"], key_filepath: 'fastlane/app_store_connect_api_key.p8', in_house: false ) - pilot( - ipa: 'Rocket.Chat.ipa', - app_identifier: options[:official] ? 'chat.rocket.ios' : 'chat.rocket.reactnative', - skip_waiting_for_build_processing: true - ) + + pilot_options = { + ipa: 'Rocket.Chat.ipa', + app_identifier: options[:official] ? 'chat.rocket.ios' : 'chat.rocket.reactnative', + skip_waiting_for_build_processing: !(options[:official] && changelog), + reject_build_waiting_for_review: true, + } + + if options[:official] && changelog + pilot_options[:changelog] = changelog + pilot_options[:distribute_external] = true + pilot_options[:notify_external_testers] = true + pilot_options[:groups] = ["External Testers"] + end + + pilot(pilot_options) upload_symbols_to_crashlytics(dsym_path: "Rocket.Chat.app.dSYM.zip") upload_symbols_to_bugsnag( config_file: "RocketChatRN/Info.plist", From ad2f552c8f6ed4e8d424833a8f9c70b726b61112 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:29:33 +0530 Subject: [PATCH 11/17] chore: added concurrency on develop action (#6921) --- .github/workflows/build-develop.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-develop.yml b/.github/workflows/build-develop.yml index b8be5d7ef33..5e9969daf74 100644 --- a/.github/workflows/build-develop.yml +++ b/.github/workflows/build-develop.yml @@ -8,6 +8,10 @@ on: branches: - 'develop' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: run-eslint-and-test: name: ESLint and Test From c079de04f151b677dee1fb5c2ae992e09c65ec3f Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:27:49 +0530 Subject: [PATCH 12/17] chore(Android): fix CI generated changelog (#6922) --- .github/actions/upload-android/action.yml | 9 +-------- .github/scripts/prepare-changelog.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .github/scripts/prepare-changelog.js diff --git a/.github/actions/upload-android/action.yml b/.github/actions/upload-android/action.yml index 0452a2832b7..aafeccf437f 100644 --- a/.github/actions/upload-android/action.yml +++ b/.github/actions/upload-android/action.yml @@ -65,14 +65,7 @@ runs: mkdir -p android/fastlane/metadata/android/en-US/changelogs if [ -f changelog.txt ]; then - char_count=$(wc -m < changelog.txt) - - if [ "$char_count" -gt 500 ]; then - cut -c1-497 changelog.txt > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" - printf "..." >> "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" - else - cat changelog.txt > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" - fi + node .github/scripts/prepare-changelog.js else printf "Internal improvements and bug fixes" > "android/fastlane/metadata/android/en-US/changelogs/${BUILD_VERSION}.txt" fi diff --git a/.github/scripts/prepare-changelog.js b/.github/scripts/prepare-changelog.js new file mode 100644 index 00000000000..9b31db56352 --- /dev/null +++ b/.github/scripts/prepare-changelog.js @@ -0,0 +1,20 @@ +const fs = require("fs"); + +const buildVersion = process.env.BUILD_VERSION; +const input = fs.readFileSync("changelog.txt", "utf8"); + +const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); +const chars = Array.from(segmenter.segment(input), s => s.segment); + +let output; +if (chars.length > 500) { + output = chars.slice(0, 497).join("") + "..."; +} else { + output = input; +} + +fs.writeFileSync( + `android/fastlane/metadata/android/en-US/changelogs/${buildVersion}.txt`, + output, + "utf8" +); From 12fb458abd592e5ac2fb5c9fa54def7b67c9f9b5 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:11:01 +0530 Subject: [PATCH 13/17] fix (android): unable to send files in E2EE channel using action menu (#6919) --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5f334cbad37..4a19e871830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5024,8 +5024,8 @@ tldts "~5.7.112" "@rocket.chat/mobile-crypto@RocketChat/rocket.chat-mobile-crypto": - version "0.2.0" - resolved "https://codeload.github.com/RocketChat/rocket.chat-mobile-crypto/tar.gz/06bab0cb2e329822911c80d1b937219c15154faa" + version "0.2.1" + resolved "https://codeload.github.com/RocketChat/rocket.chat-mobile-crypto/tar.gz/b75e261282bc0c25a3b8fde3230d1c1e01809e00" "@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile": version "1.3.3-mobile" From 2afa9ee51312b12a60f377a5d878003d55218f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio=20Stasiak?= <91474186+OtavioStasiak@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:29:40 -0300 Subject: [PATCH 14/17] fix(iOS): app crashing on render image (#6908) * fix: formatting base64 images wrong * chore: add base64 avatar storybook case * chore: improve unit tests of getAvatarUrl * chore: format code and fix lint issues [skip ci] * fix: unit test * cleanup --------- Co-authored-by: OtavioStasiak --- app/containers/Avatar/Avatar.stories.tsx | 4 + .../Avatar/__snapshots__/Avatar.test.tsx.snap | 55 ++++++ app/lib/methods/helpers/getAvatarUrl.test.ts | 182 +++++++++++++++++- app/lib/methods/helpers/getAvatarUrl.ts | 3 + 4 files changed, 243 insertions(+), 1 deletion(-) diff --git a/app/containers/Avatar/Avatar.stories.tsx b/app/containers/Avatar/Avatar.stories.tsx index ed07c9d408c..0a2503e3c7d 100644 --- a/app/containers/Avatar/Avatar.stories.tsx +++ b/app/containers/Avatar/Avatar.stories.tsx @@ -12,6 +12,8 @@ const styles = StyleSheet.create({ }); const server = 'https://open.rocket.chat'; +const base64Image = + 'data:image/png;base64,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'; export default { title: 'Avatar' @@ -31,6 +33,8 @@ export const AvatarUrl = () => ( export const AvatarPath = () => ; +export const AvatarBase64 = () => ; + export const WithETag = () => ( ); diff --git a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap b/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap index 97d9de5ea35..0a0d7df4ae6 100644 --- a/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap +++ b/app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap @@ -1,5 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Story Snapshots: AvatarBase64 should match snapshot 1`] = ` + + + +`; + exports[`Story Snapshots: AvatarExternalProviderUrl should match snapshot 1`] = ` ({ PixelRatio: { get: () => 1 } })); +jest.mock('./compareServerVersion', () => ({ + compareServerVersion: jest.fn() +})); + +const mockCompareServerVersion = compareServerVersion as jest.MockedFunction; describe('formatUrl function', () => { test('formats the default URL to get the user avatar', () => { @@ -30,3 +37,176 @@ describe('formatUrl function', () => { expect(result).toEqual(expected); }); }); + +describe('getAvatarURL function', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns the avatar unchanged when it is a base64 data URI', () => { + const avatar = 'data:image/png;base64,ABC123'; + + const expected = avatar; + const result = getAvatarURL({ avatar }); + expect(result).toEqual(expected); + }); + + test('returns the avatar unchanged when it starts with http', () => { + const avatar = 'https://example.com/avatar.png'; + const server = 'https://mobile.qa.rocket.chat'; + + const expected = avatar; + const result = getAvatarURL({ avatar, server }); + expect(result).toEqual(expected); + }); + + test('formats avatar URL with server when avatar does not start with http', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const size = 30; + + const expected = 'https://mobile.qa.rocket.chat/avatar/user123?format=png&size=30'; + const result = getAvatarURL({ avatar, server, size }); + expect(result).toEqual(expected); + }); + + test('uses external provider URL for direct messages', () => { + const type = SubscriptionType.DIRECT; + const text = 'username123'; + const avatarExternalProviderUrl = 'https://external.provider.com/avatar/{username}'; + const size = 30; + + const expected = 'https://external.provider.com/avatar/username123?format=png&size=30'; + const result = getAvatarURL({ type, text, avatarExternalProviderUrl, size }); + expect(result).toEqual(expected); + }); + + test('uses room avatar external provider URL when serverVersion >= 3.8.0', () => { + const rid = 'room123'; + const serverVersion = '3.8.0'; + const roomAvatarExternalProviderUrl = 'https://external.provider.com/room/{roomId}'; + const size = 30; + + mockCompareServerVersion.mockReturnValue(true); + + const expected = 'https://external.provider.com/room/room123?format=png&size=30'; + const result = getAvatarURL({ rid, serverVersion, roomAvatarExternalProviderUrl, size }); + expect(result).toEqual(expected); + expect(mockCompareServerVersion).toHaveBeenCalledWith('3.8.0', 'greaterThanOrEqualTo', '3.8.0'); + }); + + test('uses room/{rid} format when serverVersion >= 3.6.0', () => { + const rid = 'room123'; + const serverVersion = '3.6.0'; + const server = 'https://mobile.qa.rocket.chat'; + const size = 30; + const text = 'roomname'; + + // compareServerVersion returns false for 'lowerThan' when version >= 3.6.0 + // The condition is !compareServerVersion(..., 'lowerThan', '3.6.0') + // So we need to return false to make !false = true + mockCompareServerVersion.mockReturnValue(false); + + const expected = 'https://mobile.qa.rocket.chat/avatar/room/room123?format=png&size=30'; + const result = getAvatarURL({ rid, serverVersion, server, size, text }); + expect(result).toEqual(expected); + expect(mockCompareServerVersion).toHaveBeenCalledWith('3.6.0', 'lowerThan', '3.6.0'); + }); + + test('uses @{text} format when serverVersion < 3.6.0 or no rid', () => { + const text = 'username123'; + const serverVersion = '3.5.0'; + const server = 'https://mobile.qa.rocket.chat'; + const size = 30; + + mockCompareServerVersion.mockReturnValue(false); + + const expected = 'https://mobile.qa.rocket.chat/avatar/@username123?format=png&size=30'; + const result = getAvatarURL({ text, serverVersion, server, size }); + expect(result).toEqual(expected); + }); + + test('adds authentication query parameters when userId, token, and blockUnauthenticatedAccess are provided', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const userId = 'user123'; + const token = 'token123'; + const blockUnauthenticatedAccess = true; + const size = 30; + + const expected = 'https://mobile.qa.rocket.chat/avatar/user123?format=png&size=30&rc_token=token123&rc_uid=user123'; + const result = getAvatarURL({ avatar, server, userId, token, blockUnauthenticatedAccess, size }); + expect(result).toEqual(expected); + }); + + test('adds avatarETag query parameter when provided', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const avatarETag = 'etag123'; + const size = 30; + + const expected = 'https://mobile.qa.rocket.chat/avatar/user123?format=png&size=30&etag=etag123'; + const result = getAvatarURL({ avatar, server, avatarETag, size }); + expect(result).toEqual(expected); + }); + + test('adds both authentication and etag query parameters when all are provided', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const userId = 'user123'; + const token = 'token123'; + const blockUnauthenticatedAccess = true; + const avatarETag = 'etag123'; + const size = 30; + + const expected = + 'https://mobile.qa.rocket.chat/avatar/user123?format=png&size=30&rc_token=token123&rc_uid=user123&etag=etag123'; + const result = getAvatarURL({ avatar, server, userId, token, blockUnauthenticatedAccess, avatarETag, size }); + expect(result).toEqual(expected); + }); + + test('uses cdnPrefix when provided and starts with http', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const cdnPrefix = 'https://cdn.example.com'; + const size = 30; + + const expected = 'https://cdn.example.com/avatar/user123?format=png&size=30'; + const result = getAvatarURL({ avatar, server, cdnPrefix, size }); + expect(result).toEqual(expected); + }); + + test('returns default avatar URL when no avatar is provided', () => { + const text = 'username123'; + const server = 'https://mobile.qa.rocket.chat'; + const size = 30; + + mockCompareServerVersion.mockReturnValue(false); + + const expected = 'https://mobile.qa.rocket.chat/avatar/@username123?format=png&size=30'; + const result = getAvatarURL({ text, server, size }); + expect(result).toEqual(expected); + }); + + test('trims trailing slashes from external provider URLs', () => { + const type = SubscriptionType.DIRECT; + const text = 'username123'; + const avatarExternalProviderUrl = 'https://external.provider.com/avatar/{username}//'; + const size = 30; + + const expected = 'https://external.provider.com/avatar/username123?format=png&size=30'; + const result = getAvatarURL({ type, text, avatarExternalProviderUrl, size }); + expect(result).toEqual(expected); + }); + + test('trims trailing slashes from cdnPrefix', () => { + const avatar = '/avatar/user123'; + const server = 'https://mobile.qa.rocket.chat'; + const cdnPrefix = 'https://cdn.example.com///'; + const size = 30; + + const expected = 'https://cdn.example.com/avatar/user123?format=png&size=30'; + const result = getAvatarURL({ avatar, server, cdnPrefix, size }); + expect(result).toEqual(expected); + }); +}); diff --git a/app/lib/methods/helpers/getAvatarUrl.ts b/app/lib/methods/helpers/getAvatarUrl.ts index b59880472fa..d76f0ccdc65 100644 --- a/app/lib/methods/helpers/getAvatarUrl.ts +++ b/app/lib/methods/helpers/getAvatarUrl.ts @@ -25,6 +25,9 @@ export const getAvatarURL = ({ roomAvatarExternalProviderUrl, cdnPrefix }: IAvatar): string => { + if (!!avatar && avatar?.startsWith('data:')) { + return avatar; + } let room; if (type === SubscriptionType.DIRECT) { room = text; From 88eb8b3d770668c3ad7d0b3cd1dfe7e779da38a1 Mon Sep 17 00:00:00 2001 From: Rohit Bansal <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:15:44 +0530 Subject: [PATCH 15/17] fix: review button visibility issue on voice recording screen (#6941) --- .../MessageComposer/__snapshots__/MessageComposer.test.tsx.snap | 2 +- .../MessageComposer/components/RecordAudio/ReviewButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap b/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap index ca392c9b2f3..1251c3e9af8 100644 --- a/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap +++ b/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap @@ -220,7 +220,7 @@ exports[`MessageComposer Audio tap record 1`] = ` style={ [ { - "color": "#FFFFFF", + "color": "#2F343D", "fontSize": 24, }, [ diff --git a/app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx b/app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx index 1cf9415a3d5..dd9bdfa8672 100644 --- a/app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx +++ b/app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx @@ -22,7 +22,7 @@ export const ReviewButton = ({ onPress }: { onPress: Function }): ReactElement = onPress={() => onPress()} hitSlop={hitSlop}> - + ); From ef136906032c4d90fe1984b7a62eb849ab59fd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio=20Stasiak?= <91474186+OtavioStasiak@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:59:02 -0300 Subject: [PATCH 16/17] fix: Click on Show more on ModalActions breaks the app (#6942) * fix: hide elements instead of conditional render * chore: update snapshot test * fix: remove useMemo * chore: add case on storybook * fix: snapshot test * chore: format code and fix lint issues [skip ci] --------- Co-authored-by: OtavioStasiak --- app/containers/UIKit/Actions.tsx | 36 +- app/containers/UIKit/UiKitModal.stories.tsx | 110 ++ .../__snapshots__/UiKitMessage.test.tsx.snap | 899 +++++++++------- .../__snapshots__/UiKitModal.test.tsx.snap | 962 ++++++++++++++++++ 4 files changed, 1630 insertions(+), 377 deletions(-) diff --git a/app/containers/UIKit/Actions.tsx b/app/containers/UIKit/Actions.tsx index 15f01af1c64..a103ca2bbe5 100644 --- a/app/containers/UIKit/Actions.tsx +++ b/app/containers/UIKit/Actions.tsx @@ -1,22 +1,44 @@ import React, { useState } from 'react'; +import { View, StyleSheet } from 'react-native'; import { BlockContext } from '@rocket.chat/ui-kit'; import Button from '../Button'; import I18n from '../../i18n'; import { type IActions } from './interfaces'; +const styles = StyleSheet.create({ + hidden: { + overflow: 'hidden', + height: 0 + } +}); + export const Actions = ({ blockId, appId, elements, parser }: IActions) => { const [showMoreVisible, setShowMoreVisible] = useState(() => elements && elements.length > 5); - const renderedElements = showMoreVisible ? elements?.slice(0, 5) : elements; + const shouldShowMore = elements && elements.length > 5; + const maxVisible = 5; + + if (!elements || !parser) { + return null; + } + + // Always render all elements to maintain consistent hook calls + // This ensures hooks are always called in the same order + // Use View wrapper to conditionally hide elements instead of conditionally rendering return ( <> - <> - {renderedElements - ? renderedElements?.map(element => parser?.renderActions({ blockId, appId, ...element }, BlockContext.ACTION, parser)) - : null} - - {showMoreVisible &&