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))" - } }