capacitor-messenger-notifications 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/MessengerNotifications.podspec +18 -0
  3. package/Package.swift +26 -0
  4. package/README.md +152 -0
  5. package/android/build.gradle +62 -0
  6. package/android/src/main/AndroidManifest.xml +19 -0
  7. package/android/src/main/java/com/codecraft_studio/messenger/notifications/EncryptedMessageNotifier.java +68 -0
  8. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchManager.java +37 -0
  9. package/android/src/main/java/com/codecraft_studio/messenger/notifications/MessengerNotificationsPlugin.java +92 -0
  10. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NativeCrypto.java +43 -0
  11. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationDismissReceiver.java +20 -0
  12. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationHelper.java +618 -0
  13. package/android/src/main/java/com/codecraft_studio/messenger/notifications/PersistentSocketService.java +213 -0
  14. package/dist/esm/definitions.d.ts +48 -0
  15. package/dist/esm/definitions.js +2 -0
  16. package/dist/esm/definitions.js.map +1 -0
  17. package/dist/esm/index.d.ts +4 -0
  18. package/dist/esm/index.js +7 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/web.d.ts +25 -0
  21. package/dist/esm/web.js +42 -0
  22. package/dist/esm/web.js.map +1 -0
  23. package/dist/plugin.cjs.js +56 -0
  24. package/dist/plugin.cjs.js.map +1 -0
  25. package/dist/plugin.js +59 -0
  26. package/dist/plugin.js.map +1 -0
  27. package/ios/Sources/MessengerNotificationsPlugin/MessengerNotificationsPlugin.swift +76 -0
  28. package/ios/Sources/MessengerNotificationsPlugin/NativeCrypto.swift +22 -0
  29. package/ios/Sources/MessengerNotificationsPlugin/NotificationHelper.swift +58 -0
  30. package/ios/Sources/MessengerNotificationsPlugin/SafeStorageStore.swift +28 -0
  31. package/ios/Sources/MessengerNotificationsPlugin/TemporarySocketSessionManager.swift +186 -0
  32. package/package.json +77 -0
