@ethora/chat-component 26.2.22 → 26.2.26

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.
@@ -15,4 +15,4 @@ export declare function registerPushToken(registrationToken: string): Promise<an
15
15
  *
16
16
  * @param registrationToken – The token to unregister.
17
17
  */
18
- export declare function unregisterPushToken(registrationToken: string): Promise<any>;
18
+ export declare function unregisterPushToken(registrationToken: string, authToken?: string): Promise<any>;
@@ -65,10 +65,12 @@ export declare class XmppClient implements XmppClientInterface {
65
65
  private reconnectBaseDelayMs;
66
66
  private pausedDueToOfflineCap;
67
67
  private authFailureDetected;
68
+ private intentionalLogout;
68
69
  private isTerminalAuthFailure;
69
70
  constructor(username: string, password: string, xmppSettings?: xmppSettingsInterface);
70
71
  initializeClient(): Promise<void>;
71
72
  disconnect(): Promise<void>;
73
+ markIntentionalLogout: () => void;
72
74
  attachEventListeners(): void;
73
75
  private scheduleAdaptivePing;
74
76
  private startAdaptivePing;
@@ -5,6 +5,7 @@ export interface IMessage {
5
5
  id: string;
6
6
  user: IUser;
7
7
  date: Date | string;
8
+ messageTimestampMs?: number;
8
9
  body: string;
9
10
  roomJid: string;
10
11
  key?: string;
@@ -36,5 +36,5 @@ export declare function listenForForegroundMessages(handler: (payload: MessagePa
36
36
  /**
37
37
  * disablePushNotifications()
38
38
  */
39
- export declare function disablePushNotifications(): Promise<void>;
39
+ export declare function disablePushNotifications(_authToken?: string): Promise<void>;
40
40
  export {};
@@ -0,0 +1,3 @@
1
+ export declare const FCM_TOKEN_STORAGE_KEY = "@ethora/chat-component:fcmToken";
2
+ export declare const getStoredFcmToken: () => string;
3
+ export declare const setStoredFcmToken: (token: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component",
3
- "version": "26.2.22",
3
+ "version": "26.2.26",
4
4
  "type": "module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -1,13 +1,6 @@
1
- /* eslint-disable no-empty */
1
+ /* eslint-disable no-empty, no-restricted-globals */
2
2
  /* eslint-env worker */
3
- /* global firebase, clients, importScripts */
4
-
5
- importScripts(
6
- 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js'
7
- );
8
- importScripts(
9
- 'https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js'
10
- );
3
+ /* global clients */
11
4
 
12
5
  // ── DEBUG ─────────────────────────────────────────────────────
13
6
  const DEBUG = false;
@@ -16,8 +9,15 @@ const DEDUP_TTL = 60 * 1000;
16
9
  const params = new URL(self.location.href).searchParams;
17
10
  const CUSTOM_ICON = (params.get('iconPath') || '').trim();
18
11
  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';
12
+ const DEFAULT_ICON = `${self.location.origin}/favicon-192.png`;
13
+ const DEFAULT_BADGE = `${self.location.origin}/favicon-192.png`;
14
+
15
+ log('SW icon config', {
16
+ customIcon: CUSTOM_ICON || null,
17
+ customBadge: CUSTOM_BADGE || null,
18
+ defaultIcon: DEFAULT_ICON,
19
+ defaultBadge: DEFAULT_BADGE,
20
+ });
21
21
 
22
22
  // ── LOGGING ───────────────────────────────────────────────────
23
23
  function log(...args) {
@@ -32,42 +32,8 @@ function error(...args) {
32
32
  console.error('[SW]', ...args);
33
33
  }
34
34
 
35
- // ── CONFIG ────────────────────────────────────────────────────
36
- const firebaseConfig = {
37
- apiKey: params.get('apiKey'),
38
- authDomain: params.get('authDomain'),
39
- projectId: params.get('projectId'),
40
- storageBucket: params.get('storageBucket'),
41
- messagingSenderId: params.get('messagingSenderId'),
42
- appId: params.get('appId'),
43
- };
44
-
45
35
  const APP_URL = self.location.origin;
46
36
 
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
- }
61
-
62
- firebase.messaging();
63
- log('Firebase messaging ready (without onBackgroundMessage)');
64
- } else {
65
- warn('Firebase config missing', firebaseConfig);
66
- }
67
- } catch (e) {
68
- error('Firebase init error', e);
69
- }
70
-
71
37
  // ── DEDUP ─────────────────────────────────────────────────────
72
38
  const handledPushes = new Map();
73
39
 
@@ -110,6 +76,7 @@ function isVisibleClients(clientsList) {
110
76
  }
111
77
 
112
78
  function dumpClients(clientsList) {
79
+ if (!DEBUG) return [];
113
80
  return clientsList.map((c) => ({
114
81
  url: c.url,
115
82
  visibility: c.visibilityState,
@@ -164,6 +131,26 @@ function normalizeUrl(url) {
164
131
  }
165
132
  }
166
133
 
134
+ async function checkAssetAvailability(url) {
135
+ if (!DEBUG) return undefined;
136
+ if (!url) return { ok: false, status: null, error: 'empty_url' };
137
+
138
+ try {
139
+ const response = await fetch(url, { method: 'HEAD', cache: 'no-store' });
140
+ return {
141
+ ok: response.ok,
142
+ status: response.status,
143
+ resolvedUrl: response.url || url,
144
+ };
145
+ } catch (e) {
146
+ return {
147
+ ok: false,
148
+ status: null,
149
+ error: String(e?.message || e || 'fetch_failed'),
150
+ };
151
+ }
152
+ }
153
+
167
154
  function pickTitle(raw = {}, data = {}, notification = {}) {
168
155
  return notification.title || raw.title || data.title || 'New Message';
169
156
  }
@@ -185,12 +172,12 @@ function pickUrl(raw = {}, data = {}, notification = {}) {
185
172
  );
186
173
  }
187
174
 
188
- function pickIcon(raw = {}, data = {}, notification = {}) {
189
- return data.icon || notification.icon || raw.icon || DEFAULT_ICON;
175
+ function pickIcon() {
176
+ return CUSTOM_ICON || DEFAULT_ICON;
190
177
  }
191
178
 
192
- function pickBadge(raw = {}, data = {}, notification = {}) {
193
- return data.badge || notification.badge || raw.badge || DEFAULT_BADGE;
179
+ function pickBadge() {
180
+ return CUSTOM_BADGE || DEFAULT_BADGE;
194
181
  }
195
182
 
196
183
  function pickImage(raw = {}, data = {}, notification = {}) {
@@ -214,13 +201,6 @@ function extractMessageId(raw = {}, data = {}) {
214
201
  );
215
202
  }
216
203
 
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
- }
223
-
224
204
  function makeDedupKey(raw = {}, data = {}, notification = {}) {
225
205
  const messageId = extractMessageId(raw, data);
226
206
  if (messageId) return `id:${messageId}`;
@@ -276,6 +256,7 @@ function normalizePush(event) {
276
256
  }
277
257
 
278
258
  async function broadcastDebugMessage(payload) {
259
+ if (!DEBUG) return;
279
260
  try {
280
261
  const windowClients = await clients.matchAll({
281
262
  type: 'window',
@@ -303,13 +284,33 @@ async function handlePush(raw = {}, source = 'push') {
303
284
  const title = pickTitle(raw, data, notification);
304
285
  const body = pickBody(raw, data, notification);
305
286
  const url = pickUrl(raw, data, notification);
306
- const icon = pickIcon(raw, data, notification);
307
- const badge = pickBadge(raw, data, notification);
287
+ const icon = pickIcon();
288
+ const badge = pickBadge();
308
289
  const image = pickImage(raw, data, notification);
309
290
 
310
291
  const messageId = extractMessageId(raw, data);
311
292
  const dedupKey = makeDedupKey(raw, data, notification);
312
- const browserWillAutoShow = hasNotificationPayload(raw, notification);
293
+ const iconAvailability = DEBUG
294
+ ? await checkAssetAvailability(icon)
295
+ : undefined;
296
+ const badgeAvailability = DEBUG
297
+ ? await checkAssetAvailability(badge)
298
+ : undefined;
299
+ const iconDebug = DEBUG
300
+ ? {
301
+ selectedIcon: icon,
302
+ selectedBadge: badge,
303
+ selectedImage: image || null,
304
+ customIcon: CUSTOM_ICON || null,
305
+ customBadge: CUSTOM_BADGE || null,
306
+ defaultIcon: DEFAULT_ICON,
307
+ defaultBadge: DEFAULT_BADGE,
308
+ payloadIcon: data.icon || notification.icon || raw.icon || null,
309
+ payloadBadge: data.badge || notification.badge || raw.badge || null,
310
+ iconAvailability,
311
+ badgeAvailability,
312
+ }
313
+ : undefined;
313
314
 
314
315
  log('--- PUSH DECISION ---');
315
316
  log('source:', source);
@@ -320,7 +321,7 @@ async function handlePush(raw = {}, source = 'push') {
320
321
  log('url:', url);
321
322
  log('icon:', icon);
322
323
  log('badge:', badge);
323
- log('browserWillAutoShow:', browserWillAutoShow);
324
+ if (iconDebug) log('iconDebug:', iconDebug);
324
325
 
325
326
  if (wasHandled(dedupKey)) {
326
327
  log('SKIP DEDUP');
@@ -336,7 +337,7 @@ async function handlePush(raw = {}, source = 'push') {
336
337
 
337
338
  const visible = isVisibleClients(windowClients);
338
339
 
339
- log('clients:', dumpClients(windowClients));
340
+ if (DEBUG) log('clients:', dumpClients(windowClients));
340
341
  log('visibleClientExists:', visible);
341
342
 
342
343
  await broadcastDebugMessage({
@@ -350,17 +351,11 @@ async function handlePush(raw = {}, source = 'push') {
350
351
  icon,
351
352
  badge,
352
353
  image,
354
+ iconDebug,
353
355
  messageId,
354
356
  dedupKey,
355
- browserWillAutoShow,
356
357
  });
357
358
 
358
- if (browserWillAutoShow) {
359
- log('SKIP MANUAL SHOW: browser/FCM should auto-display notification');
360
- return;
361
- }
362
-
363
- // Data-only push: показуємо вручну
364
359
  const options = {
365
360
  body,
366
361
  icon,
@@ -393,6 +388,7 @@ async function handlePush(raw = {}, source = 'push') {
393
388
  // ── RAW PUSH ONLY ─────────────────────────────────────────────
394
389
  self.addEventListener('push', (event) => {
395
390
  log('RAW PUSH EVENT');
391
+ event.stopImmediatePropagation?.();
396
392
 
397
393
  event.waitUntil(
398
394
  (async () => {
@@ -427,26 +423,69 @@ self.addEventListener('notificationclick', (event) => {
427
423
  type: 'window',
428
424
  includeUncontrolled: true,
429
425
  });
426
+ const targetUrl = normalizeUrl(url);
427
+
428
+ const payload = {
429
+ data: event.notification?.data || {},
430
+ notification: {
431
+ title: event.notification?.title || '',
432
+ body: event.notification?.body || '',
433
+ },
434
+ };
435
+
436
+ const preferredClient =
437
+ allClients.find((client) => {
438
+ try {
439
+ return client.url.startsWith(self.location.origin);
440
+ } catch {
441
+ return false;
442
+ }
443
+ }) || allClients[0];
430
444
 
431
- for (const client of allClients) {
445
+ if (preferredClient) {
432
446
  try {
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;
447
+ if ('navigate' in preferredClient) {
448
+ await preferredClient.navigate(targetUrl);
445
449
  }
450
+
451
+ if ('focus' in preferredClient) {
452
+ await preferredClient.focus();
453
+ }
454
+
455
+ try {
456
+ preferredClient.postMessage({
457
+ type: 'PUSH_NOTIFICATION_CLICK',
458
+ ...payload,
459
+ });
460
+ } catch {}
461
+
462
+ // Backward compatibility for any legacy listeners in app code
463
+ try {
464
+ preferredClient.postMessage({
465
+ type: 'OPEN_FROM_PUSH',
466
+ url: targetUrl,
467
+ notificationData: event.notification?.data || {},
468
+ });
469
+ } catch {}
470
+
471
+ return;
472
+ } catch (e) {
473
+ warn('notificationclick client handling failed', e);
474
+ }
475
+ }
476
+
477
+ const opened = await clients.openWindow(targetUrl);
478
+
479
+ if (opened) {
480
+ try {
481
+ opened.postMessage({
482
+ type: 'PUSH_NOTIFICATION_CLICK',
483
+ ...payload,
484
+ });
446
485
  } catch {}
447
486
  }
448
487
 
449
- return clients.openWindow(url);
488
+ return opened;
450
489
  })()
451
490
  );
452
491
  });