Skip to content

Commit f39178a

Browse files
Merge branch 'RocketChat:develop' into develop
2 parents e958242 + ede2569 commit f39178a

File tree

17 files changed

+628
-520
lines changed

17 files changed

+628
-520
lines changed

android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import android.os.Bundle
99
import com.zoontek.rnbootsplash.RNBootSplash
1010
import android.content.Intent
1111
import android.content.res.Configuration
12-
import chat.rocket.reactnative.notification.VideoConfModule
13-
import chat.rocket.reactnative.notification.VideoConfNotification
14-
import com.google.gson.GsonBuilder
12+
import chat.rocket.reactnative.notification.NotificationIntentHandler
1513

1614
class MainActivity : ReactActivity() {
1715

@@ -32,56 +30,16 @@ class MainActivity : ReactActivity() {
3230
RNBootSplash.init(this, R.style.BootTheme)
3331
super.onCreate(null)
3432

35-
// Handle video conf action from notification
36-
intent?.let { handleVideoConfIntent(it) }
33+
// Handle notification intents
34+
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
3735
}
3836

3937
public override fun onNewIntent(intent: Intent) {
4038
super.onNewIntent(intent)
41-
// Handle video conf action when activity is already running
42-
handleVideoConfIntent(intent)
43-
}
44-
45-
private fun handleVideoConfIntent(intent: Intent) {
46-
if (intent.getBooleanExtra("videoConfAction", false)) {
47-
val notificationId = intent.getIntExtra("notificationId", 0)
48-
val event = intent.getStringExtra("event") ?: return
49-
val rid = intent.getStringExtra("rid") ?: ""
50-
val callerId = intent.getStringExtra("callerId") ?: ""
51-
val callerName = intent.getStringExtra("callerName") ?: ""
52-
val host = intent.getStringExtra("host") ?: ""
53-
val callId = intent.getStringExtra("callId") ?: ""
54-
55-
android.util.Log.d("RocketChat.MainActivity", "Handling video conf intent - event: $event, rid: $rid, host: $host, callId: $callId")
56-
57-
// Cancel the notification
58-
if (notificationId != 0) {
59-
VideoConfNotification.cancelById(this, notificationId)
60-
}
61-
62-
// Store action for JS to pick up - include all required fields
63-
val data = mapOf(
64-
"notificationType" to "videoconf",
65-
"rid" to rid,
66-
"event" to event,
67-
"host" to host,
68-
"callId" to callId,
69-
"caller" to mapOf(
70-
"_id" to callerId,
71-
"name" to callerName
72-
)
73-
)
74-
75-
val gson = GsonBuilder().create()
76-
val jsonData = gson.toJson(data)
77-
78-
android.util.Log.d("RocketChat.MainActivity", "Storing video conf action: $jsonData")
79-
80-
VideoConfModule.storePendingAction(this, jsonData)
81-
82-
// Clear the video conf flag to prevent re-processing
83-
intent.removeExtra("videoConfAction")
84-
}
39+
setIntent(intent)
40+
41+
// Handle notification intents when activity is already running
42+
NotificationIntentHandler.handleIntent(this, intent)
8543
}
8644

8745
override fun invokeDefaultOnBackPressed() {

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import android.content.res.Configuration
55
import com.facebook.react.PackageList
66
import com.facebook.react.ReactApplication
77
import com.facebook.react.ReactHost
8-
import com.facebook.react.ReactInstanceEventListener
98
import com.facebook.react.ReactNativeHost
109
import com.facebook.react.ReactPackage
11-
import com.facebook.react.bridge.ReactContext
12-
import com.facebook.react.bridge.ReactApplicationContext
1310
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
1411
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
1512
import com.facebook.react.defaults.DefaultReactNativeHost
@@ -21,8 +18,8 @@ import expo.modules.ApplicationLifecycleDispatcher
2118
import chat.rocket.reactnative.networking.SSLPinningTurboPackage;
2219
import chat.rocket.reactnative.storage.MMKVKeyManager;
2320
import chat.rocket.reactnative.storage.SecureStoragePackage;
24-
import chat.rocket.reactnative.notification.CustomPushNotification;
2521
import chat.rocket.reactnative.notification.VideoConfTurboPackage
22+
import chat.rocket.reactnative.notification.PushNotificationTurboPackage
2623

2724
/**
2825
* Main Application class.
@@ -45,6 +42,7 @@ open class MainApplication : Application(), ReactApplication {
4542
add(SSLPinningTurboPackage())
4643
add(WatermelonDBJSIPackage())
4744
add(VideoConfTurboPackage())
45+
add(PushNotificationTurboPackage())
4846
add(SecureStoragePackage())
4947
}
5048

@@ -71,13 +69,6 @@ open class MainApplication : Application(), ReactApplication {
7169
// Load the native entry point for the New Architecture
7270
load()
7371

74-
// Register listener to set React context when initialized
75-
reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener {
76-
override fun onReactContextInitialized(context: ReactContext) {
77-
CustomPushNotification.setReactContext(context as ReactApplicationContext)
78-
}
79-
})
80-
8172
ApplicationLifecycleDispatcher.onApplicationCreate(this)
8273
}
8374

android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Lines changed: 41 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import androidx.annotation.Nullable;
2020

21-
import com.facebook.react.bridge.ReactApplicationContext;
2221
import com.google.gson.Gson;
2322

2423
import java.util.ArrayList;
@@ -43,7 +42,6 @@ public class CustomPushNotification {
4342
private static final boolean ENABLE_VERBOSE_LOGS = BuildConfig.DEBUG;
4443

4544
// Shared state
46-
public static volatile ReactApplicationContext reactApplicationContext;
4745
private static final Gson gson = new Gson();
4846
private static final Map<String, List<Bundle>> notificationMessages = new ConcurrentHashMap<>();
4947

@@ -67,25 +65,10 @@ public CustomPushNotification(Context context, Bundle bundle) {
6765
createNotificationChannel();
6866
}
6967

70-
/**
71-
* Sets the React application context when React Native initializes.
72-
* Called from MainApplication when React context is ready.
73-
*/
74-
public static void setReactContext(ReactApplicationContext context) {
75-
reactApplicationContext = context;
76-
}
77-
7868
public static void clearMessages(int notId) {
7969
notificationMessages.remove(Integer.toString(notId));
8070
}
8171

82-
/**
83-
* Check if React Native is initialized
84-
*/
85-
private boolean isReactInitialized() {
86-
return reactApplicationContext != null;
87-
}
88-
8972
public void onReceived() {
9073
String notId = mBundle.getString("notId");
9174

@@ -101,64 +84,18 @@ public void onReceived() {
10184
return;
10285
}
10386

104-
// Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only)
105-
if (!isReactInitialized()) {
106-
Log.w(TAG, "React not initialized yet, waiting before processing notification...");
107-
108-
// Wait for React to initialize with timeout
109-
new Thread(() -> {
110-
int attempts = 0;
111-
int maxAttempts = 50; // 5 seconds total (50 * 100ms)
112-
113-
while (!isReactInitialized() && attempts < maxAttempts) {
114-
try {
115-
Thread.sleep(100); // Wait 100ms
116-
attempts++;
117-
118-
if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) {
119-
Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)");
120-
}
121-
} catch (InterruptedException e) {
122-
Log.e(TAG, "Wait interrupted", e);
123-
Thread.currentThread().interrupt();
124-
return;
125-
}
126-
}
127-
128-
if (isReactInitialized()) {
129-
Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification");
130-
try {
131-
handleNotification();
132-
} catch (Exception e) {
133-
Log.e(TAG, "Failed to process notification after React initialization", e);
134-
}
135-
} else {
136-
Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV");
137-
try {
138-
handleNotification();
139-
} catch (Exception e) {
140-
Log.e(TAG, "Failed to process notification without React context", e);
141-
}
142-
}
143-
}).start();
144-
145-
return; // Exit early, notification will be processed in the thread
146-
}
147-
148-
if (ENABLE_VERBOSE_LOGS) {
149-
Log.d(TAG, "React already initialized, proceeding with notification");
150-
}
151-
87+
// Process notification immediately - no need to wait for React Native
88+
// MMKV is initialized at app startup, so all notification types can work without React
15289
try {
15390
handleNotification();
15491
} catch (Exception e) {
155-
Log.e(TAG, "Failed to process notification on main thread", e);
92+
Log.e(TAG, "Failed to process notification", e);
15693
}
15794
}
15895

