@realtimexsco/live-chat 1.4.8 → 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.
@@ -4,15 +4,9 @@
4
4
  * /firebase-messaging-sw.js:
5
5
  *
6
6
  * cp node_modules/@realtimexsco/live-chat/dist/firebase-messaging-sw.js public/
7
- *
8
- * Do NOT edit the Firebase config into this file — the chat package passes it
9
- * via the registration URL (?config=...), so the copy stays generic.
10
- *
11
- * If your app already has its own service worker at the root scope, merge the
12
- * contents of this file into it instead (two workers cannot share a scope).
13
7
  */
14
8
 
15
- /* global firebase, importScripts */
9
+ /* global firebase, importScripts, BroadcastChannel, caches */
16
10
 
17
11
  importScripts(
18
12
  "https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js",
@@ -21,47 +15,37 @@ importScripts(
21
15
  "https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js",
22
16
  );
23
17
 
18
+ var PUSH_BROADCAST_CHANNEL = "realtimex-push";
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 = {};
25
+
24
26
  var params = new URL(self.location.href).searchParams;
25
27
  var rawConfig = params.get("config");
26
28
 
27
- if (rawConfig) {
29
+ function parseMaybeJson(value) {
30
+ if (typeof value !== "string") return value;
28
31
  try {
29
- var firebaseConfig = JSON.parse(rawConfig);
30
-
31
- firebase.initializeApp(firebaseConfig);
32
-
33
- // Background messages that carry a `notification` payload are displayed
34
- // by FCM automatically; initializing messaging here is what enables it.
35
- var messaging = firebase.messaging();
36
-
37
- // Log full backend payload when a push arrives in the background.
38
- // Check this in DevTools → Application → Service Workers → console,
39
- // or Chrome → chrome://serviceworker-internals
40
- messaging.onBackgroundMessage(function (payload) {
41
- console.log("[realtimex-push] FCM background payload (raw):", payload);
42
- console.log("[realtimex-push] FCM background data:", payload && payload.data);
43
- console.log(
44
- "[realtimex-push] FCM background notification:",
45
- payload && payload.notification,
46
- );
47
- });
48
- } catch (error) {
49
- console.error("[realtimex] Invalid firebase config in SW URL:", error);
32
+ return JSON.parse(value);
33
+ } catch (e) {
34
+ return value;
50
35
  }
51
- } else {
52
- console.warn(
53
- "[realtimex] firebase-messaging-sw.js registered without ?config= — " +
54
- "push notifications will not be delivered",
55
- );
36
+ }
37
+
38
+ function cacheRequest(path) {
39
+ return new Request(new URL(path, self.location.origin).toString());
56
40
  }
57
41
 
58
42
  function extractNotificationData(notification) {
59
43
  var raw = (notification && notification.data) || {};
44
+ var fcmMsg = parseMaybeJson(raw.FCM_MSG);
60
45
  var nested =
61
- raw.FCM_MSG && raw.FCM_MSG.data && typeof raw.FCM_MSG.data === "object"
62
- ? raw.FCM_MSG.data
63
- : null;
46
+ fcmMsg && fcmMsg.data && typeof fcmMsg.data === "object" ? fcmMsg.data : null;
64
47
  var merged = {};
48
+
65
49
  if (nested) {
66
50
  for (var key in nested) {
67
51
  if (Object.prototype.hasOwnProperty.call(nested, key)) {
@@ -69,11 +53,13 @@ function extractNotificationData(notification) {
69
53
  }
70
54
  }
71
55
  }
56
+
72
57
  for (var key2 in raw) {
73
58
  if (Object.prototype.hasOwnProperty.call(raw, key2) && key2 !== "FCM_MSG") {
74
59
  merged[key2] = raw[key2];
75
60
  }
76
61
  }
62
+
77
63
  return merged;
78
64
  }
79
65
 
@@ -84,19 +70,29 @@ function resolveConversationId(data) {
84
70
  data.conversation_id ||
85
71
  data.conversationID ||
86
72
  "",
87
- );
73
+ ).trim();
88
74
  }
89
75
 
90
- function buildTargetUrl(data, conversationId) {
91
- if (data && data.link) return String(data.link);
92
-
93
- if (conversationId) {
94
- return "/chat?conversationId=" + encodeURIComponent(conversationId);
95
- }
76
+ function buildNotificationTag(data) {
77
+ return TAG_PREFIX + (data.conversationId || data.messageId || "push");
78
+ }
96
79
 
80
+ function buildTargetUrl(data) {
81
+ if (data && data.link) return String(data.link);
97
82
  return "/chat";
98
83
  }
99
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
+
100
96
  function isChatClient(client) {
101
97
  try {
102
98
  var pathname = new URL(client.url).pathname || "";
@@ -106,49 +102,316 @@ function isChatClient(client) {
106
102
  }
107
103
  }
108
104
 
109
- // Focus (or open) the chat and tell the page which conversation to open.
110
- self.addEventListener("notificationclick", function (event) {
111
- event.notification.close();
105
+ function notifyPage(message) {
106
+ try {
107
+ var bc = new BroadcastChannel(PUSH_BROADCAST_CHANNEL);
108
+ bc.postMessage(message);
109
+ bc.close();
110
+ } catch (e) {
111
+ // ignore
112
+ }
113
+ }
112
114
 
113
- console.log(
114
- "[realtimex-push] notificationclick raw notification.data:",
115
- event.notification && event.notification.data,
115
+ function rememberPushData(data) {
116
+ var messageId = String(data.messageId || data.message_id || "").trim();
117
+ var conversationId = String(data.conversationId || data.conversation_id || "").trim();
118
+
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",
116
341
  );
342
+ }
117
343
 
118
- var data = extractNotificationData(event.notification);
344
+ function deliverNotificationClick(data) {
119
345
  var conversationId = resolveConversationId(data);
120
- var targetUrl = buildTargetUrl(data, conversationId);
346
+ var targetUrl = buildTargetUrl(data);
121
347
  var message = {
122
- type: "realtimex:notification-click",
348
+ type: NOTIFICATION_CLICK_SW_TYPE,
123
349
  conversationId: conversationId,
350
+ data: data,
124
351
  };
125
352
 
126
353
  console.log("[realtimex-push] notificationclick extracted data:", data);
127
354
  console.log("[realtimex-push] notificationclick conversationId:", conversationId);
128
355
  console.log("[realtimex-push] notificationclick targetUrl:", targetUrl);
129
356
 
130
- event.waitUntil(
131
- clients
132
- .matchAll({ type: "window", includeUncontrolled: true })
133
- .then(function (windowClients) {
134
- var chatClient = null;
135
- var anyClient = null;
136
-
137
- for (var i = 0; i < windowClients.length; i++) {
138
- var client = windowClients[i];
139
- if (!anyClient) anyClient = client;
140
- if (!chatClient && isChatClient(client)) chatClient = client;
141
- }
142
-
143
- var target = chatClient || anyClient;
144
- if (target && "focus" in target) {
145
- return target.focus().then(function () {
146
- if (target.postMessage) target.postMessage(message);
147
- });
148
- }
357
+ if (!conversationId) {
358
+ console.warn(
359
+ "[realtimex-push] notificationclick missing conversationId redirect skipped",
360
+ );
361
+ return Promise.resolve();
362
+ }
149
363
 
150
- if (clients.openWindow) return clients.openWindow(targetUrl);
151
- return undefined;
152
- }),
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
+ }),
153
416
  );
154
417
  });