/** * Post composer: Capture first, text second (DESIGN.md §11.3 "Reply with * evidence"). Structured kinds, an optional place and time anchor. */ import { icon } from './icons.js'; import { $, $$, esc, haptic, toast } from './dom.js'; import { createSheet } from './sheet.js'; import { KINDS, KIND } from './rooms.js'; import { llShort, utc } from './bands.js'; export function createComposer({ rooms, getTime, getLocation, getView, onCapture, onPosted, }) { const sheet = createSheet({ label: 'New post', detents: ['large'], modal: true, dismissible: true, closeButton: true, className: 'composer-sheet opaque', heights: { large: (H) => Math.round(Math.min(H * 0.9, 820)) }, }); let ctx = null; // { s, replyTo, kind, place, time } function placeOptions() { const s = ctx.s; const opts = [ { id: 'none', label: 'No place' }, { id: 'situation', label: 'At the situation', ll: { lat: s.lat, lon: s.lon }, }, ]; const me = getLocation(); if (me) opts.push({ id: 'me', label: 'Where I am', ll: me }); const v = getView(); if ( v && Number.isFinite(v.lat) && Math.abs(v.lat - s.lat) + Math.abs(v.lon - s.lon) > 0.0005 ) opts.push({ id: 'view', label: 'Globe centre', ll: v }); return opts; } function render() { const s = ctx.s; const r = ctx.replyTo || ctx.quote; sheet.head.innerHTML = `
Record·${esc(s.title)}

${ctx.quote ? 'Quote post' : r ? 'Reply' : 'Post to the Record'}

${r ? `
${ctx.quote ? 'Quoting · ' : ''}${esc(KIND[r.kind]?.name || '')} · ${esc(r.author.handle)}${esc(r.text.slice(0, 140))}${r.text.length > 140 ? '…' : ''}
` : ''}`; const places = placeOptions(); const t = getTime(); sheet.body.innerHTML = `
or write
${KINDS.map((k) => ``).join('')}

${esc(KIND[ctx.kind].hint)}

${icon('pin')}Anchor in place
${places.map((p) => ``).join('')}
${icon('clock')}Anchor in time
${t ? `` : ''}
On the record · hash-chained in the room ledger · shared with your other open tabs
`; } function placeholder(kind) { return { witness: 'What did you see or hear, from where, and when?', context: 'Background that helps people read the data…', question: 'What can’t the sensors tell you?', correction: 'What is wrong, and what does the evidence show instead?', prediction: 'What happens next? It stays on the record.', }[kind]; } function open(s, { replyTo = null, kind, quote = null } = {}) { ctx = { s, replyTo, quote, kind: kind || (replyTo ? replyTo.kind === 'question' ? 'context' : 'correction' : 'witness'), place: 'situation', time: 'now', }; render(); sheet.open('large'); setTimeout( () => $('.cmp-text', sheet.body)?.focus({ preventScroll: true }), 380, ); } sheet.root.addEventListener('click', async (e) => { if (!ctx) return; const k = e.target.closest('[data-kind]'); if (k) { ctx.kind = k.dataset.kind; $$('[data-kind]', sheet.body).forEach((b) => b.setAttribute('aria-checked', String(b === k)), ); $('.kind-hint', sheet.body).textContent = KIND[ctx.kind].hint; $('.cmp-text', sheet.body).placeholder = placeholder(ctx.kind); haptic('tick'); return; } const chip = e.target.closest('[data-anchor] button'); if (chip) { const group = chip.parentElement.dataset.anchor; ctx[group] = chip.dataset.v; $$('button', chip.parentElement).forEach((b) => b.setAttribute('aria-pressed', String(b === chip)), ); haptic('tick'); return; } const act = e.target.closest('[data-act]')?.dataset.act; if (act === 'capture') { sheet.close(); onCapture(ctx.s, { replyTo: ctx.replyTo }); } else if (act === 'post') { const text = $('.cmp-text', sheet.body).value.trim(); if (!text) { $('.cmp-text', sheet.body).focus(); toast('Write something first'); return; } const place = placeOptions().find((p) => p.id === ctx.place)?.ll || null; const t = ctx.time === 'now' ? Date.now() : ctx.time === 'ribbon' ? +getTime() : null; const anchor = place || t ? { lat: place?.lat, lon: place?.lon, t } : null; const p = await rooms.post(ctx.s, { kind: ctx.kind, text, replyTo: ctx.replyTo?.id || null, quoteOf: ctx.quote?.id || null, anchor, evidence: {}, }); haptic('success'); sheet.close(); toast(`Posted to the room · ${KIND[p.kind].name}`, { icon: icon(KIND[p.kind].icon), }); onPosted?.(ctx.s, p); } }); return { open, close: () => sheet.close(), root: sheet.root }; }