@@ -0,0 +1,618 @@
1
+ package com.codecraft_studio.messenger.notifications;
2
+
3
+ import android.app.Notification;
4
+ import android.app.NotificationChannel;
5
+ import android.app.NotificationManager;
6
+ import android.app.PendingIntent;
7
+ import android.content.Context;
8
+ import android.content.Intent;
9
+ import android.content.SharedPreferences;
10
+ import android.os.Build;
11
+ import android.service.notification.StatusBarNotification;
12
+ import android.text.TextUtils;
13
+ import android.util.Log;
14
+ import androidx.annotation.Nullable;
15
+ import androidx.core.app.NotificationCompat;
16
+ import androidx.core.app.Person;
17
+ import java.util.ArrayList;
18
+ import java.util.Collections;
19
+ import java.util.Iterator;
20
+ import java.util.LinkedHashSet;
21
+ import java.util.List;
22
+ import java.util.Map;
23
+ import java.util.Set;
24
+ import java.util.concurrent.ConcurrentHashMap;
25
+ import org.json.JSONArray;
26
+ import org.json.JSONException;
27
+ import org.json.JSONObject;
28
+
29
+ final class NotificationHelper {
30
+
31
+ private static final String TAG = "NotificationHelper";
32
+ private static final String CHANNEL_ID = "chat_messages";
33
+ private static final String CHANNEL_NAME = "Chat Messages";
34
+ private static final String GROUP_KEY_PREFIX = "com.codecraft_studio.messenger.notifications.ROOM_GROUP.";
35
+ private static final int GENERIC_NOTIFICATION_ID = 900000;
36
+ private static final String PREFS_NAME = "notification_history";
37
+ private static final String KEY_RECENT_IDS = "recent_message_ids";
38
+ private static final String KEY_DISMISSED_IDS = "dismissed_message_ids";
39
+ private static final String KEY_DISMISSED_UNTIL_BY_ROOM = "dismissed_until_by_room";
40
+ private static final String KEY_ROOM_NAMES = "room_names_by_id";
41
+ private static final String KEY_USER_NAMES = "user_names_by_id";
42
+ private static final int MAX_PERSISTENT_IDS = 300;
43
+ private static final int MAX_DISMISSED_IDS = 500;
44
+ static final String EXTRA_ROOM_ID = "extra_room_id";
45
+
46
+ // Static roomId that launched the app
47
+ private static Integer pendingRoomId = null;
48
+
49
+ private static final Map<Integer, List<MessageRecord>> roomMessages = new ConcurrentHashMap<>();
50
+ private static final Map<Integer, String> roomNamesById = new ConcurrentHashMap<>();
51
+ private static final Map<Integer, String> userNamesById = new ConcurrentHashMap<>();
52
+ private static final int MAX_MESSAGES_PER_ROOM = 50;
53
+
54
+ private static final Set<String> recentMessageIds = Collections.synchronizedSet(new LinkedHashSet<>());
55
+ private static final Set<String> dismissedMessageIds = Collections.synchronizedSet(new LinkedHashSet<>());
56
+ private static final Map<Integer, Long> dismissedUntilByRoom = new ConcurrentHashMap<>();
57
+
58
+ private static volatile long serverTimeOffset = 0L;
59
+ private static final Object offsetLock = new Object();
60
+
61
+ static synchronized Integer getPendingRoomId() {
62
+ return pendingRoomId;
63
+ }
64
+
65
+ static synchronized void setPendingRoomId(Integer roomId) {
66
+ pendingRoomId = roomId;
67
+ }
68
+
69
+ static synchronized void consumePendingRoomId() {
70
+ pendingRoomId = null;
71
+ }
72
+
73
+ private static String roomGroupKey(int roomId) {
74
+ return GROUP_KEY_PREFIX + roomId;
75
+ }
76
+
77
+ private static int roomSummaryId(int roomId) {
78
+ return roomId + 500000;
79
+ }
80
+
81
+ static class MessageRecord {
82
+
83
+ @Nullable
84
+ final String id;
85
+
86
+ final String sender;
87
+ final String text;
88
+ final long timestamp;
89
+ final boolean hasServerTimestamp;
90
+ final int quality;
91
+
92
+ MessageRecord(@Nullable String id, String sender, String text, long timestamp, boolean hasServerTimestamp) {
93
+ this.id = id;
94
+ this.sender = sender;
95
+ this.text = text;
96
+ this.timestamp = timestamp;
97
+ this.hasServerTimestamp = hasServerTimestamp;
98
+ this.quality = calculateQuality(id, sender);
99
+ }
100
+
101
+ private static int calculateQuality(String id, String sender) {
102
+ int q = 0;
103
+ if (id != null && !id.isEmpty() && !"null".equalsIgnoreCase(id)) q += 10;
104
+ if (!isGenericTitle(sender)) q += 5;
105
+ return q;
106
+ }
107
+ }
108
+
109
+ private NotificationHelper() {}
110
+
111
+ static void clearRoomHistory(Context context, int roomId, boolean cancelNotification) {
112
+ roomMessages.remove(roomId);
113
+ if (cancelNotification) {
114
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
115
+ if (manager != null) {
116
+ manager.cancel(resolveNotificationId(roomId));
117
+ manager.cancel(roomSummaryId(roomId));
118
+ }
119
+ }
120
+ }
121
+
122
+ static void showRoomNotification(Context context, String title, String body, int roomId, @Nullable String messageId, long timestamp) {
123
+ showRoomNotification(context, title, body, roomId, null, messageId, timestamp, false);
124
+ }
125
+
126
+ static void showRoomNotification(
127
+ Context context,
128
+ String title,
129
+ String body,
130
+ int roomId,
131
+ @Nullable String roomName,
132
+ @Nullable String messageId,
133
+ long timestamp,
134
+ boolean isSync
135
+ ) {
136
+ if (addMessageToHistory(context, roomId, 0, messageId, title, body, roomName, timestamp, isSync)) {
137
+ triggerNotificationUpdate(context, roomId, title);
138
+ }
139
+ }
140
+
141
+ static boolean addMessageToHistory(
142
+ Context context,
143
+ int roomId,
144
+ int senderId,
145
+ @Nullable String messageId,
146
+ String title,
147
+ String body,
148
+ @Nullable String roomName,
149
+ long timestamp,
150
+ boolean isSync
151
+ ) {
152
+ if (TextUtils.isEmpty(body)) return false;
153
+ if (roomId <= 0) return false;
154
+
155
+ loadNamesIfEmpty(context);
156
+ if (roomId > 0 && !TextUtils.isEmpty(roomName) && !isGenericTitle(roomName)) {
157
+ setRoomName(context, roomId, roomName);
158
+ }
159
+
160
+ boolean hasServerTimestamp = timestamp > 0;
161
+ long useTimestamp;
162
+ if (timestamp > 0) {
163
+ useTimestamp = timestamp;
164
+ synchronized (offsetLock) {
165
+ serverTimeOffset = timestamp - System.currentTimeMillis();
166
+ }
167
+ } else {
168
+ synchronized (offsetLock) {
169
+ useTimestamp = System.currentTimeMillis() + serverTimeOffset;
170
+ }
171
+ }
172
+
173
+ String senderName = title;
174
+ if (title != null && title.contains(" in ")) {
175
+ senderName = title.split(" in ")[0];
176
+ }
177
+
178
+ if (isGenericTitle(senderName)) {
179
+ if (senderId > 0) {
180
+ String cachedUser = userNamesById.get(senderId);
181
+ if (!isGenericTitle(cachedUser)) {
182
+ senderName = cachedUser;
183
+ }
184
+ }
185
+
186
+ if (isGenericTitle(senderName)) {
187
+ List<MessageRecord> history = roomMessages.get(roomId);
188
+ if (history != null) {
189
+ synchronized (history) {
190
+ for (int i = history.size() - 1; i >= 0; i--) {
191
+ if (!isGenericTitle(history.get(i).sender)) {
192
+ senderName = history.get(i).sender;
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ if (isGenericTitle(senderName) && roomId > 0) {
201
+ String cachedRoomName = roomNamesById.get(roomId);
202
+ if (!isGenericTitle(cachedRoomName)) {
203
+ senderName = cachedRoomName;
204
+ }
205
+ }
206
+ }
207
+
208
+ if (senderId > 0 && !isGenericTitle(senderName)) {
209
+ setUserName(context, senderId, senderName);
210
+ }
211
+
212
+ if (senderName == null || isGenericTitle(senderName)) {
213
+ senderName = "New Message";
214
+ }
215
+
216
+ String normText = body.trim();
217
+ boolean hasId = (messageId != null && !messageId.isEmpty() && !"null".equalsIgnoreCase(messageId));
218
+
219
+ List<MessageRecord> history = roomMessages.get(roomId);
220
+ if (history == null) {
221
+ history = Collections.synchronizedList(new ArrayList<>());
222
+ roomMessages.put(roomId, history);
223
+ }
224
+
225
+ synchronized (history) {
226
+ if (hasId && !isSync) {
227
+ loadRecentIdsIfEmpty(context);
228
+ if (recentMessageIds.contains(messageId)) {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ if (hasId) {
234
+ loadDismissedIdsIfEmpty(context);
235
+ if (dismissedMessageIds.contains(messageId)) {
236
+ return false;
237
+ }
238
+ }
239
+
240
+ if (roomId > 0 && hasServerTimestamp) {
241
+ loadDismissedCutoffByRoomIfEmpty(context);
242
+ Long value = dismissedUntilByRoom.get(roomId);
243
+ long dismissedUntil = value == null ? 0L : value;
244
+ if (dismissedUntil > 0L && timestamp <= dismissedUntil) {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ MessageRecord newRec = new MessageRecord(messageId, senderName, body, useTimestamp, hasServerTimestamp);
250
+
251
+ for (int i = 0; i < history.size(); i++) {
252
+ MessageRecord rec = history.get(i);
253
+
254
+ if (hasId && messageId.equals(rec.id)) {
255
+ if (newRec.quality > rec.quality) {
256
+ history.remove(i);
257
+ break;
258
+ }
259
+ return false;
260
+ }
261
+
262
+ if (hasId && rec.id != null && !rec.id.isEmpty() && !"null".equalsIgnoreCase(rec.id) && !messageId.equals(rec.id)) {
263
+ continue;
264
+ }
265
+
266
+ if (
267
+ normText.equalsIgnoreCase(rec.text.trim()) &&
268
+ senderName.equals(rec.sender) &&
269
+ Math.abs(useTimestamp - rec.timestamp) < 300000
270
+ ) {
271
+ if ((hasId && (rec.id == null || rec.id.isEmpty())) || newRec.quality > rec.quality) {
272
+ history.remove(i);
273
+ break;
274
+ }
275
+ return false;
276
+ }
277
+ }
278
+
279
+ if (hasId) addAndPersistMessageId(context, messageId);
280
+ history.add(newRec);
281
+ if (history.size() > 1) {
282
+ Collections.sort(history, (a, b) -> Long.compare(a.timestamp, b.timestamp));
283
+ }
284
+ if (history.size() > MAX_MESSAGES_PER_ROOM) history.remove(0);
285
+ }
286
+
287
+ FcmFetchManager.markNotificationShown(roomId);
288
+ return true;
289
+ }
290
+
291
+ static void triggerNotificationUpdate(Context context, int roomId, String title) {
292
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
293
+ if (manager == null) return;
294
+
295
+ ensureChannel(manager);
296
+ loadNamesIfEmpty(context);
297
+
298
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
299
+ if (intent != null) {
300
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
301
+ if (roomId > 0) intent.putExtra("roomId", roomId);
302
+ }
303
+
304
+ PendingIntent pendingIntent = PendingIntent.getActivity(
305
+ context,
306
+ resolveRequestCode(roomId),
307
+ intent,
308
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
309
+ );
310
+
311
+ Intent dismissIntent = new Intent(context, NotificationDismissReceiver.class);
312
+ dismissIntent.putExtra(EXTRA_ROOM_ID, roomId);
313
+ PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(
314
+ context,
315
+ resolveRequestCode(roomId) + 1000000,
316
+ dismissIntent,
317
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
318
+ );
319
+
320
+ String convTitle = (title != null && title.contains(" in ")) ? title.split(" in ")[1] : title;
321
+ if (roomId > 0 && isGenericTitle(convTitle)) {
322
+ String cachedRoomName = roomNamesById.get(roomId);
323
+ if (!TextUtils.isEmpty(cachedRoomName)) {
324
+ convTitle = cachedRoomName;
325
+ }
326
+ }
327
+ if (isGenericTitle(convTitle)) {
328
+ convTitle = "Chat";
329
+ }
330
+
331
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
332
+ .setSmallIcon(context.getApplicationInfo().icon)
333
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
334
+ .setDefaults(NotificationCompat.DEFAULT_ALL)
335
+ .setVibrate(new long[] { 0, 250, 250, 250 })
336
+ .setAutoCancel(true)
337
+ .setGroup(roomGroupKey(roomId))
338
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
339
+ .setOnlyAlertOnce(true)
340
+ .setContentIntent(pendingIntent)
341
+ .setDeleteIntent(dismissPendingIntent);
342
+
343
+ List<MessageRecord> history = roomMessages.get(roomId);
344
+ if (history != null && !history.isEmpty()) {
345
+ Person user = new Person.Builder().setName("Me").build();
346
+
347
+ NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(user).setConversationTitle(convTitle);
348
+
349
+ MessageRecord lastMsg = null;
350
+ synchronized (history) {
351
+ for (MessageRecord msg : history) {
352
+ Person sender = new Person.Builder().setName(nonEmptyOrDefault(msg.sender, "User")).build();
353
+ style.addMessage(msg.text, msg.timestamp, sender);
354
+ lastMsg = msg;
355
+ }
356
+ }
357
+ builder.setStyle(style);
358
+ if (lastMsg != null) {
359
+ builder
360
+ .setContentTitle(nonEmptyOrDefault(lastMsg.sender, "New Message"))
361
+ .setContentText(lastMsg.text)
362
+ .setWhen(lastMsg.timestamp);
363
+ }
364
+ } else {
365
+ builder.setContentTitle(nonEmptyOrDefault(title, "New Message")).setContentText("You have new messages");
366
+ }
367
+
368
+ manager.notify(resolveNotificationId(roomId), builder.build());
369
+ postRoomSummary(context, manager, roomId, convTitle);
370
+ }
371
+
372
+ private static void postRoomSummary(Context context, NotificationManager manager, int roomId, String convTitle) {
373
+ List<MessageRecord> history = roomMessages.get(roomId);
374
+ int msgCount = history != null ? history.size() : 0;
375
+
376
+ if (msgCount <= 1) {
377
+ manager.cancel(roomSummaryId(roomId));
378
+ return;
379
+ }
380
+
381
+ String summaryText = msgCount + " new messages";
382
+
383
+ Notification summaryNotification = new NotificationCompat.Builder(context, CHANNEL_ID)
384
+ .setContentTitle(convTitle)
385
+ .setContentText(summaryText)
386
+ .setSmallIcon(context.getApplicationInfo().icon)
387
+ .setStyle(new NotificationCompat.InboxStyle().setSummaryText(convTitle))
388
+ .setGroup(roomGroupKey(roomId))
389
+ .setGroupSummary(true)
390
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
391
+ .setSilent(true)
392
+ .setAutoCancel(true)
393
+ .build();
394
+
395
+ manager.notify(roomSummaryId(roomId), summaryNotification);
396
+ }
397
+
398
+ static void onNotificationDismissed(Context context, int roomId) {
399
+ List<MessageRecord> history = roomMessages.get(roomId);
400
+ long latestServerTimestamp = 0L;
401
+ if (history != null) {
402
+ synchronized (history) {
403
+ for (MessageRecord record : history) {
404
+ if (!TextUtils.isEmpty(record.id) && !"null".equalsIgnoreCase(record.id)) {
405
+ addAndPersistDismissedMessageId(context, record.id);
406
+ }
407
+ if (record.hasServerTimestamp && record.timestamp > latestServerTimestamp) {
408
+ latestServerTimestamp = record.timestamp;
409
+ }
410
+ }
411
+ }
412
+ }
413
+ if (roomId > 0 && latestServerTimestamp > 0L) {
414
+ setAndPersistDismissedUntilForRoom(context, roomId, latestServerTimestamp);
415
+ }
416
+ roomMessages.remove(roomId);
417
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
418
+ if (manager != null) {
419
+ manager.cancel(roomSummaryId(roomId));
420
+ }
421
+ }
422
+
423
+ private static boolean isGenericTitle(@Nullable String title) {
424
+ if (TextUtils.isEmpty(title)) return true;
425
+ String t = title.trim().toLowerCase();
426
+ return "new message".equals(t) || "new messages".equals(t) || "message".equals(t);
427
+ }
428
+
429
+ private static void loadRecentIdsIfEmpty(Context context) {
430
+ if (!recentMessageIds.isEmpty()) return;
431
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
432
+ String json = prefs.getString(KEY_RECENT_IDS, null);
433
+ if (json != null) {
434
+ try {
435
+ JSONArray array = new JSONArray(json);
436
+ synchronized (recentMessageIds) {
437
+ for (int i = 0; i < array.length(); i++) {
438
+ recentMessageIds.add(array.getString(i));
439
+ }
440
+ }
441
+ } catch (JSONException ignored) {}
442
+ }
443
+ }
444
+
445
+ private static void loadDismissedIdsIfEmpty(Context context) {
446
+ if (!dismissedMessageIds.isEmpty()) return;
447
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
448
+ String json = prefs.getString(KEY_DISMISSED_IDS, null);
449
+ if (json != null) {
450
+ try {
451
+ JSONArray array = new JSONArray(json);
452
+ synchronized (dismissedMessageIds) {
453
+ for (int i = 0; i < array.length(); i++) {
454
+ dismissedMessageIds.add(array.getString(i));
455
+ }
456
+ }
457
+ } catch (JSONException ignored) {}
458
+ }
459
+ }
460
+
461
+ private static void loadDismissedCutoffByRoomIfEmpty(Context context) {
462
+ if (!dismissedUntilByRoom.isEmpty()) return;
463
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
464
+ String json = prefs.getString(KEY_DISMISSED_UNTIL_BY_ROOM, null);
465
+ if (TextUtils.isEmpty(json)) return;
466
+ try {
467
+ JSONObject object = new JSONObject(json);
468
+ Iterator<String> keys = object.keys();
469
+ while (keys.hasNext()) {
470
+ String roomKey = keys.next();
471
+ try {
472
+ int roomId = Integer.parseInt(roomKey);
473
+ long dismissedUntil = object.optLong(roomKey, 0L);
474
+ if (roomId > 0 && dismissedUntil > 0L) {
475
+ dismissedUntilByRoom.put(roomId, dismissedUntil);
476
+ }
477
+ } catch (NumberFormatException ignored) {}
478
+ }
479
+ } catch (JSONException ignored) {}
480
+ }
481
+
482
+ private static void loadNamesIfEmpty(Context context) {
483
+ if (!roomNamesById.isEmpty() || !userNamesById.isEmpty()) return;
484
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
485
+ loadMapFromPrefs(prefs, KEY_ROOM_NAMES, roomNamesById);
486
+ loadMapFromPrefs(prefs, KEY_USER_NAMES, userNamesById);
487
+ }
488
+
489
+ private static void loadMapFromPrefs(SharedPreferences prefs, String key, Map<Integer, String> targetMap) {
490
+ String json = prefs.getString(key, null);
491
+ if (TextUtils.isEmpty(json)) return;
492
+ try {
493
+ JSONObject object = new JSONObject(json);
494
+ Iterator<String> keys = object.keys();
495
+ while (keys.hasNext()) {
496
+ String strId = keys.next();
497
+ try {
498
+ int id = Integer.parseInt(strId);
499
+ String name = object.getString(strId);
500
+ if (id > 0 && !TextUtils.isEmpty(name)) targetMap.put(id, name);
501
+ } catch (NumberFormatException ignored) {}
502
+ }
503
+ } catch (JSONException ignored) {}
504
+ }
505
+
506
+ private static void persistMapToPrefs(Context context, String key, Map<Integer, String> map) {
507
+ JSONObject object = new JSONObject();
508
+ for (Map.Entry<Integer, String> entry : map.entrySet()) {
509
+ try {
510
+ object.put(String.valueOf(entry.getKey()), entry.getValue());
511
+ } catch (JSONException ignored) {}
512
+ }
513
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().putString(key, object.toString()).apply();
514
+ }
515
+
516
+ private static void addAndPersistMessageId(Context context, String messageId) {
517
+ synchronized (recentMessageIds) {
518
+ recentMessageIds.remove(messageId);
519
+ recentMessageIds.add(messageId);
520
+ if (recentMessageIds.size() > MAX_PERSISTENT_IDS) {
521
+ Iterator<String> it = recentMessageIds.iterator();
522
+ int toRemove = recentMessageIds.size() - MAX_PERSISTENT_IDS;
523
+ while (toRemove > 0 && it.hasNext()) {
524
+ it.next();
525
+ it.remove();
526
+ toRemove--;
527
+ }
528
+ }
529
+ }
530
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
531
+ JSONArray array = new JSONArray();
532
+ synchronized (recentMessageIds) {
533
+ for (String id : recentMessageIds) array.put(id);
534
+ }
535
+ prefs.edit().putString(KEY_RECENT_IDS, array.toString()).apply();
536
+ }
537
+
538
+ private static void addAndPersistDismissedMessageId(Context context, String messageId) {
539
+ synchronized (dismissedMessageIds) {
540
+ dismissedMessageIds.remove(messageId);
541
+ dismissedMessageIds.add(messageId);
542
+ if (dismissedMessageIds.size() > MAX_DISMISSED_IDS) {
543
+ Iterator<String> it = dismissedMessageIds.iterator();
544
+ int toRemove = dismissedMessageIds.size() - MAX_DISMISSED_IDS;
545
+ while (toRemove > 0 && it.hasNext()) {
546
+ it.next();
547
+ it.remove();
548
+ toRemove--;
549
+ }
550
+ }
551
+ }
552
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
553
+ JSONArray array = new JSONArray();
554
+ synchronized (dismissedMessageIds) {
555
+ for (String id : dismissedMessageIds) array.put(id);
556
+ }
557
+ prefs.edit().putString(KEY_DISMISSED_IDS, array.toString()).apply();
558
+ }
559
+
560
+ private static void setAndPersistDismissedUntilForRoom(Context context, int roomId, long dismissedUntilTs) {
561
+ if (roomId <= 0 || dismissedUntilTs <= 0L) return;
562
+ loadDismissedCutoffByRoomIfEmpty(context);
563
+ Long existing = dismissedUntilByRoom.get(roomId);
564
+ if (existing != null && existing >= dismissedUntilTs) return;
565
+ dismissedUntilByRoom.put(roomId, dismissedUntilTs);
566
+ persistDismissedCutoffByRoom(context);
567
+ }
568
+
569
+ private static void persistDismissedCutoffByRoom(Context context) {
570
+ JSONObject object = new JSONObject();
571
+ for (Map.Entry<Integer, Long> entry : dismissedUntilByRoom.entrySet()) {
572
+ if (entry.getKey() == null || entry.getValue() == null || entry.getKey() <= 0 || entry.getValue() <= 0L) continue;
573
+ try {
574
+ object.put(String.valueOf(entry.getKey()), entry.getValue());
575
+ } catch (JSONException ignored) {}
576
+ }
577
+ context
578
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
579
+ .edit()
580
+ .putString(KEY_DISMISSED_UNTIL_BY_ROOM, object.toString())
581
+ .apply();
582
+ }
583
+
584
+ private static void ensureChannel(NotificationManager manager) {
585
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
586
+ NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID);
587
+ if (channel == null) {
588
+ channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
589
+ channel.setDescription("Notifications for chat messages");
590
+ channel.enableVibration(true);
591
+ manager.createNotificationChannel(channel);
592
+ }
593
+ }
594
+
595
+ private static int resolveNotificationId(int roomId) {
596
+ return roomId > 0 ? roomId : GENERIC_NOTIFICATION_ID;
597
+ }
598
+
599
+ private static int resolveRequestCode(int roomId) {
600
+ return roomId > 0 ? roomId : GENERIC_NOTIFICATION_ID;
601
+ }
602
+
603
+ private static String nonEmptyOrDefault(@Nullable String value, String fallback) {
604
+ return value == null || value.trim().isEmpty() ? fallback : value;
605
+ }
606
+
607
+ static void setRoomName(Context context, int roomId, @Nullable String roomName) {
608
+ if (roomId <= 0 || TextUtils.isEmpty(roomName) || isGenericTitle(roomName)) return;
609
+ roomNamesById.put(roomId, roomName);
610
+ persistMapToPrefs(context, KEY_ROOM_NAMES, roomNamesById);
611
+ }
612
+
613
+ static void setUserName(Context context, int userId, @Nullable String userName) {
614
+ if (userId <= 0 || TextUtils.isEmpty(userName) || isGenericTitle(userName)) return;
615
+ userNamesById.put(userId, userName);
616
+ persistMapToPrefs(context, KEY_USER_NAMES, userNamesById);
617
+ }
618
+ }