Add in-app and OS notifications for new messages

Polls every 30s for unread messages on both kid and admin sides.
Kid app shows a tappable in-page toast and fires a browser OS
notification when a parent message arrives while the tab is open.
Admin app shows a toast and OS notification when a student replies.
Clicking the OS notification focuses the tab and opens the messages view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-24 10:19:29 -04:00
1 parent 4ad11ea4ee
commit 44b481e177
4 files changed
+100

No files matched your search

+23
View File
@@ -72,13 +72,21 @@ async function loadAppSettings() {
} catch {}
}
function fireOsNotif(title, body, onClick) {
if (!('Notification' in window) || Notification.permission !== 'granted') return;
const n = new Notification(title, { body, icon: '/favicon.ico' });
if (onClick) n.onclick = () => { window.focus(); onClick(); n.close(); };
}
async function unlock() {
await loadAppSettings();
$('#pin-gate').hidden = true;
$('#console').hidden = false;
showPanel('kids');
if ('Notification' in window && Notification.permission === 'default') Notification.requestPermission();
loadGradingBadge();
loadMessagesBadge();
startAdminMsgPoll();
}
async function loadGradingBadge() {
@@ -1953,6 +1961,8 @@ $('#grading-student-filter').addEventListener('change', loadGrading);
// Messages
// ============================================================
let _adminLastMsgCount = -1;
async function loadMessagesBadge() {
try {
const { count } = await api('/api/admin/messages/count');
@@ -1967,9 +1977,22 @@ async function loadMessagesBadge() {
badge?.remove();
pill.classList.remove('grading-pending');
}
if (_adminLastMsgCount >= 0 && count > _adminLastMsgCount) {
msg(`💬 New message from a student (${count} unread)`, 'success');
fireOsNotif('💬 New student message', `${count} unread message${count !== 1 ? 's' : ''} waiting.`,
() => document.querySelector('.nav-pill[data-panel="messages"]')?.click());
}
_adminLastMsgCount = count;
} catch { /* non-critical */ }
}
let _adminMsgPollInterval = null;
function startAdminMsgPoll() {
clearInterval(_adminMsgPollInterval);
_adminMsgPollInterval = setInterval(loadMessagesBadge, 30000);
}
let activeMessageStudentId = null;
async function loadMessages() {