@ethora/chat-component 25.12.2 → 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.
- package/README.md +587 -145
- package/dist/components/MainComponents/MessageContainer.d.ts +1 -0
- package/dist/components/MessageBubble/MessageReply.d.ts +2 -2
- package/dist/components/MessageNotification/MessageNotificationToast.d.ts +17 -0
- package/dist/components/MessageNotification/index.d.ts +2 -0
- package/dist/components/Modals/SettingsModals/SharedStyledComponents.d.ts +22 -0
- package/dist/components/Modals/styledModalComponents.d.ts +28 -0
- package/dist/components/styled/Button.d.ts +2 -2
- package/dist/components/styled/DateLabel.d.ts +2 -2
- package/dist/components/styled/StyledComponents.d.ts +10 -10
- package/dist/components/styled/StyledInputComponents/StyledInputComponents.d.ts +6 -6
- package/dist/context/MessageNotificationContext.d.ts +12 -0
- package/dist/firebase-config.d.ts +6 -2
- package/dist/firebase-messaging-sw.js +351 -0
- package/dist/helpers/getDataFromXml.d.ts +1 -0
- package/dist/helpers/initRoomsPresence.d.ts +1 -1
- package/dist/helpers/storeConsole.d.ts +6 -0
- package/dist/hooks/useInAppNotifications.d.ts +18 -0
- package/dist/hooks/usePushNotifications.d.ts +45 -0
- package/dist/hooks/useTabVisibility.d.ts +1 -0
- package/dist/main.css +1 -1
- package/dist/main.d.ts +5 -0
- package/dist/main.js +14686 -11893
- package/dist/main.js.map +1 -1
- package/dist/networking/api-requests/push.api.d.ts +18 -0
- package/dist/networking/stanzaHandlers.d.ts +4 -2
- package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +11 -1
- package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
- package/dist/networking/xmpp/sendTextMessage.xmpp.d.ts +1 -1
- package/dist/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts +1 -1
- package/dist/networking/xmpp/subscribeToRoomMessages.xmpp.d.ts +9 -0
- package/dist/networking/xmppClient.d.ts +13 -3
- package/dist/roomStore/chatSettingsSlice.d.ts +1 -0
- package/dist/roomStore/index.d.ts +28 -0
- package/dist/roomStore/roomHeapSlice.d.ts +1 -1
- package/dist/roomStore/roomsSlice.d.ts +11 -1
- package/dist/types/models/config.model.d.ts +35 -1
- package/dist/types/models/room.model.d.ts +4 -1
- package/dist/types/models/xmpp.model.d.ts +3 -1
- package/dist/utils/firebasePushNotifications.d.ts +34 -0
- package/dist/utils/formatError.d.ts +1 -0
- package/dist/utils/messageNotificationManager.d.ts +16 -0
- package/dist/utils/notificationPolicy.d.ts +45 -0
- package/dist/utils/notificationUtils.d.ts +22 -0
- package/dist/utils/pushSubscriptionService.d.ts +12 -0
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/public/firebase-messaging-sw.js +351 -0
- 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
|
+
}
|