15996
private void handleNotification() {
16097
Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
161-
98+
16299
if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) {
163100
Log.d(TAG, "Detected message-id-only notification, will fetch full content from server");
164101
loadNotificationAndProcess(receivedEjson);
@@ -210,7 +147,7 @@ private void processNotification() {
210147
// Handle E2E encrypted notifications
211148
if (isE2ENotification(loadedEjson)) {
212149
handleE2ENotification(mBundle, loadedEjson, notId);
213-
return; // E2E processor will handle showing the notification
150+
return; // handleE2ENotification will decrypt and show the notification
214151
}
215152

216153
// Handle regular (non-E2E) notifications
@@ -225,54 +162,30 @@ private boolean isE2ENotification(Ejson ejson) {
225162
}
226163

227164
/**
228-
* Handles E2E encrypted notifications by delegating to the async processor.
165+
* Handles E2E encrypted notifications by decrypting immediately using regular Android Context.
166+
* No longer waits for React Native initialization.
229167
*/
230168
private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) {
231-
// Check if React context is immediately available
232-
if (reactApplicationContext != null) {
233-
// Fast path: decrypt immediately
234-
String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext);
235-
236-
if (decrypted != null) {
237-
bundle.putString("message", decrypted);
169+
// Decrypt immediately using regular Android Context (mContext)
170+
// This works without React Native initialization
171+
String decrypted = Encryption.shared.decryptMessage(ejson, mContext);
172+
173+
if (decrypted != null) {
174+
bundle.putString("message", decrypted);
175+
synchronized(this) {
238176
mBundle = bundle;
239-
ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
240-
showNotification(bundle, ejson, notId);
241-
} else {
242-
Log.w(TAG, "E2E decryption failed for notification");
243177
}
244-
return;
245-
}
246-
247-
// Slow path: wait for React context asynchronously
248-
Log.i(TAG, "Waiting for React context to decrypt E2E notification");
249-
250-
E2ENotificationProcessor processor = new E2ENotificationProcessor(
251-
// Context provider
252-
() -> reactApplicationContext,
253-
254-
// Callback
255-
new E2ENotificationProcessor.NotificationCallback() {
256-
@Override
257-
public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) {
258-
mBundle = decryptedBundle;
259-
Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class);
260-
showNotification(decryptedBundle, finalEjson, notificationId);
261-
}
262-
263-
@Override
264-
public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) {
265-
Log.w(TAG, "E2E decryption failed for notification");
266-
}
267-
268-
@Override
269-
public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) {
270-
Log.w(TAG, "Timeout waiting for React context for E2E notification");
271-
}
178+
showNotification(bundle, ejson, notId);
179+
} else {
180+
Log.w(TAG, "E2E decryption failed for notification, showing fallback notification");
181+
// Show fallback notification so user knows a message arrived
182+
// Use a placeholder message since we can't decrypt
183+
bundle.putString("message", "Encrypted message");
184+
synchronized(this) {
185+
mBundle = bundle;
272186
}
273-
);
274-
275-
processor.processAsync(bundle, ejson, notId);
187+
showNotification(bundle, ejson, notId);
188+
}
276189
}
277190

