/** * — Thread listing grid with cards. * * Subscribes to Automerge doc and renders all threads sorted by updatedAt. * Falls back to demo data when space=demo. */ import { socialsSchema, socialsDocId } from '../schemas'; import type { SocialsDoc, ThreadData } from '../schemas'; import type { DocumentId } from '../../../shared/local-first/document'; import { DEMO_FEED } from '../lib/types'; export class FolkThreadGallery extends HTMLElement { private _space = 'demo'; private _threads: ThreadData[] = []; private _offlineUnsub: (() => void) | null = null; static get observedAttributes() { return ['space']; } connectedCallback() { this.attachShadow({ mode: 'open' }); this._space = this.getAttribute('space') || 'demo'; this.render(); if (this._space === 'demo') { this.loadDemoData(); } else { this.subscribeOffline(); } } disconnectedCallback() { this._offlineUnsub?.(); this._offlineUnsub = null; } attributeChangedCallback(name: string, _old: string, val: string) { if (name === 'space') this._space = val; } private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; try { const docId = socialsDocId(this._space) as DocumentId; const doc = await runtime.subscribe(docId, socialsSchema); this.renderFromDoc(doc); this._offlineUnsub = runtime.onChange(docId, (updated: any) => { this.renderFromDoc(updated); }); } catch { // Runtime unavailable } } private renderFromDoc(doc: SocialsDoc) { if (!doc?.threads) return; this._threads = Object.values(doc.threads).sort((a, b) => b.updatedAt - a.updatedAt); this.render(); } private loadDemoData() { this._threads = [ { id: 'demo-1', name: 'Alice', handle: '@alice', title: 'Building local-first apps with rSpace', tweets: ['Just deployed the new rFlows river view! The enoughness score is such a powerful concept.', 'The key insight: local-first means your data is always available, even offline.', 'And with Automerge, real-time sync just works. No conflict resolution needed.'], createdAt: Date.now() - 86400000, updatedAt: Date.now() - 3600000, }, { id: 'demo-2', name: 'Bob', handle: '@bob', title: 'Why cosmolocal production matters', tweets: ['The cosmolocal print network now has 6 providers across 4 countries.', 'Design global, manufacture local — this is the future of sustainable production.'], createdAt: Date.now() - 172800000, updatedAt: Date.now() - 86400000, }, { id: 'demo-3', name: 'Carol', handle: '@carol', title: 'Governance lessons from Elinor Ostrom', tweets: ['Reading "Governing the Commons" — so many parallels to what we\'re building.', 'Ostrom\'s 8 principles for managing commons map perfectly to DAO governance.', 'The key: graduated sanctions and local monitoring. Not one-size-fits-all.'], createdAt: Date.now() - 259200000, updatedAt: Date.now() - 172800000, }, ]; this.render(); } private esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } private render() { if (!this.shadowRoot) return; const space = this._space; const threads = this._threads; const cardsHTML = threads.length === 0 ? `

No threads yet. Create your first thread!

Create Thread
` : `
${threads.map(t => { const initial = (t.name || '?').charAt(0).toUpperCase(); const preview = this.esc((t.tweets[0] || '').substring(0, 200)); const dateStr = new Date(t.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const imageTag = t.imageUrl ? `
` : ''; return ` ${imageTag}

${this.esc(t.title || 'Untitled Thread')}

${preview}

${this.esc(initial)}
${this.esc(t.handle || t.name || 'Anonymous')}
${t.tweets.length} tweet${t.tweets.length === 1 ? '' : 's'} ${dateStr}
`; }).join('')}
`; this.shadowRoot.innerHTML = ` `; } } customElements.define('folk-thread-gallery', FolkThreadGallery);