Skip to content

Commit 9e32847

Browse files
committed
notifications
1 parent be21258 commit 9e32847

7 files changed

Lines changed: 1516 additions & 164 deletions

File tree

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ NOTIFICATION_SYSTEM.md
3434
NOTIFICATION_ARCHITECTURE.md
3535
NOTIFICATION_CHECKLIST.md
3636
NOTIFICATION_IMPLEMENTATION_SUMMARY.md
37+
38+
# Offline Sync & Notification Documentation
39+
OFFLINE_NOTIFICATION_FIX.md
40+
NOTIFICATION_SETUP_GUIDE.md
41+
TESTING_CHECKLIST.md
42+
CHANGES_SUMMARY.md
43+
QUICK_REFERENCE.md
44+
OFFLINE_SYNC_README.md
45+
DEPLOYMENT_CHECKLIST.md
46+
IMPLEMENTATION_REPORT.md

frontend/public/service-worker.js

Lines changed: 233 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ self.addEventListener("push", (event) => {
106106
badge: notificationData.badge || "/logo.png",
107107
vibrate: [200, 100, 200],
108108
data: notificationData.data || {},
109+
requireInteraction: false,
110+
silent: false,
111+
tag: notificationData.tag || "default",
112+
renotify: true,
109113
actions: notificationData.actions || [
110114
{
111115
action: "view",
@@ -119,7 +123,23 @@ self.addEventListener("push", (event) => {
119123
};
120124

121125
event.waitUntil(
122-
self.registration.showNotification(notificationData.title, options),
126+
self.registration
127+
.showNotification(notificationData.title, options)
128+
.then(() => {
129+
// Play notification sound by messaging all clients
130+
return self.clients.matchAll({
131+
type: "window",
132+
includeUncontrolled: true,
133+
});
134+
})
135+
.then((clientList) => {
136+
clientList.forEach((client) => {
137+
client.postMessage({
138+
type: "play-notification-sound",
139+
data: notificationData.data,
140+
});
141+
});
142+
}),
123143
);
124144
});
125145

@@ -209,7 +229,64 @@ self.addEventListener("message", (event) => {
209229

210230
if (event.data && event.data.type === "SHOW_NOTIFICATION") {
211231
const { title, options } = event.data;
212-
event.waitUntil(self.registration.showNotification(title, options));
232+
event.waitUntil(
233+
self.registration
234+
.showNotification(title, options)
235+
.then(() => {
236+
// Notify clients to play sound
237+
return self.clients.matchAll({
238+
type: "window",
239+
includeUncontrolled: true,
240+
});
241+
})
242+
.then((clientList) => {
243+
clientList.forEach((client) => {
244+
client.postMessage({
245+
type: "play-notification-sound",
246+
data: options.data || {},
247+
});
248+
});
249+
}),
250+
);
251+
}
252+
253+
// Handle sync request from client
254+
if (event.data && event.data.type === "SYNC_MESSAGES") {
255+
event.waitUntil(
256+
syncPendingMessages()
257+
.then(() => {
258+
// Notify client that sync is complete
259+
event.source.postMessage({
260+
type: "sync-complete",
261+
success: true,
262+
});
263+
})
264+
.catch((error) => {
265+
event.source.postMessage({
266+
type: "sync-complete",
267+
success: false,
268+
error: error.message,
269+
});
270+
}),
271+
);
272+
}
273+
274+
// Handle fetch new messages request
275+
if (event.data && event.data.type === "FETCH_NEW_MESSAGES") {
276+
const { conversationId } = event.data;
277+
event.waitUntil(
278+
fetchNewMessages(conversationId)
279+
.then((messages) => {
280+
event.source.postMessage({
281+
type: "new-messages-fetched",
282+
conversationId,
283+
messages,
284+
});
285+
})
286+
.catch((error) => {
287+
console.error("[Service Worker] Error fetching new messages:", error);
288+
}),
289+
);
213290
}
214291
});
215292

@@ -219,21 +296,169 @@ self.addEventListener("sync", (event) => {
219296

220297
if (event.tag === "sync-messages") {
221298
event.waitUntil(
222-
// Sync pending messages
223-
syncPendingMessages(),
299+
syncPendingMessages()
300+
.then(() => {
301+
console.log("[Service Worker] Messages synced successfully");
302+
// Notify all clients about successful sync
303+
return self.clients.matchAll({ type: "window" });
304+
})
305+
.then((clientList) => {
306+
clientList.forEach((client) => {
307+
client.postMessage({
308+
type: "messages-synced",
309+
success: true,
310+
});
311+
});
312+
})
313+
.catch((error) => {
314+
console.error("[Service Worker] Sync failed:", error);
315+
}),
316+
);
317+
}
318+
319+
if (event.tag === "fetch-new-messages") {
320+
event.waitUntil(
321+
fetchAllNewMessages()
322+
.then(() => {
323+
// Notify clients about new messages
324+
return self.clients.matchAll({ type: "window" });
325+
})
326+
.then((clientList) => {
327+
clientList.forEach((client) => {
328+
client.postMessage({
329+
type: "new-messages-available",
330+
success: true,
331+
});
332+
});
333+
}),
224334
);
225335
}
226336
});
227337

338+
// Function to sync pending messages
228339
async function syncPendingMessages() {
229340
try {
230-
// Retrieve pending messages from IndexedDB or cache
231-
// Send them to the server
232-
// This is a placeholder for future implementation
233-
console.log("[Service Worker] Syncing pending messages...");
341+
// Open IndexedDB to get pending messages
342+
const db = await openDatabase();
343+
const pendingMessages = await getPendingMessages(db);
344+
345+
if (pendingMessages.length === 0) {
346+
console.log("[Service Worker] No pending messages to sync");
347+
return Promise.resolve();
348+
}
349+
350+
console.log(
351+
`[Service Worker] Syncing ${pendingMessages.length} pending messages...`,
352+
);
353+
354+
// Send each pending message
355+
const syncPromises = pendingMessages.map(async (msg) => {
356+
try {
357+
const response = await fetch("/api/chat/send", {
358+
method: "POST",
359+
headers: {
360+
"Content-Type": "application/json",
361+
},
362+
credentials: "include",
363+
body: JSON.stringify(msg.data),
364+
});
365+
366+
if (response.ok) {
367+
// Remove from pending queue
368+
await removePendingMessage(db, msg.id);
369+
return { success: true, id: msg.id };
370+
} else {
371+
return { success: false, id: msg.id };
372+
}
373+
} catch (error) {
374+
console.error("[Service Worker] Failed to sync message:", error);
375+
return { success: false, id: msg.id, error };
376+
}
377+
});
378+
379+
await Promise.all(syncPromises);
234380
return Promise.resolve();
235381
} catch (error) {
236382
console.error("[Service Worker] Sync failed:", error);
237383
return Promise.reject(error);
238384
}
239385
}
386+
387+
// Function to fetch new messages for a conversation
388+
async function fetchNewMessages(conversationId) {
389+
try {
390+
const response = await fetch(`/api/chat/messages/${conversationId}`, {
391+
credentials: "include",
392+
});
393+
394+
if (response.ok) {
395+
const messages = await response.json();
396+
return messages;
397+
} else {
398+
throw new Error("Failed to fetch messages");
399+
}
400+
} catch (error) {
401+
console.error("[Service Worker] Error fetching messages:", error);
402+
throw error;
403+
}
404+
}
405+
406+
// Function to fetch all new messages when coming back online
407+
async function fetchAllNewMessages() {
408+
try {
409+
const response = await fetch("/api/chat/conversations", {
410+
credentials: "include",
411+
});
412+
413+
if (response.ok) {
414+
return await response.json();
415+
} else {
416+
throw new Error("Failed to fetch conversations");
417+
}
418+
} catch (error) {
419+
console.error("[Service Worker] Error fetching all messages:", error);
420+
throw error;
421+
}
422+
}
423+
424+
// IndexedDB helper functions
425+
function openDatabase() {
426+
return new Promise((resolve, reject) => {
427+
const request = indexedDB.open("ChatDB", 1);
428+
429+
request.onerror = () => reject(request.error);
430+
request.onsuccess = () => resolve(request.result);
431+
432+
request.onupgradeneeded = (event) => {
433+
const db = event.target.result;
434+
if (!db.objectStoreNames.contains("pendingMessages")) {
435+
db.createObjectStore("pendingMessages", {
436+
keyPath: "id",
437+
autoIncrement: true,
438+
});
439+
}
440+
};
441+
});
442+
}
443+
444+
function getPendingMessages(db) {
445+
return new Promise((resolve, reject) => {
446+
const transaction = db.transaction(["pendingMessages"], "readonly");
447+
const store = transaction.objectStore("pendingMessages");
448+
const request = store.getAll();
449+
450+
request.onsuccess = () => resolve(request.result);
451+
request.onerror = () => reject(request.error);
452+
});
453+
}
454+
455+
function removePendingMessage(db, id) {
456+
return new Promise((resolve, reject) => {
457+
const transaction = db.transaction(["pendingMessages"], "readwrite");
458+
const store = transaction.objectStore("pendingMessages");
459+
const request = store.delete(id);
460+
461+
request.onsuccess = () => resolve();
462+
request.onerror = () => reject(request.error);
463+
});
464+
}

0 commit comments

Comments
 (0)