2727import java .util .List ;
2828import java .util .Map ;
2929import java .util .concurrent .ConcurrentHashMap ;
30+ import java .util .concurrent .CopyOnWriteArrayList ;
3031
3132import chat .rocket .reactnative .BuildConfig ;
3233import chat .rocket .reactnative .MainActivity ;
@@ -46,6 +47,20 @@ public class CustomPushNotification {
4647 public static volatile ReactApplicationContext reactApplicationContext ;
4748 private static final Gson gson = new Gson ();
4849 private static final Map <String , List <Bundle >> notificationMessages = new ConcurrentHashMap <>();
50+ private static final List <PendingNotification > pendingNotifications = new CopyOnWriteArrayList <>();
51+
52+ /**
53+ * Holds a notification that arrived before React Native was initialized.
54+ */
55+ private static class PendingNotification {
56+ final Context context ;
57+ final Bundle bundle ;
58+
59+ PendingNotification (Context context , Bundle bundle ) {
60+ this .context = context ;
61+ this .bundle = bundle ;
62+ }
63+ }
4964
5065 // Constants
5166 public static final String KEY_REPLY = "KEY_REPLY" ;
@@ -70,21 +85,32 @@ public CustomPushNotification(Context context, Bundle bundle) {
7085 /**
7186 * Sets the React application context when React Native initializes.
7287 * Called from MainApplication when React context is ready.
88+ * Processes any notifications that were queued before React was ready.
7389 */
7490 public static void setReactContext (ReactApplicationContext context ) {
7591 reactApplicationContext = context ;
92+
93+ // Process any notifications that arrived before React was initialized
94+ if (!pendingNotifications .isEmpty ()) {
95+ int count = pendingNotifications .size ();
96+ Log .i (TAG , "React initialized, processing " + count + " queued notification(s)" );
97+
98+ for (PendingNotification pending : pendingNotifications ) {
99+ try {
100+ CustomPushNotification notification = new CustomPushNotification (pending .context , pending .bundle );
101+ notification .handleNotification ();
102+ } catch (Exception e ) {
103+ Log .e (TAG , "Failed to process queued notification" , e );
104+ }
105+ }
106+
107+ pendingNotifications .clear ();
108+ }
76109 }
77110
78111 public static void clearMessages (int notId ) {
79112 notificationMessages .remove (Integer .toString (notId ));
80113 }
81-
82- /**
83- * Check if React Native is initialized
84- */
85- private boolean isReactInitialized () {
86- return reactApplicationContext != null ;
87- }
88114
89115 public void onReceived () {
90116 String notId = mBundle .getString ("notId" );
@@ -101,71 +127,32 @@ public void onReceived() {
101127 return ;
102128 }
103129
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-
130+ // Process notification
131+ // E2E notifications can be processed immediately (no React needed after SQLiteDatabase refactor)
132+ // Other notifications (message-id-only, avatars) still need React for MMKV access
152133 try {
153134 handleNotification ();
154135 } catch (Exception e ) {
155- Log .e (TAG , "Failed to process notification on main thread " , e );
136+ Log .e (TAG , "Failed to process notification" , e );
156137 }
157138 }
158139
159140 private void handleNotification () {
160141 Ejson receivedEjson = safeFromJson (mBundle .getString ("ejson" , "{}" ), Ejson .class );
161142
143+ // Message-id-only notifications need React for MMKV access (tokens for API calls)
162144 if (receivedEjson != null && receivedEjson .notificationType != null && receivedEjson .notificationType .equals ("message-id-only" )) {
163- Log .d (TAG , "Detected message-id-only notification, will fetch full content from server" );
145+ if (reactApplicationContext == null ) {
146+ Log .w (TAG , "React not initialized, queueing message-id-only notification for later..." );
147+ pendingNotifications .add (new PendingNotification (mContext , mBundle ));
148+ return ;
149+ }
164150 loadNotificationAndProcess (receivedEjson );
165151 return ; // Exit early, notification will be processed in callback
166152 }
167153
168- // For non-message-id-only notifications, process immediately
154+ // For other notifications (including E2E), process immediately
155+ // E2E notifications can decrypt without React (using SQLiteDatabase directly)
169156 processNotification ();
170157 }
171158
@@ -174,19 +161,11 @@ private void loadNotificationAndProcess(Ejson ejson) {
174161 @ Override
175162 public void call (@ Nullable Bundle bundle ) {
176163 if (bundle != null ) {
177- Log .d (TAG , "Successfully loaded notification content from server, updating notification props" );
178-
179- if (ENABLE_VERBOSE_LOGS ) {
180- Log .d (TAG , "[BEFORE update] bundle.notificationLoaded=" + bundle .getBoolean ("notificationLoaded" , false ));
181- Log .d (TAG , "[BEFORE update] bundle.title=" + (bundle .getString ("title" ) != null ? "[present]" : "[null]" ));
182- Log .d (TAG , "[BEFORE update] bundle.message length=" + (bundle .getString ("message" ) != null ? bundle .getString ("message" ).length () : 0 ));
183- }
184-
185164 synchronized (CustomPushNotification .this ) {
186165 mBundle = bundle ;
187166 }
188167 } else {
189- Log .w (TAG , "Failed to load notification content from server, will display placeholder notification " );
168+ Log .e (TAG , "Failed to load notification content from server" );
190169 }
191170
192171 processNotification ();
@@ -198,15 +177,6 @@ private void processNotification() {
198177 Ejson loadedEjson = safeFromJson (mBundle .getString ("ejson" , "{}" ), Ejson .class );
199178 String notId = mBundle .getString ("notId" , "1" );
200179
201- if (ENABLE_VERBOSE_LOGS ) {
202- Log .d (TAG , "[processNotification] notId=" + notId );
203- Log .d (TAG , "[processNotification] bundle.notificationLoaded=" + mBundle .getBoolean ("notificationLoaded" , false ));
204- Log .d (TAG , "[processNotification] bundle.title=" + (mBundle .getString ("title" ) != null ? "[present]" : "[null]" ));
205- Log .d (TAG , "[processNotification] bundle.message length=" + (mBundle .getString ("message" ) != null ? mBundle .getString ("message" ).length () : 0 ));
206- Log .d (TAG , "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson .notificationType : "null" ));
207- Log .d (TAG , "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson .sender != null ? loadedEjson .sender .username : "null" ));
208- }
209-
210180 // Handle E2E encrypted notifications
211181 if (isE2ENotification (loadedEjson )) {
212182 handleE2ENotification (mBundle , loadedEjson , notId );
@@ -225,54 +195,21 @@ private boolean isE2ENotification(Ejson ejson) {
225195 }
226196
227197 /**
228- * Handles E2E encrypted notifications by delegating to the async processor.
198+ * Handles E2E encrypted notifications.
199+ * Decrypts immediately using standard SQLiteDatabase (no React Native dependency needed).
229200 */
230201 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 );
238- 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" );
243- }
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- }
272- }
273- );
202+ // Decrypt immediately using static MainApplication.instance (no Context parameter needed)
203+ String decrypted = Encryption .shared .decryptMessage (ejson );
274204
275- processor .processAsync (bundle , ejson , notId );
205+ if (decrypted != null ) {
206+ bundle .putString ("message" , decrypted );
207+ mBundle = bundle ;
208+ ejson = safeFromJson (bundle .getString ("ejson" , "{}" ), Ejson .class );
209+ showNotification (bundle , ejson , notId );
210+ } else {
211+ Log .e (TAG , "E2E decryption failed, notification will not be shown" );
212+ }
276213 }
277214
278215 /**
@@ -302,13 +239,7 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
302239 return ;
303240 } else {
304241 // Show regular notification
305- if (ENABLE_VERBOSE_LOGS ) {
306- Log .d (TAG , "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle .getString ("message" ) != null ? bundle .getString ("message" ).length () : 0 ) + ", bundle.notificationLoaded=" + bundle .getBoolean ("notificationLoaded" , false ));
307- }
308242 notificationMessages .get (notId ).add (bundle );
309- if (ENABLE_VERBOSE_LOGS ) {
310- Log .d (TAG , "[After add] notificationMessages[" + notId + "].size=" + notificationMessages .get (notId ).size ());
311- }
312243 postNotification (Integer .parseInt (notId ));
313244 }
314245 }
@@ -364,7 +295,6 @@ private void createNotificationChannel() {
364295 }
365296
366297 private Notification .Builder buildNotification (int notificationId ) {
367- String notId = Integer .toString (notificationId );
368298 String title = mBundle .getString ("title" );
369299 String message = mBundle .getString ("message" );
370300 Boolean notificationLoaded = mBundle .getBoolean ("notificationLoaded" , false );
@@ -386,14 +316,6 @@ private Notification.Builder buildNotification(int notificationId) {
386316 }
387317 }
388318
389- if (ENABLE_VERBOSE_LOGS ) {
390- Log .d (TAG , "[buildNotification] notId=" + notId );
391- Log .d (TAG , "[buildNotification] notificationLoaded=" + notificationLoaded );
392- Log .d (TAG , "[buildNotification] title=" + (title != null ? "[present]" : "[null]" ));
393- Log .d (TAG , "[buildNotification] notificationTitle=" + (notificationTitle != null ? "[present]" : "[null]" ));
394- Log .d (TAG , "[buildNotification] message length=" + (message != null ? message .length () : 0 ));
395- }
396-
397319 // Create pending intent to open the app
398320 Intent intent = new Intent (mContext , MainActivity .class );
399321 intent .setFlags (Intent .FLAG_ACTIVITY_NEW_TASK | Intent .FLAG_ACTIVITY_CLEAR_TOP );
@@ -516,9 +438,6 @@ private void notificationColor(Notification.Builder notification) {
516438 private void notificationStyle (Notification .Builder notification , int notId , Bundle bundle ) {
517439 List <Bundle > bundles = notificationMessages .get (Integer .toString (notId ));
518440
519- if (ENABLE_VERBOSE_LOGS ) {
520- Log .d (TAG , "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles .size () : "null" ));
521- }
522441
523442 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .N ) {
524443 Notification .InboxStyle messageStyle = new Notification .InboxStyle ();
0 commit comments