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:
1 parent
4ad11ea4ee
commit
44b481e177
4 files changed
+100
No files matched your search
@@ -236,6 +236,7 @@ function resetTheme() {
|
||||
let currentStudent = null;
|
||||
|
||||
async function loadHome() {
|
||||
stopMsgPoll();
|
||||
resetTheme();
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? 'Good morning! ☀️'
|
||||
@@ -369,6 +370,7 @@ async function loadInbox() {
|
||||
|
||||
markAllSeen(sid, items.map((r) => r.submissionId));
|
||||
updateInboxBadge(sid, 0);
|
||||
_lastParentMsgCount = 0;
|
||||
renderMsgThread(msgs);
|
||||
}
|
||||
|
||||
@@ -427,6 +429,61 @@ async function refreshInboxBadge(studentId) {
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
let _msgPollInterval = null;
|
||||
let _lastParentMsgCount = -1;
|
||||
|
||||
function showMsgNotifToast() {
|
||||
const existing = document.getElementById('msg-notif-toast');
|
||||
if (existing) existing.remove();
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'msg-notif-toast';
|
||||
toast.className = 'msg-notif-toast';
|
||||
toast.innerHTML = '💬 New message from your parent! <span class="msg-notif-tap">Tap to read</span>';
|
||||
toast.addEventListener('click', () => { toast.remove(); switchTab('inbox'); });
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.classList.add('msg-notif-toast-show'), 10);
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('msg-notif-toast-show');
|
||||
setTimeout(() => toast.remove(), 400);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
function requestNotifPermission() {
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
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(); };
|
||||
}
|
||||
|
||||
function startMsgPoll(studentId) {
|
||||
clearInterval(_msgPollInterval);
|
||||
_lastParentMsgCount = -1;
|
||||
_msgPollInterval = setInterval(async () => {
|
||||
try {
|
||||
const { count } = await api(`/api/students/${studentId}/messages/count`);
|
||||
if (_lastParentMsgCount >= 0 && count > _lastParentMsgCount) {
|
||||
fireOsNotif('💬 New message!', 'Your parent sent you a message.', () => switchTab('inbox'));
|
||||
if (currentTab !== 'inbox') {
|
||||
showMsgNotifToast();
|
||||
updateInboxBadge(studentId, count);
|
||||
}
|
||||
}
|
||||
_lastParentMsgCount = count;
|
||||
} catch { /* non-critical */ }
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopMsgPoll() {
|
||||
clearInterval(_msgPollInterval);
|
||||
_msgPollInterval = null;
|
||||
_lastParentMsgCount = -1;
|
||||
}
|
||||
|
||||
async function openKid(id, tab = 'today', welcome = false) {
|
||||
const [state, agenda, courses, { tests: recentTests }] = await Promise.all([
|
||||
api(`/api/state/${id}`),
|
||||
@@ -435,6 +492,8 @@ async function openKid(id, tab = 'today', welcome = false) {
|
||||
api(`/api/students/${id}/tests`),
|
||||
]);
|
||||
refreshInboxBadge(id);
|
||||
requestNotifPermission();
|
||||
startMsgPoll(id);
|
||||
currentStudent = state.student;
|
||||
youngLearnerMode = !!currentStudent.young_learner;
|
||||
applyTheme(currentStudent.theme || 'blue');
|
||||
|
||||
Reference in new issue
Block a user