278191
/**
@@ -289,13 +202,23 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
289202
boolean hasSender = ejson != null && ejson.sender != null;
290203
String title = bundle.getString("title");
291204

205+
String displaySenderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty())
206+
? ejson.senderName
207+
: (hasSender ? ejson.sender.username : title);
208+
292209
bundle.putLong("time", new Date().getTime());
293-
bundle.putString("username", hasSender ? ejson.sender.username : title);
210+
bundle.putString("username", displaySenderName);
294211
bundle.putString("senderId", hasSender ? ejson.sender._id : "1");
295212

296213
String avatarUri = ejson != null ? ejson.getAvatarUri() : null;
297214
bundle.putString("avatarUri", avatarUri);
298215

216+
// Ensure mBundle is updated with all modifications before building notification
217+
// This ensures buildNotification() sees the complete bundle with all fields (including ejson)
218+
synchronized(this) {
219+
mBundle = bundle;
220+
}
221+
299222
// Handle special notification types
300223
if (ejson != null && "videoconf".equals(ejson.notificationType)) {
301224
handleVideoConfNotification(bundle, ejson);
@@ -372,19 +295,6 @@ private Notification.Builder buildNotification(int notificationId) {
372295

373296
// Determine the correct title based on notification type
374297
String notificationTitle = title;
375-
if (ejson != null && ejson.type != null) {
376-
if ("p".equals(ejson.type) || "c".equals(ejson.type)) {
377-
// For groups/channels, use room name if available, otherwise fall back to title
378-
notificationTitle = (ejson.name != null && !ejson.name.isEmpty()) ? ejson.name : title;
379-
} else if ("d".equals(ejson.type)) {
380-
// For direct messages, use title (sender name from server)
381-
notificationTitle = title;
382-
} else if ("l".equals(ejson.type)) {
383-
// For omnichannel, use sender name if available, otherwise fall back to title
384-
notificationTitle = (ejson.sender != null && ejson.sender.name != null && !ejson.sender.name.isEmpty())
385-
? ejson.sender.name : title;
386-
}
387-
}
388298

389299
if (ENABLE_VERBOSE_LOGS) {
390300
Log.d(TAG, "[buildNotification] notId=" + notId);
@@ -546,19 +456,6 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
546456
// Determine the correct conversation title based on notification type
547457
Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
548458
String conversationTitle = title;
549-
if (bundleEjson != null && bundleEjson.type != null) {
550-
if ("p".equals(bundleEjson.type) || "c".equals(bundleEjson.type)) {
551-
// For groups/channels, use room name if available, otherwise fall back to title
552-
conversationTitle = (bundleEjson.name != null && !bundleEjson.name.isEmpty()) ? bundleEjson.name : title;
553-
} else if ("d".equals(bundleEjson.type)) {
554-
// For direct messages, use title (sender name from server)
555-
conversationTitle = title;
556-
} else if ("l".equals(bundleEjson.type)) {
557-
// For omnichannel, use sender name if available, otherwise fall back to title
558-
conversationTitle = (bundleEjson.sender != null && bundleEjson.sender.name != null && !bundleEjson.sender.name.isEmpty())
559-
? bundleEjson.sender.name : title;
560-
}
561-
}
562459
messageStyle.setConversationTitle(conversationTitle);
563460

564461
if (bundles != null) {
@@ -570,15 +467,17 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
570467
Ejson ejson = safeFromJson(data.getString("ejson", "{}"), Ejson.class);
571468
String m = extractMessage(message, ejson);
572469

470+
String displaySenderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty())
471+
? ejson.senderName
472+
: (ejson != null && ejson.sender != null ? ejson.sender.username : title);
473+
573474
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
574-
String senderName = ejson != null ? ejson.senderName : "Unknown";
575-
messageStyle.addMessage(m, timestamp, senderName);
475+
messageStyle.addMessage(m, timestamp, displaySenderName);
576476
} else {
577477
Bitmap avatar = getAvatar(avatarUri);
578-
String senderName = ejson != null ? ejson.senderName : "Unknown";
579478
Person.Builder senderBuilder = new Person.Builder()
580479
.setKey(senderId)
581-
.setName(senderName);
480+
.setName(displaySenderName);
582481

583482
if (avatar != null) {
584483
senderBuilder.setIcon(Icon.createWithBitmap(avatar));

0 commit comments

Comments
 (0)