From 2c6b116e9aac9a3834dace987e2c7e9bba1a2d74 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 22 Sep 2026 06:26:21 -0500 Subject: [PATCH] Add to Calendar: hand phones the .ics file instead of a Google link On a phone the Google Calendar app claims calendar.google.com links and opens its own home screen, dropping the event. The pop-up now offers one "Add to Calendar" button that goes to the calendar file on iOS/Android (served inline so the phone hands it to its calendar) and to the Google event editor on desktop, with the other route as the second button either way. The .ics is also folded to RFC 5545 line lengths so Outlook accepts it. Co-Authored-By: Claude Opus 5 --- public/announce.js | 12 ++++++++++-- public/app.html | 2 +- server.js | 10 ++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/public/announce.js b/public/announce.js index 012038a..5c158bd 100644 --- a/public/announce.js +++ b/public/announce.js @@ -98,15 +98,23 @@ var details = (EV.meetUrl ? 'Join: ' + EV.meetUrl + '\n' : '') + String(EV.times || '').replace(/\|/g, ' '); var gcal = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=' + encodeURIComponent(title) + '&dates=' + f(st) + '/' + f(en) + '&details=' + encodeURIComponent(details) + '&location=' + encodeURIComponent(EV.meetUrl || ''); + // On a phone a calendar.google.com link is claimed by the Google Calendar app, which opens its own + // home screen and throws the event details away. Phones get the calendar file instead: iOS, Android + // and the Google Calendar app all import it straight into the default calendar. + var phone = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || ''); var row = document.createElement('div'); row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px;'; var btnCss = 'display:block;text-align:center;text-decoration:none;background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.35);' + 'color:#f3c34a;font-size:13px;font-weight:700;padding:10px 8px;border-radius:10px;'; var g = document.createElement('a'); - g.href = gcal; g.target = '_blank'; g.rel = 'noopener'; g.textContent = '📅 Google Calendar'; g.style.cssText = btnCss; + g.textContent = '📅 Add to Calendar'; g.style.cssText = btnCss; + if (phone) g.href = '/announce.ics?open=1'; + else { g.href = gcal; g.target = '_blank'; g.rel = 'noopener'; } g.addEventListener('click', done); var a = document.createElement('a'); - a.href = '/announce.ics'; a.setAttribute('download', 'rm-circle-huddle.ics'); a.textContent = '📅 Apple / Outlook'; a.style.cssText = btnCss; + a.style.cssText = btnCss; + if (phone) { a.href = gcal; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Google Calendar'; } + else { a.href = '/announce.ics'; a.setAttribute('download', 'rm-circle-huddle.ics'); a.textContent = 'Apple / Outlook'; } a.addEventListener('click', done); row.appendChild(g); row.appendChild(a); foot.appendChild(row); } diff --git a/public/app.html b/public/app.html index 61baf4f..c7bddcb 100644 --- a/public/app.html +++ b/public/app.html @@ -28,5 +28,5 @@ Open the dashboard - + diff --git a/server.js b/server.js index b3e1702..a10417b 100644 --- a/server.js +++ b/server.js @@ -689,8 +689,14 @@ async function handleApi(req,res,pathname){ 'UID:'+(c.announceId||'event')+'@rmcircle.team','DTSTAMP:'+fmt(Date.now()),'DTSTART:'+fmt(st),'DTEND:'+fmt(st+dur), 'SUMMARY:'+esc(title),'DESCRIPTION:'+esc(desc),'LOCATION:'+esc(c.announceMeetUrl||''), ...(c.announceMeetUrl?['URL:'+c.announceMeetUrl]:[]), - 'BEGIN:VALARM','TRIGGER:-PT30M','ACTION:DISPLAY','DESCRIPTION:'+esc(title),'END:VALARM','END:VEVENT','END:VCALENDAR'].join('\r\n')+'\r\n'; - res.writeHead(200,{'Content-Type':'text/calendar; charset=utf-8','Content-Disposition':'attachment; filename="rm-circle-huddle.ics"','Cache-Control':'no-store'}); + 'BEGIN:VALARM','TRIGGER:-PT30M','ACTION:DISPLAY','DESCRIPTION:'+esc(title),'END:VALARM','END:VEVENT','END:VCALENDAR'] + .map(l=>{ // RFC 5545 line folding: Outlook rejects content lines past 75 octets + if(l.length<=72)return l;let out=l.slice(0,72),rest=l.slice(72); + while(rest.length){out+='\r\n '+rest.slice(0,71);rest=rest.slice(71);}return out; + }).join('\r\n')+'\r\n'; + // ?open=1 (phones): served inline so the phone hands it to its calendar instead of filing it away + const inline=new URL(req.url,'http://x').searchParams.get('open')==='1'; + res.writeHead(200,{'Content-Type':'text/calendar; charset=utf-8','Content-Disposition':(inline?'inline':'attachment')+'; filename="rm-circle-huddle.ics"','Cache-Control':'no-store'}); return res.end(ics); } if(req.method==='GET'&&pathname==='/api/public/member'){