@realtimexsco/live-chat 1.4.9 → 1.4.10

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.
@@ -6,7 +6,7 @@
6
6
  * cp node_modules/@realtimexsco/live-chat/dist/firebase-messaging-sw.js public/
7
7
  */
8
8
 
9
- /* global firebase, importScripts, BroadcastChannel */
9
+ /* global firebase, importScripts, BroadcastChannel, caches */
10
10
 
11
11
  importScripts(
12
12
  "https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js",
@@ -17,45 +17,15 @@ importScripts(
17
17
 
18
18
  var PUSH_BROADCAST_CHANNEL = "realtimex-push";
19
19
  var NOTIFICATION_CLICK_SW_TYPE = "realtimex:notification-click";
20
+ var PUSH_DATA_CACHE = "realtimex-push-data-v1";
21
+ var TAG_PREFIX = "realtimex-";
22
+
23
+ /** In-memory fallback when Cache API or FCM click data is empty. */
24
+ var pendingPushDataStore = {};
20
25
 
21
26
  var params = new URL(self.location.href).searchParams;
22
27
  var rawConfig = params.get("config");
23
28
 
24
- if (rawConfig) {
25
- try {
26
- var firebaseConfig = JSON.parse(rawConfig);
27
- firebase.initializeApp(firebaseConfig);
28
- var messaging = firebase.messaging();
29
-
30
- messaging.onBackgroundMessage(function (payload) {
31
- console.log("[realtimex-push] FCM background payload (raw):", payload);
32
- console.log("[realtimex-push] FCM background data:", payload && payload.data);
33
-
34
- var title =
35
- (payload.notification && payload.notification.title) || "New message";
36
- var body = (payload.notification && payload.notification.body) || "";
37
- var data = payload.data || {};
38
- var tag = "realtimex-" + (data.conversationId || data.messageId || "push");
39
-
40
- // Show notification ourselves so click always carries `data` for redirect.
41
- // Same tag replaces FCM auto-notification when both fire.
42
- return self.registration.showNotification(title, {
43
- body: body,
44
- data: data,
45
- tag: tag,
46
- renotify: true,
47
- });
48
- });
49
- } catch (error) {
50
- console.error("[realtimex] Invalid firebase config in SW URL:", error);
51
- }
52
- } else {
53
- console.warn(
54
- "[realtimex] firebase-messaging-sw.js registered without ?config= — " +
55
- "push notifications will not be delivered",
56
- );
57
- }
58
-
59
29
  function parseMaybeJson(value) {
60
30
  if (typeof value !== "string") return value;
61
31
  try {
@@ -65,6 +35,10 @@ function parseMaybeJson(value) {
65
35
  }
66
36
  }
67
37
 
38
+ function cacheRequest(path) {
39
+ return new Request(new URL(path, self.location.origin).toString());
40
+ }
41
+
68
42
  function extractNotificationData(notification) {
69
43
  var raw = (notification && notification.data) || {};
70
44
  var fcmMsg = parseMaybeJson(raw.FCM_MSG);
@@ -99,14 +73,26 @@ function resolveConversationId(data) {
99
73
  ).trim();
100
74
  }
101
75
 
102
- function buildTargetUrl(data, conversationId) {
76
+ function buildNotificationTag(data) {
77
+ return TAG_PREFIX + (data.conversationId || data.messageId || "push");
78
+ }
79
+
80
+ function buildTargetUrl(data) {
103
81
  if (data && data.link) return String(data.link);
104
- if (conversationId) {
105
- return "/chat?conversationId=" + encodeURIComponent(conversationId);
106
- }
107
82
  return "/chat";
108
83
  }
109
84
 
85
+ function cachePendingOpen(data) {
86
+ return caches.open(PUSH_DATA_CACHE).then(function (cache) {
87
+ return cache.put(
88
+ cacheRequest("pending-open"),
89
+ new Response(JSON.stringify(data), {
90
+ headers: { "Content-Type": "application/json" },
91
+ }),
92
+ );
93
+ });
94
+ }
95
+
110
96
  function isChatClient(client) {
111
97
  try {
112
98
  var pathname = new URL(client.url).pathname || "";
@@ -126,17 +112,238 @@ function notifyPage(message) {
126
112
  }
127
113
  }
128
114
 
129
- self.addEventListener("notificationclick", function (event) {
130
- event.notification.close();
115
+ function rememberPushData(data) {
116
+ var messageId = String(data.messageId || data.message_id || "").trim();
117
+ var conversationId = String(data.conversationId || data.conversation_id || "").trim();
131
118
 
132
- console.log(
133
- "[realtimex-push] notificationclick raw notification.data:",
134
- event.notification && event.notification.data,
119
+ if (messageId) pendingPushDataStore["msg:" + messageId] = data;
120
+ if (conversationId) pendingPushDataStore["conv:" + conversationId] = data;
121
+
122
+ var keys = Object.keys(pendingPushDataStore);
123
+ if (keys.length > 40) {
124
+ keys.slice(0, keys.length - 20).forEach(function (key) {
125
+ delete pendingPushDataStore[key];
126
+ });
127
+ }
128
+
129
+ return cachePushData(data);
130
+ }
131
+
132
+ function recallPushData(data) {
133
+ var messageId = String(
134
+ data.messageId || data.message_id || data.messageID || "",
135
+ ).trim();
136
+ var conversationId = String(
137
+ data.conversationId || data.conversation_id || "",
138
+ ).trim();
139
+
140
+ if (messageId && pendingPushDataStore["msg:" + messageId]) {
141
+ return pendingPushDataStore["msg:" + messageId];
142
+ }
143
+ if (conversationId && pendingPushDataStore["conv:" + conversationId]) {
144
+ return pendingPushDataStore["conv:" + conversationId];
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function cachePushData(data) {
150
+ var messageId = String(data.messageId || data.message_id || "").trim();
151
+ var conversationId = String(data.conversationId || data.conversation_id || "").trim();
152
+ var entries = [];
153
+ if (messageId) entries.push("push-data/" + messageId);
154
+ if (conversationId) entries.push("push-data/conv/" + conversationId);
155
+ if (!entries.length) return Promise.resolve();
156
+
157
+ var body = JSON.stringify(data);
158
+
159
+ return caches.open(PUSH_DATA_CACHE).then(function (cache) {
160
+ return Promise.all(
161
+ entries.map(function (path) {
162
+ return cache.put(
163
+ cacheRequest(path),
164
+ new Response(body, {
165
+ headers: { "Content-Type": "application/json" },
166
+ }),
167
+ );
168
+ }),
169
+ );
170
+ });
171
+ }
172
+
173
+ function loadCachedPushData(messageId) {
174
+ var key = String(messageId || "").trim();
175
+ if (!key) return Promise.resolve(null);
176
+ return caches
177
+ .open(PUSH_DATA_CACHE)
178
+ .then(function (cache) {
179
+ return cache.match(cacheRequest("push-data/" + key));
180
+ })
181
+ .then(function (res) {
182
+ return res ? res.json() : null;
183
+ })
184
+ .catch(function () {
185
+ return null;
186
+ });
187
+ }
188
+
189
+ function loadCachedPushDataByConversationId(conversationId) {
190
+ var key = String(conversationId || "").trim();
191
+ if (!key) return Promise.resolve(null);
192
+ return caches
193
+ .open(PUSH_DATA_CACHE)
194
+ .then(function (cache) {
195
+ return cache.match(cacheRequest("push-data/conv/" + key));
196
+ })
197
+ .then(function (res) {
198
+ return res ? res.json() : null;
199
+ })
200
+ .catch(function () {
201
+ return null;
202
+ });
203
+ }
204
+
205
+ function resolveFromNotificationTag(tag) {
206
+ if (!tag || tag.indexOf(TAG_PREFIX) !== 0) return Promise.resolve(null);
207
+ var id = tag.slice(TAG_PREFIX.length);
208
+ if (!id || id === "push") return Promise.resolve(null);
209
+
210
+ return loadCachedPushData(id).then(function (cached) {
211
+ if (cached) return cached;
212
+ return loadCachedPushDataByConversationId(id);
213
+ });
214
+ }
215
+
216
+ function resolveNotificationClickData(data, notification) {
217
+ var merged = Object.assign({}, data);
218
+ if (resolveConversationId(merged)) return Promise.resolve(merged);
219
+
220
+ var recalled = recallPushData(merged);
221
+ if (recalled) merged = Object.assign({}, recalled, merged);
222
+ if (resolveConversationId(merged)) return Promise.resolve(merged);
223
+
224
+ var messageId = String(
225
+ merged.messageId || merged.message_id || merged.messageID || "",
226
+ ).trim();
227
+
228
+ return resolveFromNotificationTag(notification && notification.tag)
229
+ .then(function (fromTag) {
230
+ if (fromTag) merged = Object.assign({}, fromTag, merged);
231
+ if (resolveConversationId(merged)) return merged;
232
+
233
+ return loadCachedPushData(messageId).then(function (cached) {
234
+ if (cached) merged = Object.assign({}, cached, merged);
235
+ if (resolveConversationId(merged)) return merged;
236
+
237
+ return loadCachedPushDataByConversationId(
238
+ merged.conversationId || merged.conversation_id,
239
+ ).then(function (byConv) {
240
+ if (byConv) merged = Object.assign({}, byConv, merged);
241
+ return merged;
242
+ });
243
+ });
244
+ });
245
+ }
246
+
247
+ /** Keep only our tagged notification that carries redirect data. */
248
+ function pruneDuplicateNotifications(keepTag) {
249
+ return self.registration.getNotifications().then(function (notifications) {
250
+ notifications.forEach(function (n) {
251
+ var cid = resolveConversationId(extractNotificationData(n));
252
+ if (n.tag === keepTag && cid) return;
253
+ if (!cid || n.tag !== keepTag) n.close();
254
+ });
255
+ });
256
+ }
257
+
258
+ /**
259
+ * One background OS notification with redirect `data`.
260
+ * Closes FCM auto duplicates that lack click data.
261
+ */
262
+ function showSingleBackgroundNotification(title, body, data) {
263
+ var tag = buildNotificationTag(data);
264
+
265
+ return rememberPushData(data)
266
+ .then(function () {
267
+ return self.registration.getNotifications();
268
+ })
269
+ .then(function (notifications) {
270
+ notifications.forEach(function (n) {
271
+ n.close();
272
+ });
273
+ return self.registration.showNotification(title, {
274
+ body: body,
275
+ data: data,
276
+ tag: tag,
277
+ });
278
+ })
279
+ .then(function () {
280
+ var delays = [80, 250, 600, 1200];
281
+ return delays.reduce(function (chain, ms) {
282
+ return chain.then(function () {
283
+ return new Promise(function (resolve) {
284
+ setTimeout(function () {
285
+ pruneDuplicateNotifications(tag).then(resolve);
286
+ }, ms);
287
+ });
288
+ });
289
+ }, Promise.resolve());
290
+ });
291
+ }
292
+
293
+ if (rawConfig) {
294
+ try {
295
+ var firebaseConfig = JSON.parse(rawConfig);
296
+ firebase.initializeApp(firebaseConfig);
297
+ var messaging = firebase.messaging();
298
+
299
+ messaging.onBackgroundMessage(function (payload) {
300
+ console.log("[realtimex-push] FCM background payload (raw):", payload);
301
+ console.log("[realtimex-push] FCM background data:", payload && payload.data);
302
+
303
+ return clients
304
+ .matchAll({ type: "window", includeUncontrolled: true })
305
+ .then(function (windowClients) {
306
+ var hasVisibleClient = windowClients.some(function (client) {
307
+ return client.visibilityState === "visible";
308
+ });
309
+ if (hasVisibleClient) {
310
+ console.log(
311
+ "[realtimex-push] app visible — skip OS notification (use in-app toast)",
312
+ );
313
+ return;
314
+ }
315
+
316
+ var data = payload.data || {};
317
+ var title =
318
+ (payload.notification && payload.notification.title) ||
319
+ data.title ||
320
+ "New message";
321
+ var body =
322
+ (payload.notification && payload.notification.body) ||
323
+ data.body ||
324
+ "";
325
+
326
+ console.log(
327
+ "[realtimex-push] single background notification:",
328
+ data,
329
+ );
330
+
331
+ return showSingleBackgroundNotification(title, body, data);
332
+ });
333
+ });
334
+ } catch (error) {
335
+ console.error("[realtimex] Invalid firebase config in SW URL:", error);
336
+ }
337
+ } else {
338
+ console.warn(
339
+ "[realtimex] firebase-messaging-sw.js registered without ?config= — " +
340
+ "push notifications will not be delivered",
135
341
  );
342
+ }
136
343
 
137
- var data = extractNotificationData(event.notification);
344
+ function deliverNotificationClick(data) {
138
345
  var conversationId = resolveConversationId(data);
139
- var targetUrl = buildTargetUrl(data, conversationId);
346
+ var targetUrl = buildTargetUrl(data);
140
347
  var message = {
141
348
  type: NOTIFICATION_CLICK_SW_TYPE,
142
349
  conversationId: conversationId,
@@ -147,30 +354,64 @@ self.addEventListener("notificationclick", function (event) {
147
354
  console.log("[realtimex-push] notificationclick conversationId:", conversationId);
148
355
  console.log("[realtimex-push] notificationclick targetUrl:", targetUrl);
149
356
 
150
- event.waitUntil(
151
- clients
152
- .matchAll({ type: "window", includeUncontrolled: true })
153
- .then(function (windowClients) {
154
- var chatClient = null;
155
- var anyClient = null;
156
-
157
- for (var i = 0; i < windowClients.length; i++) {
158
- var client = windowClients[i];
159
- if (!anyClient) anyClient = client;
160
- if (!chatClient && isChatClient(client)) chatClient = client;
161
- }
162
-
163
- var target = chatClient || anyClient;
164
- if (target && "focus" in target) {
165
- return target.focus().then(function () {
166
- notifyPage(message);
167
- if (target.postMessage) target.postMessage(message);
168
- });
169
- }
357
+ if (!conversationId) {
358
+ console.warn(
359
+ "[realtimex-push] notificationclick missing conversationId redirect skipped",
360
+ );
361
+ return Promise.resolve();
362
+ }
170
363
 
171
- notifyPage(message);
172
- if (clients.openWindow) return clients.openWindow(targetUrl);
173
- return undefined;
174
- }),
364
+ return clients
365
+ .matchAll({ type: "window", includeUncontrolled: true })
366
+ .then(function (windowClients) {
367
+ var chatClient = null;
368
+ var anyClient = null;
369
+
370
+ for (var i = 0; i < windowClients.length; i++) {
371
+ var client = windowClients[i];
372
+ if (!anyClient) anyClient = client;
373
+ if (!chatClient && isChatClient(client)) chatClient = client;
374
+ }
375
+
376
+ var target = chatClient || anyClient;
377
+
378
+ if (target && "focus" in target) {
379
+ return target.focus().then(function () {
380
+ notifyPage(message);
381
+ if (target.postMessage) target.postMessage(message);
382
+
383
+ if (conversationId && !isChatClient(target) && clients.openWindow) {
384
+ return cachePendingOpen(data).then(function () {
385
+ return clients.openWindow(targetUrl);
386
+ });
387
+ }
388
+ return undefined;
389
+ });
390
+ }
391
+
392
+ notifyPage(message);
393
+ if (clients.openWindow) {
394
+ return cachePendingOpen(data).then(function () {
395
+ return clients.openWindow(targetUrl);
396
+ });
397
+ }
398
+ return undefined;
399
+ });
400
+ }
401
+
402
+ self.addEventListener("notificationclick", function (event) {
403
+ event.notification.close();
404
+
405
+ console.log(
406
+ "[realtimex-push] notificationclick raw notification.data:",
407
+ event.notification && event.notification.data,
408
+ );
409
+
410
+ var rawData = extractNotificationData(event.notification);
411
+
412
+ event.waitUntil(
413
+ resolveNotificationClickData(rawData, event.notification).then(function (data) {
414
+ return deliverNotificationClick(data);
415
+ }),
175
416
  );
176
417
  });