@ethora/chat-component 25.12.1 → 26.1.1

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 (49) hide show
  1. package/README.md +587 -145
  2. package/dist/components/MainComponents/MessageContainer.d.ts +1 -0
  3. package/dist/components/MessageBubble/MessageReply.d.ts +2 -2
  4. package/dist/components/MessageNotification/MessageNotificationToast.d.ts +17 -0
  5. package/dist/components/MessageNotification/index.d.ts +2 -0
  6. package/dist/components/Modals/SettingsModals/SharedStyledComponents.d.ts +22 -0
  7. package/dist/components/Modals/styledModalComponents.d.ts +28 -0
  8. package/dist/components/styled/Button.d.ts +2 -2
  9. package/dist/components/styled/DateLabel.d.ts +2 -2
  10. package/dist/components/styled/StyledComponents.d.ts +10 -10
  11. package/dist/components/styled/StyledInputComponents/StyledInputComponents.d.ts +6 -6
  12. package/dist/context/MessageNotificationContext.d.ts +12 -0
  13. package/dist/firebase-config.d.ts +6 -2
  14. package/dist/firebase-messaging-sw.js +351 -0
  15. package/dist/helpers/getDataFromXml.d.ts +1 -0
  16. package/dist/helpers/initRoomsPresence.d.ts +1 -1
  17. package/dist/helpers/storeConsole.d.ts +6 -0
  18. package/dist/hooks/useInAppNotifications.d.ts +18 -0
  19. package/dist/hooks/usePushNotifications.d.ts +45 -0
  20. package/dist/hooks/useTabVisibility.d.ts +1 -0
  21. package/dist/main.css +1 -1
  22. package/dist/main.d.ts +5 -0
  23. package/dist/main.js +14698 -11894
  24. package/dist/main.js.map +1 -1
  25. package/dist/networking/api-requests/push.api.d.ts +18 -0
  26. package/dist/networking/stanzaHandlers.d.ts +4 -2
  27. package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +11 -1
  28. package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
  29. package/dist/networking/xmpp/sendTextMessage.xmpp.d.ts +1 -1
  30. package/dist/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts +1 -1
  31. package/dist/networking/xmpp/subscribeToRoomMessages.xmpp.d.ts +9 -0
  32. package/dist/networking/xmppClient.d.ts +13 -3
  33. package/dist/roomStore/chatSettingsSlice.d.ts +1 -0
  34. package/dist/roomStore/index.d.ts +28 -0
  35. package/dist/roomStore/roomHeapSlice.d.ts +1 -1
  36. package/dist/roomStore/roomsSlice.d.ts +11 -1
  37. package/dist/types/models/config.model.d.ts +36 -1
  38. package/dist/types/models/room.model.d.ts +4 -1
  39. package/dist/types/models/xmpp.model.d.ts +3 -1
  40. package/dist/utils/firebasePushNotifications.d.ts +34 -0
  41. package/dist/utils/formatError.d.ts +1 -0
  42. package/dist/utils/messageNotificationManager.d.ts +16 -0
  43. package/dist/utils/notificationPolicy.d.ts +45 -0
  44. package/dist/utils/notificationUtils.d.ts +22 -0
  45. package/dist/utils/pushSubscriptionService.d.ts +12 -0
  46. package/dist/version.d.ts +1 -1
  47. package/package.json +7 -4
  48. package/public/firebase-messaging-sw.js +351 -0
  49. package/scripts/init-push.js +48 -0
