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'){