Scheduled meetings: deliver a background push (fixes no iOS notification)

Root cause: the scheduled-meeting INVITE (routes.js) and the ~10-min REMINDER
(reminders.js) both notified only via CHAT.pushToUser — the WebSocket channel,
which only reaches an OPEN tab with a live socket. Neither called
PUSH.sendToUser, the path that produces a background APNs/FCM/web-push alert.
On iOS the webview is suspended in the background, so the WS event was simply
missed and no notification appeared. (Chat messages already call PUSH.sendToUser,
which is why chat notices arrive on iOS but meeting ones didn't.)

Fix:
- schedule invite: also PUSH.sendToUser to every invited participant + group
  members (kind:'meeting', id:roomCode) so a closed app is notified.
- reminders.js: also PUSH.sendToUser to all reminder recipients.
- client: a kind:'meeting' notification tap now opens the Meeting tab + its list
  (both the live-tab open-chat handler and the cold-boot openKind path), instead
  of calling selectChat with an unsupported kind.

Also: APPSTORE_SUBMISSION.md §8 updated — the UGC Report/Block gate (guideline
1.2) is now implemented, with a suggested reviewer note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:51:25 +05:30
parent e4d361f298
commit 10b2251efe
4 changed files with 173 additions and 3 deletions
+7 -2
View File
@@ -4222,7 +4222,7 @@ async function setupPush(){
try{ _swReg=await navigator.serviceWorker.ready; }catch(e){ console.warn('[push] SW never became ready:', e); return; } // ensure an ACTIVE worker before subscribe()
console.log('[push] service worker ready');
// Clicking an OS notification asks us (if a tab is already open) to open that chat in place.
try{ navigator.serviceWorker.addEventListener('message',(e)=>{ const d=e.data||{}; if(d.type==='open-chat' && d.id){ try{ selectChat(d.kind||'dm', d.id); }catch(_){} } }); }catch(_){}
try{ navigator.serviceWorker.addEventListener('message',(e)=>{ const d=e.data||{}; if(d.type==='open-chat' && d.id){ if((d.kind||'')==='meeting'){ try{ switchTab('meeting'); if(typeof loadScheduledMeetings==='function') loadScheduledMeetings(); }catch(_){} } else { try{ selectChat(d.kind||'dm', d.id); }catch(_){} } } }); }catch(_){}
await subscribePush();
}
async function subscribePush(){
@@ -6570,7 +6570,12 @@ window.addEventListener('message',(e)=>{
// thread fetch) and load the sidebar in the background — don't make the reload wait on it.
let oid=null, okind='dm';
try{ const q=new URLSearchParams(location.search); oid=q.get('openId'); okind=q.get('openKind')||'dm'; if(oid) history.replaceState(null,'','/home'); }catch(_){}
if(oid){
if(oid && okind==='meeting'){
// Opened from a scheduled-meeting notification → jump to the Meeting tab + its list (not a chat).
document.getElementById('loading').style.display='none';
switchTab('meeting'); try{ if(typeof loadScheduledMeetings==='function') loadScheduledMeetings(); }catch(_){}
loadSidebar();
} else if(oid){
selectChat(okind, oid); // open the chat right away (only waits on the thread fetch)
document.getElementById('loading').style.display='none'; // the chat pane shows its own thread loader
loadSidebar().then(()=>{ try{ const it=rowFor(okind,oid); const nm=document.querySelector('#chatPanel .convo-head .nm'); if(it&&nm) nm.textContent=it.name; }catch(_){} });
+3 -1
View File
@@ -2,6 +2,7 @@
// group members, and invited participants. Runs on a 60s tick; marks each meeting reminded.
const R = require('./repos');
const CHAT = require('./chat');
const PUSH = require('./push'); // native/web background push so a CLOSED app still gets the reminder
async function tick() {
try {
@@ -13,7 +14,8 @@ async function tick() {
invited.forEach((id) => recipients.add(id));
if (s.group_id) { try { (await R.conversations.members(s.group_id)).forEach((m) => recipients.add(m)); } catch (_) {} }
const evt = { type: 'meeting-reminder', meeting: { id: s.id, title: s.title, scheduledAt: s.scheduled_at, room: s.room_code } };
recipients.forEach((uid) => { try { CHAT.pushToUser(uid, evt); } catch (_) {} });
recipients.forEach((uid) => { try { CHAT.pushToUser(uid, evt); } catch (_) {} }); // open tab
recipients.forEach((uid) => { try { PUSH.sendToUser(uid, { title: 'Meeting starting soon', body: (s.title || 'Your meeting') + ' starts in ~10 minutes', kind: 'meeting', id: s.room_code, tag: 'meet:' + s.room_code }); } catch (_) {} }); // closed app (iOS APNs etc.)
await R.scheduledMeetings.markReminded(s.id);
}
} catch (_) { /* never let the timer die */ }
+7
View File
@@ -1267,6 +1267,13 @@ route('POST', '/api/meetings/schedule', async (req, res) => {
// Invitation notification to each invited participant.
const inviteEvt = { type: 'meeting-invite', meeting: { id, title: t, scheduledAt: when, whenText: label, room: code, by: u.name || u.email } };
for (const pid of invited) { try { CHAT.pushToUser(pid, inviteEvt); } catch (_) {} }
// Background/native push (iOS APNs, Android FCM, web push) so a CLOSED app still gets the invite. The
// CHAT.pushToUser above only reaches an OPEN tab with a live socket — which is exactly why scheduled-
// meeting notices never arrived on iOS (the webview is suspended in the background). Mirror the chat path.
const notifyIds = new Set(invited);
if (groupId) { try { (await R.conversations.members(groupId)).forEach((m) => notifyIds.add(m)); } catch (_) {} }
notifyIds.delete(u.id);
for (const pid of notifyIds) { try { PUSH.sendToUser(pid, { title: (u.name || u.email) + ' scheduled a meeting', body: t + ' · ' + label, kind: 'meeting', id: code, tag: 'meet:' + code }); } catch (_) {} }
// Email invites (#4): the guest join link goes to external invitees, plus any invited Connect users
// who have an email on file. Fire-and-forget — a mail outage never fails scheduling. No-op if SMTP off.
try {