@ethora/chat-component 26.2.20 → 26.2.22

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.
@@ -1,8 +1,7 @@
1
- /* eslint-disable no-empty, no-restricted-globals */
1
+ /* eslint-disable no-empty */
2
2
  /* eslint-env worker */
3
- /* global firebase, clients */
3
+ /* global firebase, clients, importScripts */
4
4
 
5
- // ── Firebase SDK ──────────────────────────────────────────────
6
5
  importScripts(
7
6
  'https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js'
8
7
  );
@@ -10,9 +9,30 @@ importScripts(
10
9
  'https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js'
11
10
  );
12
11
 
13
- // ── Dynamic Configuration ─────────────────────────────────────
12
+ // ── DEBUG ─────────────────────────────────────────────────────
13
+ const DEBUG = false;
14
+ const DEDUP_TTL = 60 * 1000;
15
+
14
16
  const params = new URL(self.location.href).searchParams;
17
+ const CUSTOM_ICON = (params.get('iconPath') || '').trim();
18
+ const CUSTOM_BADGE = (params.get('badgePath') || '').trim();
19
+ const DEFAULT_ICON = CUSTOM_ICON || '/favicon.ico';
20
+ const DEFAULT_BADGE = CUSTOM_BADGE || CUSTOM_ICON || '/favicon.ico';
21
+
22
+ // ── LOGGING ───────────────────────────────────────────────────
23
+ function log(...args) {
24
+ if (DEBUG) console.log('[SW]', ...args);
25
+ }
15
26
 