@@ -0,0 +1,351 @@
1
+ /* ============================================================
2
+ * Unified Service Worker
3
+ * Handles BOTH Firebase Cloud Messaging (FCM) background pushes
4
+ * AND generic Web Push API events, so all push notifications
5
+ * are captured regardless of origin.
6
+ * ============================================================ */
7
+
8
+ // ── Firebase SDK ──────────────────────────────────────────────
9
+ importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');
10
+ importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');
11
+
12
+ // ── Dynamic Configuration ─────────────────────────────────────
13
+ const params = new URL(self.location).searchParams;
14
+ const firebaseConfig = {
15
+ apiKey: params.get('apiKey'),
16
+ authDomain: params.get('authDomain'),
17
+ projectId: params.get('projectId'),
18
+ storageBucket: params.get('storageBucket'),
19
+ messagingSenderId: params.get('messagingSenderId'),
20
+ appId: params.get('appId'),
21
+ };
22
+
23
+ // Only initialize if we have at least apiKey and appId
24
+ if (firebaseConfig.apiKey && firebaseConfig.appId) {
25
+ firebase.initializeApp(firebaseConfig);
26
+ }
27
+
28
+ const messaging = firebase.messaging();
29
+ const APP_URL = self.location.origin;
30
+
31
+ // ── Deduplication ─────────────────────────────────────────────
32
+ // Track message IDs handled by FCM so the generic `push` listener
33
+ // can skip them and avoid duplicate notifications.
34
+ const _handledByFcm = new Set();
35
+ const DEDUP_TTL_MS = 60_000; // auto-cleanup after 60 s
36
+
37
+ function markHandledByFcm(id) {
38
+ if (!id) return;
39
+ _handledByFcm.add(id);
40
+ console.log('[SW] FCM message marked as handled:', id);
41
+ setTimeout(() => _handledByFcm.delete(id), DEDUP_TTL_MS);
42
+ }
43
+
44
+ function wasHandledByFcm(id) {
45
+ return id ? _handledByFcm.has(id) : false;
46
+ }
47
+
48
+ // ── Shared Helpers ────────────────────────────────────────────
49
+ function isSystemPayload(data) {
50
+ return !data?.msgID && !data?.jid && !!data?.userJid;
51
+ }
52
+
53
+ function buildTargetUrl(data, notification, fallbackUrl) {
54
+ if (fallbackUrl && String(fallbackUrl).startsWith('http')) return fallbackUrl;
55
+
56
+ const title = notification?.title || data?.title || '';
57
+ if (title.startsWith('http')) return title;
58
+
59
+ if (isSystemPayload(data)) return APP_URL;
60
+ if (!data?.jid) return APP_URL;
61
+ const chatId = String(data.jid).split('@')[0];
62
+ return `${APP_URL}/chat?chatId=${encodeURIComponent(chatId)}`;
63
+ }
64
+
65
+ function postBridgeToClients(windowClients, bridgePayload) {
66
+ windowClients.forEach((client) => {
67
+ client.postMessage({
68
+ type: 'PUSH_FOREGROUND_BRIDGE',
69
+ payload: bridgePayload,
70
+ });
71
+ });
72
+ }
73
+
74
+ function isAnyClientVisible(windowClients) {
75
+ return windowClients.some((c) => c.visibilityState === 'visible');
76
+ }
77
+
78
+ // ── 1. FCM Background Messages ───────────────────────────────
79
+ // This fires for pushes delivered through Firebase Cloud Messaging
80
+ // when no foreground tab is handling them via onMessage().
81
+ messaging.onBackgroundMessage((payload) => {
82
+ console.log('[SW] FCM onBackgroundMessage received', payload);
83
+
84
+ const data = payload.data || {};
85
+ const notification = payload.notification || {};
86
+ const messageId = payload.messageId || data.msgID || null;
87
+
88
+ // Mark so the generic push handler skips this one
89
+ markHandledByFcm(messageId);
90
+
91
+ const isSystem = isSystemPayload(data);
92
+ const title =
93
+ (isSystem ? 'System' : undefined) ||
94
+ notification.title ||
95
+ data.title ||
96
+ 'New Message';
97
+ const body =
98
+ notification.body || data.body || 'You have a new message.';
99
+ const url = buildTargetUrl(data, notification, data.url);
100
+
101
+ const bridgePayload = {
102
+ messageId,
103
+ notification: { title, body, image: notification.image || '/favicon.ico' },
104
+ data: {
105
+ ...data,
106
+ jid: data.jid || '',
107
+ userJid: data.userJid || '',
108
+ msgID: data.msgID || '',
109
+ workspaceId: data.workspaceId || data.workspace_id || '',
110
+ },
111
+ };
112
+
113
+ const options = {
114
+ body,
115
+ icon: notification.image || '/favicon.ico',
116
+ badge: '/favicon.ico',
117
+ tag: data.tag || 'ethora-notification',
118
+ data: {
119
+ url,
120
+ roomJid: data.jid || null,
121
+ senderId: data.userJid || null,
122
+ messageId,
123
+ workspaceId: data.workspaceId || data.workspace_id || null,
124
+ },
125
+ requireInteraction: false,
126
+ silent: false,
127
+ };
128
+
129
+ return clients
130
+ .matchAll({ type: 'window', includeUncontrolled: true })
131
+ .then((windowClients) => {
132
+ postBridgeToClients(windowClients, bridgePayload);
133
+
134
+ if (!isAnyClientVisible(windowClients)) {
135
+ console.log(
136
+ `[NotifyPolicy] source=fcm_bg action=show reason=${isSystem ? 'system' : 'background'} msgId=${messageId || ''}`
137
+ );
138
+ return self.registration.showNotification(title, options);
139
+ } else {
140
+ console.log(
141
+ `[NotifyPolicy] source=fcm_bg action=skip reason=tab_visible msgId=${messageId || ''}`
142
+ );
143
+ }
144
+ });
145
+ });
146
+
147
+ // ── 2. Generic Web Push API ───────────────────────────────────
148
+ // This fires for ANY push delivered via the Web Push protocol,
149
+ // including direct backend pushes for XMPP messages that are
150
+ // NOT routed through FCM.
151
+ self.addEventListener('push', (event) => {
152
+ console.log('[SW] Generic push event received');
153
+
154
+ let raw = {};
155
+ try {
156
+ if (event.data) {
157
+ raw = event.data.json();
158
+ }
159
+ } catch (err) {
160
+ console.warn('[SW] Failed to parse push payload as JSON:', err);
161
+ raw = { title: 'New message', body: event.data ? event.data.text() : '' };
162
+ }
163
+
164
+ const notification = raw.notification || {};
165
+ const payloadData = raw.data || {};
166
+ const messageId = raw.messageId || payloadData.msgID || null;
167
+
168
+ // Skip if FCM already handled this exact message
169
+ if (wasHandledByFcm(messageId)) {
170
+ console.log(`[SW] push event skipped – already handled by FCM (msgId=${messageId})`);
171
+ return;
172
+ }
173
+
174
+ const isSystem = isSystemPayload(payloadData);
175
+ const title =
176
+ (isSystem ? 'System' : undefined) ||
177
+ notification.title ||
178
+ raw.title ||
179
+ 'Ethora Chat';
180
+ const body =
181
+ notification.body ||
182
+ raw.body ||
183
+ raw.message ||
184
+ payloadData.body ||
185
+ 'You have a new message.';
186
+ const url = buildTargetUrl(
187
+ payloadData,
188
+ notification,
189
+ raw.url || raw.clickAction
190
+ );
191
+
192
+ const bridgePayload = {
193
+ messageId,
194
+ notification: {
195
+ title,
196
+ body,
197
+ image: raw.icon || '/favicon.ico',
198
+ },
199
+ data: {
200
+ ...payloadData,
201
+ jid: raw.roomJid || payloadData.jid || '',
202
+ userJid: raw.senderId || payloadData.userJid || '',
203
+ msgID: messageId || '',
204
+ workspaceId: raw.workspaceId || payloadData.workspaceId || payloadData.workspace_id || '',
205
+ },
206
+ };
207
+
208
+ const options = {
209
+ body,
210
+ icon: raw.icon || '/favicon.ico',
211
+ badge: raw.badge || '/favicon.ico',
212
+ tag: raw.tag || 'ethora-notification',
213
+ data: {
214
+ url,
215
+ roomJid: raw.roomJid || payloadData.jid || null,
216
+ senderId: raw.senderId || payloadData.userJid || null,
217
+ messageId,
218
+ workspaceId: raw.workspaceId || payloadData.workspaceId || payloadData.workspace_id || null,
219
+ },
220
+ requireInteraction: false,
221
+ silent: false,
222
+ };
223
+
224
+ event.waitUntil(
225
+ clients
226
+ .matchAll({ type: 'window', includeUncontrolled: true })
227
+ .then((windowClients) => {
228
+ postBridgeToClients(windowClients, bridgePayload);
229
+
230
+ if (!isAnyClientVisible(windowClients)) {
231
+ console.log(
232
+ `[NotifyPolicy] source=push_bg action=show reason=${isSystem ? 'system' : 'background'} msgId=${messageId || ''}`
233
+ );
234
+ return self.registration.showNotification(title, options);
235
+ } else {
236
+ console.log(
237
+ `[NotifyPolicy] source=push_bg action=skip reason=tab_visible msgId=${messageId || ''}`
238
+ );
239
+ }
240
+ })
241
+ );
242
+ });
243
+
244
+ // ── 3. Notification Click ─────────────────────────────────────
245
+ self.addEventListener('notificationclick', (event) => {
246
+ console.log('[NotifyPolicy] source=push_bg action=click_start');
247
+ event.notification.close();
248
+
249
+ const targetUrl =
250
+ (event.notification.data && event.notification.data.url) || APP_URL;
251
+ console.log('[NotifyPolicy] source=push_bg action=click_url', {
252
+ url: targetUrl,
253
+ });
254
+
255
+ async function handleClick() {
256
+ try {
257
+ const windowClients = await clients.matchAll({
258
+ type: 'window',
259
+ includeUncontrolled: true,
260
+ });
261
+ console.log('[NotifyPolicy] source=push_bg action=clients_found', {
262
+ count: windowClients.length,
263
+ });
264
+
265
+ // 1. Try to find an exactly matching URL
266
+ for (const client of windowClients) {
267
+ if (client.url === targetUrl && 'focus' in client) {
268
+ console.log('[NotifyPolicy] source=push_bg action=focus_exact', {
269
+ url: client.url,
270
+ });
271
+ return client.focus();
272
+ }
273
+ }
274
+
275
+ // 2. Try to find any client on our origin, focus it, and navigate
276
+ for (const client of windowClients) {
277
+ try {
278
+ const clientOrigin = new URL(client.url).origin;
279
+ const swOrigin = self.location.origin;
280
+ if (clientOrigin === swOrigin && 'focus' in client) {
281
+ console.log(
282
+ '[NotifyPolicy] source=push_bg action=focus_and_navigate',
283
+ { from: client.url, to: targetUrl }
284
+ );
285
+ const focusedClient = await client.focus();
286
+ if (focusedClient && 'navigate' in focusedClient) {
287
+ return focusedClient.navigate(targetUrl);
288
+ }
289
+ }
290
+ } catch (e) {
291
+ // Ignore invalid URLs
292
+ }
293
+ }
294
+
295
+ // 3. Fallback: Open a new window
296
+ if (clients.openWindow) {
297
+ console.log('[NotifyPolicy] source=push_bg action=open_new_window', {
298
+ url: targetUrl,
299
+ });
300
+ return clients.openWindow(targetUrl);
301
+ }
302
+ } catch (err) {
303
+ console.error('[NotifyPolicy] source=push_bg action=click_error', err);
304
+ if (clients.openWindow) {
305
+ return clients.openWindow(targetUrl);
306
+ }
307
+ }
308
+ }
309
+
310
+ event.waitUntil(handleClick());
311
+ });
312
+
313
+ // ── 4. Push Subscription Change ───────────────────────────────
314
+ // Fired when the browser automatically re-creates a subscription
315
+ // (e.g. after expiry). Notify clients so they can re-register.
316
+ self.addEventListener('pushsubscriptionchange', (event) => {
317
+ console.log('[SW] Push subscription changed – re-subscribing');
318
+
319
+ event.waitUntil(
320
+ self.registration.pushManager
321
+ .subscribe({ userVisibleOnly: true })
322
+ .then((subscription) => {
323
+ console.log(
324
+ '[SW] New subscription obtained:',
325
+ subscription.endpoint
326
+ );
327
+ return clients.matchAll().then((allClients) => {
328
+ allClients.forEach((client) => {
329
+ client.postMessage({
330
+ type: 'PUSH_SUBSCRIPTION_CHANGED',
331
+ subscription: subscription.toJSON(),
332
+ });
333
+ });
334
+ });
335
+ })
336
+ .catch((err) => {
337
+ console.error('[SW] pushsubscriptionchange – failed to re-subscribe:', err);
338
+ })
339
+ );
340
+ });
341
+
342
+ // ── 5. Lifecycle ──────────────────────────────────────────────
343
+ self.addEventListener('activate', (event) => {
344
+ console.log('[SW] Service worker activated');
345
+ event.waitUntil(clients.claim());
346
+ });
347
+
348
+ self.addEventListener('install', () => {
349
+ console.log('[SW] Service worker installed');
350
+ self.skipWaiting();
351
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function findPublicDir(startPath) {
7
+ const possiblePaths = ['public', 'static', 'assets'];
8
+ let currentPath = startPath;
9
+
10
+ while (currentPath !== path.parse(currentPath).root) {
11
+ for (const dir of possiblePaths) {
12
+ const fullPath = path.join(currentPath, dir);
13
+ if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()) {
14
+ return fullPath;
15
+ }
16
+ }
17
+ currentPath = path.dirname(currentPath);
18
+ }
19
+ return null;
20
+ }
21
+
22
+ const swFileName = 'firebase-messaging-sw.js';
23
+ const sourcePath = path.join(__dirname, '..', 'public', swFileName);
24
+
25
+ if (!fs.existsSync(sourcePath)) {
26
+ console.error(`[Ethora] Error: Source service worker not found at ${sourcePath}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ // When run via npx, process.cwd() is the user's project root
31
+ const targetDir = findPublicDir(process.cwd());
32
+
33
+ if (!targetDir) {
34
+ console.error('[Ethora] Error: Could not find a public directory in your project.');
35
+ console.info('Please manually copy the service worker:');
36
+ console.info(`cp node_modules/@ethora/chat-component/public/${swFileName} ./public/`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const targetPath = path.join(targetDir, swFileName);
41
+
42
+ try {
43
+ fs.copyFileSync(sourcePath, targetPath);
44
+ console.log(`[Ethora] ✅ Successfully copied ${swFileName} to ${targetDir}`);
45
+ } catch (err) {
46
+ console.error(`[Ethora] Error: Failed to copy service worker: ${err.message}`);
47
+ process.exit(1);
48
+ }