114 lines
11 KiB
JavaScript
114 lines
11 KiB
JavaScript
// Shared state
|
|
const st = { tailLoading: false, atBottom: true, current: '#', earliest: null, sse: null, channels: [], twLoaded: false, seen: new Set(), loadingHistory: false };
|
|
|
|
function measureBars(){
|
|
const hdr = document.querySelector('header.nav');
|
|
const ftr = document.querySelector('footer');
|
|
if(hdr){ document.documentElement.style.setProperty('--headerH', hdr.getBoundingClientRect().height+'px'); }
|
|
if(ftr){ document.documentElement.style.setProperty('--footerH', ftr.getBoundingClientRect().height+'px'); }
|
|
}
|
|
|
|
function colorFor(nick){ let h=0; for(let i=0;i<nick.length;i++){ h=(h*31+nick.charCodeAt(i))>>>0 } return 'hsl('+(h%360)+',60%,'+(window.matchMedia('(prefers-color-scheme: dark)').matches? '70%':'35%')+')'; }
|
|
function escapeHtml(s){ return s.replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
function linkify(t){ return t.replace(/https?:\/\/\S+/g, function(u){ return '<a href="' + u + '" target="_blank" rel="noopener">' + u + '</a>'; }); }
|
|
function lineHTML(m){ const ts = '<span class=ts>[' + m.time + ']</span>'; const nick = '<b style="color:' + colorFor(m.author) + '">' + m.author + '</b>'; const body = escapeHtml(m.body); return ts + ' ' + nick + ': ' + linkify(body); }
|
|
|
|
function msgKey(m){ return m.time + '|' + m.author + '|' + m.body; }
|
|
|
|
function snapBottom(){ window.scrollTo(0, document.documentElement.scrollHeight); }
|
|
function pinBottomMulti(){ if(!st.atBottom) return; [0,16,64,200].forEach(d=> setTimeout(()=>requestAnimationFrame(snapBottom), d)); }
|
|
|
|
async function api(path, params){
|
|
const url = new URL(path, window.location.origin);
|
|
if(params && params.query){ Object.entries(params.query).forEach(([k,v])=>url.searchParams.set(k,v)); }
|
|
const res = await fetch(url);
|
|
if(!res.ok) throw new Error('HTTP '+res.status);
|
|
const ct = res.headers.get('content-type')||'';
|
|
if(ct.includes('application/json')) return res.json();
|
|
return res.text();
|
|
}
|
|
|
|
function ensureTwitterWidgets(){ if(st.twLoaded) { if(window.twttr && twttr.widgets && typeof twttr.widgets.load==='function'){ /* keep for later calls */ } return; } st.twLoaded = true; const s=document.createElement('script'); s.async=true; s.src='https://platform.twitter.com/widgets.js'; document.head.appendChild(s); }
|
|
|
|
function appendBatch(arr){ const el=document.getElementById('tail'); const frag=document.createDocumentFragment(); arr.forEach(m=>{ const key=msgKey(m); if(st.seen.has(key)) return; st.seen.add(key); const div=document.createElement('div'); div.className='msg'; div.dataset.key=key; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.appendChild(frag); pinBottomMulti(); }
|
|
function prependBatch(arr){ const el=document.getElementById('tail'); const oldTop=el.firstChild; const beforeTop = oldTop ? oldTop.getBoundingClientRect().top : 0; const frag=document.createDocumentFragment(); arr.forEach(m=>{ const key=msgKey(m); if(st.seen.has(key)) return; st.seen.add(key); const div=document.createElement('div'); div.className='msg'; div.dataset.key=key; div.innerHTML=lineHTML(m); frag.appendChild(div); processLinks(div); }); el.insertBefore(frag, el.firstChild); if(oldTop){ const afterTop = oldTop.getBoundingClientRect().top; const delta = afterTop - beforeTop; window.scrollBy(0, delta); } }
|
|
|
|
function processLinks(scope){ const links = scope.querySelectorAll('a[href]:not([data-card])'); links.forEach(a=>{ a.setAttribute('data-card','1');
|
|
// Fetch and render card
|
|
fetch('/api/linkcard?url='+encodeURIComponent(a.href)).then(r=>r.json()).then(card=>{
|
|
if(!card) return;
|
|
if(card.title||card.description||card.image||card.html){
|
|
const c=document.createElement('div'); c.className='card';
|
|
const details=document.createElement('div'); details.className='card-details';
|
|
var html='';
|
|
if(card.image){ html += '<div><img src="'+card.image+'" alt="" style="max-width:160px;max-height:120px;object-fit:cover;border-radius:.25rem"></div>'; }
|
|
html += '<div style="flex:1;margin-left:.5rem">';
|
|
if(card.title){ html += '<div style="font-weight:600">'+escapeHtml(card.title)+'</div>'; }
|
|
if(card.description){ html += '<div style="opacity:.8">'+escapeHtml(card.description)+'</div>'; }
|
|
html += '</div>';
|
|
const row=document.createElement('div'); row.style.display='flex'; row.style.alignItems='flex-start'; row.style.gap='.5rem'; row.innerHTML=html;
|
|
details.appendChild(row);
|
|
if(card.html){
|
|
const wrap=document.createElement('div');
|
|
wrap.innerHTML=card.html;
|
|
// Tweak YouTube iframe sizing
|
|
const ifr=wrap.querySelector('iframe');
|
|
if(ifr){ ifr.removeAttribute('width'); ifr.removeAttribute('height'); ifr.style.width='100%'; ifr.style.maxWidth='640px'; ifr.style.aspectRatio='16/9'; ifr.style.height='auto'; ifr.style.borderRadius='.5rem'; }
|
|
details.appendChild(wrap);
|
|
// Twitter embed render pass
|
|
ensureTwitterWidgets();
|
|
if(window.twttr && twttr.widgets && typeof twttr.widgets.load==='function'){ try{ twttr.widgets.load(wrap); }catch(e){} }
|
|
}
|
|
details.querySelectorAll('img').forEach(img=> img.addEventListener('load', ()=> pinBottomMulti()));
|
|
c.appendChild(details);
|
|
const sum=document.createElement('div'); sum.className='link-summary'; sum.style.whiteSpace='pre-wrap'; sum.style.marginTop='.25rem'; sum.style.display='none'; c.appendChild(sum);
|
|
// Place actions inline next to the original link
|
|
const act=document.createElement('span'); act.style.marginLeft='.35rem';
|
|
const sumBtn=document.createElement('button'); sumBtn.type='button'; sumBtn.title='Summarize'; sumBtn.textContent='🌚'; sumBtn.style.padding='0 .35rem'; sumBtn.style.fontSize='.9rem';
|
|
const chevron=document.createElement('button'); chevron.type='button'; chevron.title='Expand/collapse'; chevron.textContent='▾'; chevron.style.padding='0 .35rem'; chevron.style.fontSize='.9rem';
|
|
const spinner=document.createElement('span'); spinner.textContent=''; spinner.style.marginLeft='.25rem';
|
|
act.appendChild(sumBtn); act.appendChild(chevron); act.appendChild(spinner);
|
|
a.insertAdjacentElement('afterend', act);
|
|
// Insert card after the link line
|
|
a.parentNode.insertBefore(c, act.nextSibling);
|
|
const toggle = ()=>{ const hidden = c.style.display==='none'; c.style.display = hidden? '' : 'none'; chevron.textContent = hidden? '▾':'▸'; pinBottomMulti(); };
|
|
chevron.onclick = (ev)=>{ ev.stopPropagation(); toggle(); };
|
|
sumBtn.onclick = async ()=>{
|
|
if(sum.style.display!== 'none' && sum.textContent){ sum.style.display='none'; sumBtn.textContent='🌚'; pinBottomMulti(); return; }
|
|
// Ensure card is visible when showing summary
|
|
if(c.style.display==='none'){ toggle(); }
|
|
sumBtn.disabled=true; spinner.textContent='…'; sum.textContent=''; sum.style.display='';
|
|
try{ const data = await api('/api/linksummary',{query:{url:a.href}}); sum.textContent = (data && data.summary) ? data.summary : '(no summary)'; sumBtn.textContent='🌝'; }
|
|
catch(e){ sum.textContent = 'error: '+e; }
|
|
spinner.textContent=''; sumBtn.disabled=false; pinBottomMulti();
|
|
};
|
|
}
|
|
}).catch(()=>{});
|
|
}); }
|
|
|
|
async function loadChannels(){ try{ const data = await api('/api/channels'); st.channels = data; renderChannels(); if(data.length>0){ selectChannel(data[0]); } } catch(e){} }
|
|
function renderChannels(){ const list=document.getElementById('nav-chans') || document.getElementById('brand-chans')?.querySelector('ul'); if(!list) return; list.innerHTML=''; st.channels.forEach(c=>{ const li=document.createElement('li'); const a=document.createElement('a'); a.href='#'; a.textContent=c + (c===st.current? ' ✓':''); a.onclick=(ev)=>{ev.preventDefault(); selectChannel(c);}; li.appendChild(a); list.appendChild(li); }); }
|
|
|
|
async function selectChannel(ch){ if(st.sse){ st.sse.close(); st.sse=null; } st.current=ch; st.seen = new Set(); st.loadingHistory = false; renderChannels(); const el=document.getElementById('tail'); if(!el) return; el.textContent=''; const data = await api('/api/tail',{query:{channel:ch,limit:50}}); appendBatch(data); requestAnimationFrame(()=>{ snapBottom(); st.atBottom=true; }); st.earliest = data.length? data[0].time : null; startStream(); initScrollHandlers(); }
|
|
function initScrollHandlers(){ const el=document.getElementById('tail'); if(!el) return; const onScroll = async ()=>{ const nearBottom = (window.innerHeight + window.pageYOffset + 16) >= document.documentElement.scrollHeight; st.atBottom = nearBottom; const nearTop = window.pageYOffset <= 4; if(nearTop && st.earliest && !st.loadingHistory){ try{ st.loadingHistory = true; const before = st.earliest; const older = await api('/api/history',{query:{channel:st.current,before:before,limit:50}}); if(older.length){ prependBatch(older); st.earliest = older[0].time; } } catch(e){} finally { st.loadingHistory = false; } } }; window.removeEventListener('scroll', st._scrollHandler || (()=>{})); st._scrollHandler = onScroll; window.addEventListener('scroll', onScroll, {passive:true}); }
|
|
|
|
function startStream(){ const el=document.getElementById('tail'); if(!el) return; const url=new URL('/api/stream', window.location.origin); url.searchParams.set('channel', st.current); const es=new EventSource(url); st.sse=es; es.onmessage=(ev)=>{ try{ const m=JSON.parse(ev.data); appendBatch([m]); }catch(e){} }; es.onerror=()=>{ es.close(); st.sse=null; setTimeout(startStream, 3000); } }
|
|
|
|
async function summarize(){ const ch=document.getElementById('channel'); const win=document.getElementById('window'); const push=document.getElementById('push'); const btn=document.getElementById('btn'); const out=document.getElementById('out'); if(!ch||!win||!btn||!out) return; btn.disabled=true; out.textContent=''; try{ const data = await api('/api/trigger',{query:{channel:ch.value,window:win.value||'6h',push:push && push.checked? '1':'0'}}); if(typeof data === 'string'){ out.textContent = data; } else { out.textContent = (data.summary||''); } } catch(e){ out.textContent = 'error: '+e; } btn.disabled=false; }
|
|
|
|
window.addEventListener('DOMContentLoaded', ()=>{
|
|
if('scrollRestoration' in history){ history.scrollRestoration = 'manual'; }
|
|
measureBars();
|
|
loadChannels();
|
|
// Open brand dropdown on hover (desktop) for the dashboard
|
|
const brandDetails = document.getElementById('brand-chans');
|
|
if(brandDetails){
|
|
brandDetails.addEventListener('mouseenter', ()=>{ brandDetails.setAttribute('open',''); });
|
|
brandDetails.addEventListener('mouseleave', ()=>{ brandDetails.removeAttribute('open'); });
|
|
}
|
|
if(document.getElementById('channel')){
|
|
fetch('/api/channels').then(r=>r.json()).then(arr=>{ const sel=document.getElementById('channel'); arr.forEach(c=>{ const o=document.createElement('option'); o.value=c; o.textContent=c; sel.appendChild(o); }); }).catch(()=>{});
|
|
}
|
|
});
|
|
window.addEventListener('load', measureBars);
|
|
window.addEventListener('resize', ()=>{ measureBars(); if(st.atBottom) pinBottomMulti(); });
|