diff --git a/client/public/app.js b/client/public/app.js index c760a5b..c48e921 100644 --- a/client/public/app.js +++ b/client/public/app.js @@ -12,6 +12,7 @@ import { } from "./data.js"; import { renderChat } from "./render.js"; import { initWebSocket } from "./wsModule.js"; +import { primeNotifications } from "./notifications.js"; document.addEventListener("DOMContentLoaded", () => { const chat = document.getElementById("chat"); @@ -25,6 +26,7 @@ document.addEventListener("DOMContentLoaded", () => { chat.style.display = ""; inputContainer.style.display = "flex"; + primeNotifications(); const defaultName = "Anon"; const MAX_NAME_LEN = 15; // server limit is NAME_MAX_LENGTH-1 (15 chars) diff --git a/client/public/data.js b/client/public/data.js index 3386284..152af2b 100644 --- a/client/public/data.js +++ b/client/public/data.js @@ -1,6 +1,8 @@ // coms/client/public/data.js // Data module: holds the in‐memory threads, notices and reply state +import { notifyNotice } from "./notifications.js"; + // Map of all messages and notices by ID (string IDs) // Client’s own session ID let myId = null; @@ -139,6 +141,7 @@ function addNotice(content) { children: [], }); rootIds.push(id); + notifyNotice(content); } /** diff --git a/client/public/notifications.js b/client/public/notifications.js new file mode 100644 index 0000000..ec98b2c --- /dev/null +++ b/client/public/notifications.js @@ -0,0 +1,59 @@ +// Simple notification helpers for the chat client. + +const APP_NAME = "COMS"; + +function supportsNotifications() { + return typeof Notification !== "undefined"; +} + +function stripHtml(input) { + const div = document.createElement("div"); + div.innerHTML = input; + return div.textContent || div.innerText || ""; +} + +/** + * Ask for permission if not already granted/denied. + */ +function primeNotifications() { + if (!supportsNotifications()) return; + if (Notification.permission === "default") { + Notification.requestPermission(); + } +} + +function showNotification(title, body) { + if (!supportsNotifications()) return; + const options = { body }; + if (Notification.permission === "granted") { + new Notification(title, options); + } else if (Notification.permission === "default") { + Notification.requestPermission().then((perm) => { + if (perm === "granted") new Notification(title, options); + }); + } +} + +/** + * Notify the user of a system notice. + * @param {string} content - HTML or text content of the notice. + */ +function notifyNotice(content) { + const text = stripHtml(content); + if (!text) return; + showNotification(`${APP_NAME} notice`, text); +} + +/** + * Notify the user that someone replied directly to their thread. + * @param {string} authorName + * @param {string} content + */ +function notifyDirectReply(authorName, content) { + const text = stripHtml(content); + if (!text) return; + const name = authorName || "Anon"; + showNotification(`${name} replied to your thread`, text); +} + +export { primeNotifications, notifyNotice, notifyDirectReply }; diff --git a/client/public/wsModule.js b/client/public/wsModule.js index 3dedbe2..27e4025 100644 --- a/client/public/wsModule.js +++ b/client/public/wsModule.js @@ -13,12 +13,15 @@ import { updateUser, getGroupedUsers, setMyId, + getMyId, + getThreads, clearData, users, setFocused, setReplyTo, } from "./data.js"; import { renderChat } from "./render.js"; +import { notifyDirectReply } from "./notifications.js"; /** * Initialize WebSocket connection and wire up event handlers. @@ -132,6 +135,23 @@ export function initWebSocket( ? null : String(parent), }); + + // Notify when someone else replies directly to one of my root messages. + const myId = getMyId(); + const parentId = + parent === null || parent === undefined || parent === "-1" + ? null + : String(parent); + if (myId && parentId && author?.id && String(author.id) !== myId) { + const parentMsg = getThreads().get(parentId); + if ( + parentMsg && + parentMsg.parent === null && + parentMsg.authorId === myId + ) { + notifyDirectReply(username, content); + } + } break; } case "msg_ack": {