27
+ function warn(...args) {
28
+ if (DEBUG) console.warn('[SW]', ...args);
29
+ }
30
+
31
+ function error(...args) {
32
+ console.error('[SW]', ...args);
33
+ }
34
+
35
+ // ── CONFIG ────────────────────────────────────────────────────
16
36
  const firebaseConfig = {
17
37
  apiKey: params.get('apiKey'),
18
38
  authDomain: params.get('authDomain'),
@@ -22,477 +42,422 @@ const firebaseConfig = {
22
42
  appId: params.get('appId'),
23
43
  };
24
44
 
25
- let messaging = null;
45
+ const APP_URL = self.location.origin;
46
+
47
+ // ── INIT FIREBASE ─────────────────────────────────────────────
48
+ try {
49
+ if (
50
+ firebaseConfig.apiKey &&
51
+ firebaseConfig.appId &&
52
+ firebaseConfig.messagingSenderId &&
53
+ firebaseConfig.projectId
54
+ ) {
55
+ if (!firebase.apps.length) {
56
+ firebase.initializeApp(firebaseConfig);
57
+ log('Firebase initialized');
58
+ } else {
59
+ log('Firebase already initialized');
60
+ }
26
61
 
27
- if (
28
- firebaseConfig.apiKey &&
29
- firebaseConfig.appId &&
30
- firebaseConfig.messagingSenderId &&
31
- firebaseConfig.projectId
32
- ) {
33
- if (!firebase.apps.length) {
34
- firebase.initializeApp(firebaseConfig);
62
+ firebase.messaging();
63
+ log('Firebase messaging ready (without onBackgroundMessage)');
64
+ } else {
65
+ warn('Firebase config missing', firebaseConfig);
35
66
  }
36
- messaging = firebase.messaging();
37
- } else {
38
- // console.warn('[SW] Firebase config missing → FCM disabled');
67
+ } catch (e) {
68
+ error('Firebase init error', e);
39
69
  }
40
70
 
41
- const APP_URL = self.location.origin;
71
+ // ── DEDUP ─────────────────────────────────────────────────────
72
+ const handledPushes = new Map();
42
73
 
43
- // ── Deduplication ─────────────────────────────────────────────
44
- const _handledByFcm = new Set();
45
- const DEDUP_TTL_MS = 60_000;
74
+ function cleanupHandledPushes() {
75
+ const now = Date.now();
46
76
 
47
- function markHandledByFcm(id) {
48
- if (!id) return;
49
- _handledByFcm.add(id);
50
- // console.log('[SW] FCM handled:', id);
51
- setTimeout(() => _handledByFcm.delete(id), DEDUP_TTL_MS);
77
+ for (const [key, ts] of handledPushes.entries()) {
78
+ if (now - ts > DEDUP_TTL) {
79
+ handledPushes.delete(key);
80
+ }
81
+ }
52
82
  }
53
83
 
54
- function wasHandledByFcm(id) {
55
- return id ? _handledByFcm.has(id) : false;
84
+ function rememberPush(key) {
85
+ if (!key) return;
86
+ cleanupHandledPushes();
87
+ handledPushes.set(key, Date.now());
88
+ log('DEDUP remember:', key);
56
89
  }
57
90
 
58
- function buildDedupeKey(payload = {}) {
59
- const data = payload.data || {};
60
- const notification = payload.notification || {};
61
- const explicitMessageId =
62
- payload.messageId ||
63
- data.msgID ||
64
- data.messageId ||
65
- data.message_id ||
66
- '';
91
+ function wasHandled(key) {
92
+ if (!key) return false;
93
+ cleanupHandledPushes();
67
94
 
68
- const roomJid = data.jid || payload.roomJid || '';
69
- const senderId = data.userJid || payload.senderId || '';
70
- const title = notification.title || payload.title || '';
71
- const body = notification.body || payload.body || payload.message || '';
95
+ const hit = handledPushes.has(key);
96
+ if (hit) log('DEDUP hit:', key);
72
97
 
73
- return explicitMessageId || `${roomJid}|${senderId}|${title}|${body}`;
98
+ return hit;
74
99
  }
75
100
 
76
- // ── Helpers ───────────────────────────────────────────────────
77
- function isSystemPayload(data) {
78
- return !data?.msgID && !data?.jid && !!data?.userJid;
101
+ // ── HELPERS ───────────────────────────────────────────────────
102
+ function isVisibleClients(clientsList) {
103
+ return clientsList.some((c) => {
104
+ try {
105
+ return c.visibilityState === 'visible';
106
+ } catch {
107
+ return false;
108
+ }
109
+ });
79
110
  }
80
111
 
81
- function buildTargetUrl(data, notification, fallbackUrl) {
82
- if (fallbackUrl && String(fallbackUrl).startsWith('http')) return fallbackUrl;
83
-
84
- const title = notification?.title || data?.title || '';
85
- if (title.startsWith('http')) return title;
86
-
87
- if (isSystemPayload(data)) return `${APP_URL}/?fromPush=1`;
88
- if (!data?.jid) return `${APP_URL}/?fromPush=1`;
112
+ function dumpClients(clientsList) {
113
+ return clientsList.map((c) => ({
114
+ url: c.url,
115
+ visibility: c.visibilityState,
116
+ }));
117
+ }
89
118
 
90
- const chatId = String(data.jid).split('@')[0];
91
- const messageId = data.msgID || data.messageId || data.message_id || '';
119
+ function safeJsonParse(text) {
120
+ try {
121
+ return JSON.parse(text);
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
92
126
 
93
- const params = new URLSearchParams({ chatId });
94
- if (messageId) params.set('messageId', messageId);
95
- params.set('fromPush', '1');
127
+ function stableStringify(value) {
128
+ try {
129
+ if (value === null || value === undefined) return '';
130
+ if (typeof value !== 'object') return String(value);
96
131
 
97
- return `${APP_URL}/chat?${params.toString()}`;
98
- }
132
+ if (Array.isArray(value)) {
133
+ return `[${value.map((v) => stableStringify(v)).join(',')}]`;
134
+ }
99
135
 
100
- function isAnyClientVisible(windowClients) {
101
- return windowClients.some((c) => c.visibilityState === 'visible');
136
+ const keys = Object.keys(value).sort();
137
+ return `{${keys
138
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
139
+ .join(',')}}`;
140
+ } catch {
141
+ return '';
142
+ }
102
143
  }
103
144
 
104
- function postBridgeToClients(windowClients, payload) {
105
- windowClients.forEach((client) => {
106
- client.postMessage({
107
- type: 'PUSH_FOREGROUND_BRIDGE',
108
- payload,
109
- });
110
- });
111
- }
145
+ function simpleHash(input) {
146
+ const str = String(input || '');
147
+ let hash = 0;
112
148
 
113
- function normalizeRawPushPayload(event) {
114
- if (!event.data) {
115
- // console.warn('[SW][EMPTY_PUSH] Push without payload');
116
- return {
117
- sourceType: 'empty',
118
- raw: {
119
- title: 'New message',
120
- body: '',
121
- data: {},
122
- },
123
- };
149
+ for (let i = 0; i < str.length; i += 1) {
150
+ hash = (hash << 5) - hash + str.charCodeAt(i);
151
+ hash |= 0;
124
152
  }
125
153
 
126
- let textPayload = '';
154
+ return String(hash);
155
+ }
156
+
157
+ function normalizeUrl(url) {
158
+ if (!url) return APP_URL;
127
159
 
128
160
  try {
129
- textPayload = event.data.text();
130
- } catch (err) {
131
- // console.warn('[SW][PUSH_TEXT_READ_FAILED]', err);
161
+ return new URL(url, self.location.origin).toString();
162
+ } catch {
163
+ return APP_URL;
132
164
  }
165
+ }
133
166
 
134
- if (!textPayload) {
135
- // console.warn('[SW][EMPTY_PUSH_TEXT] Push payload text is empty');
136
- return {
137
- sourceType: 'empty',
138
- raw: {
139
- title: 'New message',
140
- body: '',
141
- data: {},
142
- },
143
- };
144
- }
167
+ function pickTitle(raw = {}, data = {}, notification = {}) {
168
+ return notification.title || raw.title || data.title || 'New Message';
169
+ }
145
170
 
146
- try {
147
- const parsed = JSON.parse(textPayload);
148
- // console.log('[SW][JSON_PUSH]', parsed);
149
- return {
150
- sourceType: 'json',
151
- raw: parsed,
152
- };
153
- } catch (err) {
154
- /* console.warn('[SW][TEXT_PUSH] Non-JSON push received, processing as plain text', {
155
- error: err,
156
- rawText: textPayload,
157
- });*/
158
-
159
- return {
160
- sourceType: 'text',
161
- raw: {
162
- title: 'New message',
163
- body: textPayload,
164
- data: {},
165
- },
166
- };
167
- }
171
+ function pickBody(raw = {}, data = {}, notification = {}) {
172
+ return notification.body || raw.body || data.body || 'You have a new message';
168
173
  }
169
174
 
170
- // ── FCM ───────────────────────────────────────────────────────
171
- if (messaging) {
172
- messaging.onBackgroundMessage((payload) => {
173
- // console.log('[SW][FCM_PUSH]', payload);
175
+ function pickUrl(raw = {}, data = {}, notification = {}) {
176
+ return normalizeUrl(
177
+ data.url ||
178
+ data.link ||
179
+ data.click_action ||
180
+ notification.click_action ||
181
+ raw.url ||
182
+ raw.link ||
183
+ raw.click_action ||
184
+ APP_URL
185
+ );
186
+ }
174
187
 
175
- const data = payload.data || {};
176
- const notification = payload.notification || {};
177
- const messageId = payload.messageId || data.msgID || null;
178
- const dedupeKey = buildDedupeKey({
179
- messageId,
180
- data,
181
- notification,
182
- });
188
+ function pickIcon(raw = {}, data = {}, notification = {}) {
189
+ return data.icon || notification.icon || raw.icon || DEFAULT_ICON;
190
+ }
183
191
 
184
- markHandledByFcm(messageId);
185
- markHandledByFcm(dedupeKey);
192
+ function pickBadge(raw = {}, data = {}, notification = {}) {
193
+ return data.badge || notification.badge || raw.badge || DEFAULT_BADGE;
194
+ }
186
195
 
187
- const isSystem = isSystemPayload(data);
188
- const title =
189
- (isSystem ? 'System' : undefined) ||
190
- notification.title ||
191
- data.title ||
192
- 'New Message';
196
+ function pickImage(raw = {}, data = {}, notification = {}) {
197
+ return data.image || notification.image || raw.image || undefined;
198
+ }
193
199
 
194
- const body = notification.body || data.body || 'You have a new message.';
200
+ function extractMessageId(raw = {}, data = {}) {
201
+ return (
202
+ raw.messageId ||
203
+ raw.fcmMessageId ||
204
+ raw.fcmMessageId ||
205
+ raw.message_id ||
206
+ data.messageId ||
207
+ data.fcmMessageId ||
208
+ data.message_id ||
209
+ data.msgID ||
210
+ data['gcm.message_id'] ||
211
+ data['google.message_id'] ||
212
+ data['google.c.a.c_id'] ||
213
+ null
214
+ );
215
+ }
195
216
 
196
- const url = buildTargetUrl(data, notification, data.url);
217
+ function hasNotificationPayload(raw = {}, notification = {}) {
218
+ return Boolean(
219
+ (notification && (notification.title || notification.body)) ||
220
+ (raw.notification && (raw.notification.title || raw.notification.body))
221
+ );
222
+ }
197
223
 
198
- const bridgePayload = {
199
- messageId,
200
- notification: {
201
- title,
202
- body,
203
- image: notification.image || notification.icon || '/favicon.ico',
204
- },
205
- data: {
206
- ...data,
207
- jid: data.jid || '',
208
- userJid: data.userJid || '',
209
- msgID: data.msgID || '',
210
- workspaceId: data.workspaceId || data.workspace_id || '',
211
- },
212
- meta: {
213
- source: 'fcm',
214
- },
215
- };
216
-
217
- const options = {
218
- body,
219
- icon: notification.icon || notification.image || '/favicon.ico',
220
- badge: '/favicon.ico',
221
- tag: data.tag || 'ethora-notification',
222
- data: {
223
- url,
224
- roomJid: data.jid || null,
225
- senderId: data.userJid || null,
226
- messageId,
227
- workspaceId: data.workspaceId || data.workspace_id || null,
228
- },
229
- requireInteraction: false,
230
- silent: false,
231
- };
232
-
233
- return clients
234
- .matchAll({ type: 'window', includeUncontrolled: true })
235
- .then((windowClients) => {
236
- postBridgeToClients(windowClients, bridgePayload);
237
-
238
- if (!isAnyClientVisible(windowClients)) {
239
- /* console.log(
240
- `[NotifyPolicy] source=fcm_bg action=show reason=${isSystem ? 'system' : 'background'} msgId=${messageId || ''}`
241
- );*/
242
- return self.registration.showNotification(title, options);
243
- }
244
-
245
- /* console.log(
246
- `[NotifyPolicy] source=fcm_bg action=skip reason=tab_visible msgId=${messageId || ''}`
247
- );*/
248
- });
224
+ function makeDedupKey(raw = {}, data = {}, notification = {}) {
225
+ const messageId = extractMessageId(raw, data);
226
+ if (messageId) return `id:${messageId}`;
227
+
228
+ const title = pickTitle(raw, data, notification);
229
+ const body = pickBody(raw, data, notification);
230
+ const url = pickUrl(raw, data, notification);
231
+
232
+ const canonical = stableStringify({
233
+ title,
234
+ body,
235
+ url,
236
+ data,
237
+ notification,
249
238
  });
239
+
240
+ return `fp:${simpleHash(canonical)}`;
250
241
  }
251
242
 
252
- // ── PUSH (WebPush / text / fallback) ──────────────────────────
253
- self.addEventListener('push', (event) => {
254
- // console.log('[SW] push event');
243
+ function normalizePush(event) {
244
+ if (!event.data) {
245
+ warn('EMPTY PUSH');
246
+ return { type: 'empty', raw: {}, text: '' };
247
+ }
255
248
 
256
- const { sourceType, raw } = normalizeRawPushPayload(event);
249
+ let text = '';
257
250
 
258
- if (!raw || typeof raw !== 'object') {
259
- console.error('[SW][BROKEN_PUSH]', raw);
260
- return;
251
+ try {
252
+ text = event.data.text();
253
+ log('PUSH TEXT', text);
254
+ } catch (e) {
255
+ warn('TEXT READ FAIL', e);
261
256
  }
262
257
 
263
- const notification = raw.notification || {};
264
- const payloadData = raw.data || {};
265
- const messageId =
266
- raw.messageId ||
267
- payloadData.msgID ||
268
- payloadData.messageId ||
269
- payloadData.message_id ||
270
- null;
271
- const dedupeKey = buildDedupeKey({
272
- messageId,
273
- data: payloadData,
274
- notification,
275
- title: raw.title,
276
- body: raw.body || raw.message,
277
- roomJid: raw.roomJid,
278
- senderId: raw.senderId,
279
- });
258
+ const json = safeJsonParse(text);
280
259
 
281
- if (wasHandledByFcm(messageId) || wasHandledByFcm(dedupeKey)) {
282
- // console.log(`[SW] push skipped – already handled by FCM (msgId=${messageId})`);
283
- return;
260
+ if (json) {
261
+ log('PUSH JSON', json);
262
+ return { type: 'json', raw: json, text };
284
263
  }
285
264
 
286
- markHandledByFcm(messageId);
287
- markHandledByFcm(dedupeKey);
265
+ warn('NOT JSON PUSH', text);
266
+
267
+ return {
268
+ type: 'text',
269
+ raw: {
270
+ title: 'New Message',
271
+ body: text,
272
+ data: {},
273
+ },
274
+ text,
275
+ };
276
+ }
277
+
278
+ async function broadcastDebugMessage(payload) {
279
+ try {
280
+ const windowClients = await clients.matchAll({
281
+ type: 'window',
282
+ includeUncontrolled: true,
283
+ });
288
284
 
289
- if (sourceType !== 'json') {
290
- /* console.log('[SW][NON_JSON_PUSH_PROCESSED]', {
291
- sourceType,
292
- raw,
293
- });*/
285
+ for (const c of windowClients) {
286
+ try {
287
+ c.postMessage({
288
+ type: 'PUSH_DEBUG',
289
+ payload,
290
+ });
291
+ } catch {}
292
+ }
293
+ } catch (e) {
294
+ warn('broadcastDebugMessage failed', e);
294
295
  }
296
+ }
297
+
298
+ // ── MAIN HANDLER ──────────────────────────────────────────────
299
+ async function handlePush(raw = {}, source = 'push') {
300
+ const data = raw.data || {};
301
+ const notification = raw.notification || {};
295
302
 
296
- if (!raw.from && !raw.fcmOptions) {
297
- // console.log('[SW][NON_FCM_PUSH]', raw);
303
+ const title = pickTitle(raw, data, notification);
304
+ const body = pickBody(raw, data, notification);
305
+ const url = pickUrl(raw, data, notification);
306
+ const icon = pickIcon(raw, data, notification);
307
+ const badge = pickBadge(raw, data, notification);
308
+ const image = pickImage(raw, data, notification);
309
+
310
+ const messageId = extractMessageId(raw, data);
311
+ const dedupKey = makeDedupKey(raw, data, notification);
312
+ const browserWillAutoShow = hasNotificationPayload(raw, notification);
313
+
314
+ log('--- PUSH DECISION ---');
315
+ log('source:', source);
316
+ log('messageId:', messageId);
317
+ log('dedupKey:', dedupKey);
318
+ log('title:', title);
319
+ log('body:', body);
320
+ log('url:', url);
321
+ log('icon:', icon);
322
+ log('badge:', badge);
323
+ log('browserWillAutoShow:', browserWillAutoShow);
324
+
325
+ if (wasHandled(dedupKey)) {
326
+ log('SKIP DEDUP');
327
+ return;
298
328
  }
299
329
 
300
- const isSystem = isSystemPayload(payloadData);
330
+ rememberPush(dedupKey);
301
331
 
302
- const title =
303
- (isSystem ? 'System' : undefined) ||
304
- notification.title ||
305
- raw.title ||
306
- 'Ethora Chat';
332
+ const windowClients = await clients.matchAll({
333
+ type: 'window',
334
+ includeUncontrolled: true,
335
+ });
307
336
 
308
- const body =
309
- notification.body ||
310
- raw.body ||
311
- raw.message ||
312
- payloadData.body ||
313
- 'You have a new message.';
337
+ const visible = isVisibleClients(windowClients);
314
338
 
315
- const icon =
316
- raw.icon || notification.icon || notification.image || '/favicon.ico';
339
+ log('clients:', dumpClients(windowClients));
340
+ log('visibleClientExists:', visible);
317
341
 
318
- const url = buildTargetUrl(
319
- payloadData,
342
+ await broadcastDebugMessage({
343
+ source,
344
+ raw,
345
+ data,
320
346
  notification,
321
- raw.url || raw.clickAction
322
- );
323
-
324
- const bridgePayload = {
347
+ title,
348
+ body,
349
+ url,
350
+ icon,
351
+ badge,
352
+ image,
325
353
  messageId,
326
- notification: {
327
- title,
328
- body,
329
- image: icon,
330
- },
331
- data: {
332
- ...payloadData,
333
- jid: raw.roomJid || payloadData.jid || '',
334
- userJid: raw.senderId || payloadData.userJid || '',
335
- msgID: messageId || '',
336
- workspaceId:
337
- raw.workspaceId ||
338
- payloadData.workspaceId ||
339
- payloadData.workspace_id ||
340
- '',
341
- },
342
- meta: {
343
- source: sourceType === 'json' ? 'webpush' : 'text_push',
344
- },
345
- };
354
+ dedupKey,
355
+ browserWillAutoShow,
356
+ });
346
357
 
358
+ if (browserWillAutoShow) {
359
+ log('SKIP MANUAL SHOW: browser/FCM should auto-display notification');
360
+ return;
361
+ }
362
+
363
+ // Data-only push: показуємо вручну
347
364
  const options = {
348
365
  body,
349
366
  icon,
350
- badge: raw.badge || '/favicon.ico',
351
- tag: raw.tag || 'ethora-notification',
367
+ badge,
368
+ image,
369
+ tag: dedupKey,
370
+ renotify: false,
352
371
  data: {
353
372
  url,
354
- roomJid: raw.roomJid || payloadData.jid || null,
355
- senderId: raw.senderId || payloadData.userJid || null,
356
373
  messageId,
357
- workspaceId:
358
- raw.workspaceId ||
359
- payloadData.workspaceId ||
360
- payloadData.workspace_id ||
361
- null,
362
- sourceType,
374
+ dedupKey,
375
+ source,
363
376
  },
364
- requireInteraction: false,
365
- silent: false,
366
377
  };
367
378
 
379
+ if (!visible) {
380
+ log('SHOW NOTIFICATION', {
381
+ reason: 'NO_VISIBLE_CLIENT',
382
+ tag: options.tag,
383
+ });
384
+
385
+ return self.registration.showNotification(title, options);
386
+ }
387
+
388
+ log('SKIP NOTIFICATION', {
389
+ reason: 'VISIBLE_CLIENT',
390
+ });
391
+ }
392
+
393
+ // ── RAW PUSH ONLY ─────────────────────────────────────────────
394
+ self.addEventListener('push', (event) => {
395
+ log('RAW PUSH EVENT');
396
+
368
397
  event.waitUntil(
369
- clients
370
- .matchAll({ type: 'window', includeUncontrolled: true })
371
- .then((windowClients) => {
372
- postBridgeToClients(windowClients, bridgePayload);
373
-
374
- if (!isAnyClientVisible(windowClients)) {
375
- /* console.log(
376
- `[NotifyPolicy] source=push_bg action=show type=${sourceType} msgId=${messageId || ''}`
377
- );*/
378
- return self.registration.showNotification(title, options);
379
- }
380
-
381
- /* console.log(
382
- `[NotifyPolicy] source=push_bg action=skip reason=tab_visible type=${sourceType} msgId=${messageId || ''}`
383
- );*/
384
- })
398
+ (async () => {
399
+ try {
400
+ const { type, raw } = normalizePush(event);
401
+
402
+ log('PARSED PUSH', {
403
+ type,
404
+ raw,
405
+ messageId: extractMessageId(raw, raw.data || {}),
406
+ });
407
+
408
+ return handlePush(raw, `raw_push_${type}`);
409
+ } catch (e) {
410
+ error('PUSH HANDLER ERROR', e);
411
+ }
412
+ })()
385
413
  );
386
414
  });
387
415
 
388
- // ── Notification Click ────────────────────────────────────────
416
+ // ── CLICK ─────────────────────────────────────────────────────
389
417
  self.addEventListener('notificationclick', (event) => {
390
- // console.log('[NotifyPolicy] source=push_bg action=click_start');
391
- event.notification.close();
418
+ log('NOTIFICATION CLICK', event.notification);
392
419
 
393
- const targetUrl =
394
- (event.notification.data && event.notification.data.url) || APP_URL;
420
+ event.notification.close();
395
421
 
396
- /* console.log('[NotifyPolicy] source=push_bg action=click_url', {
397
- url: targetUrl,
398
- });*/
422
+ const url = event.notification?.data?.url || APP_URL;
399
423
 
400
- async function handleClick() {
401
- try {
402
- const windowClients = await clients.matchAll({
424
+ event.waitUntil(
425
+ (async () => {
426
+ const allClients = await clients.matchAll({
403
427
  type: 'window',
404
428
  includeUncontrolled: true,
405
429
  });
406
430
 
407
- /* console.log('[NotifyPolicy] source=push_bg action=clients_found', {
408
- count: windowClients.length,
409
- });*/
410
-
411
- const clickPayload = {
412
- type: 'PUSH_NOTIFICATION_CLICK',
413
- data: event.notification.data || {},
414
- notification: {
415
- title: event.notification.title || '',
416
- body: event.notification.body || '',
417
- },
418
- };
419
-
420
- windowClients.forEach((client) => {
421
- client.postMessage(clickPayload);
422
- });
423
-
424
- for (const client of windowClients) {
425
- if (client.url === targetUrl && 'focus' in client) {
426
- /* console.log('[NotifyPolicy] source=push_bg action=focus_exact', {
427
- url: client.url,
428
- });*/
429
- return client.focus();
430
- }
431
- }
432
-
433
- for (const client of windowClients) {
431
+ for (const client of allClients) {
434
432
  try {
435
- const clientOrigin = new URL(client.url).origin;
436
- const swOrigin = self.location.origin;
437
-
438
- if (clientOrigin === swOrigin && 'focus' in client) {
439
- /* console.log(
440
- '[NotifyPolicy] source=push_bg action=focus_and_navigate',
441
- { from: client.url, to: targetUrl }
442
- );*/
443
-
444
- const focusedClient = await client.focus();
445
-
446
- if (focusedClient && 'navigate' in focusedClient) {
447
- return focusedClient.navigate(targetUrl);
448
- }
449
-
450
- return focusedClient;
433
+ if ('focus' in client) {
434
+ await client.focus();
435
+
436
+ try {
437
+ client.postMessage({
438
+ type: 'OPEN_FROM_PUSH',
439
+ url,
440
+ notificationData: event.notification?.data || {},
441
+ });
442
+ } catch {}
443
+
444
+ return;
451
445
  }
452
- } catch (_) {}
453
- }
454
-
455
- if (clients.openWindow) {
456
- /* console.log('[NotifyPolicy] source=push_bg action=open_new_window', {
457
- url: targetUrl,
458
- });*/
459
- return clients.openWindow(targetUrl);
446
+ } catch {}
460
447
  }
461
- } catch (err) {
462
- console.error('[NotifyPolicy] source=push_bg action=click_error', err);
463
448
 
464
- if (clients.openWindow) {
465
- return clients.openWindow(targetUrl);
466
- }
467
- }
468
- }
469
-
470
- event.waitUntil(handleClick());
449
+ return clients.openWindow(url);
450
+ })()
451
+ );
471
452
  });
472
453
 
473
- // ── Debug / Subscription ──────────────────────────────────────
474
- self.addEventListener('pushsubscriptionchange', (event) => {
475
- // console.warn('[SW][POSSIBLE_DROP] subscription changed');
476
- event.waitUntil(
477
- clients
478
- .matchAll({ type: 'window', includeUncontrolled: true })
479
- .then((allClients) => {
480
- allClients.forEach((client) => {
481
- client.postMessage({
482
- type: 'PUSH_SUBSCRIPTION_CHANGED',
483
- });
484
- });
485
- })
486
- );
454
+ // ── LIFECYCLE ─────────────────────────────────────────────────
455
+ self.addEventListener('install', (event) => {
456
+ log('SW INSTALLED');
457
+ event.waitUntil(self.skipWaiting());
487
458
  });
488
459
 
489
- // ── Lifecycle ─────────────────────────────────────────────────
490
460
  self.addEventListener('activate', (event) => {
491
- // console.warn('[SW] activated');
461
+ log('SW ACTIVATED');
492
462
  event.waitUntil(clients.claim());
493
463
  });
494
-
495
- self.addEventListener('install', (event) => {
496
- // console.log('[SW] installed');
497
- event.waitUntil(self.skipWaiting());
498
- });