/** * Chat — Surface Entry Point (v0.2.0) * * Messaging surface built on chat-core library: * sw.api.ext('chat-core') — conversation/message CRUD * sw.api.ext('chat') — typing indicators * sw.realtime — live events * sw.ui.* — primitive components * sw.shell.Topbar — navigation bar */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; // ── Boot SDK ─────────────────────────────── try { if (!window.preact) { var { h, render } = await import(base + '/js/sw/vendor/preact.module.js'); var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js'); var htmModule = await import(base + '/js/sw/vendor/htm.module.js'); window.preact = { h, render }; window.hooks = hooksModule; window.html = htmModule.default.bind(h); } var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver); await sdk.boot(); } catch (e) { mount.innerHTML = '

SDK boot failed: ' + e.message + '

'; return; } var { html } = window; var { useState, useEffect, useCallback, useRef, useMemo } = hooks; var { render } = preact; // ── SDK modules ──────────────────────────── var api = sw.api.ext('chat-core'); var chatApi = sw.api.ext('chat'); var { Button, Spinner, Avatar, Dialog, Tabs } = sw.ui; var Topbar = sw.shell.Topbar; // Import UserPicker directly (not in sw.ui index) var { UserPicker } = await import(base + '/js/sw/primitives/user-picker.js?v=' + ver); // ── Helpers ──────────────────────────────── var _timers = {}; function debounce(key, fn, ms) { clearTimeout(_timers[key]); _timers[key] = setTimeout(fn, ms); } function timeAgo(ts) { if (!ts) return ''; var d = new Date(ts); var now = Date.now(); var diff = Math.floor((now - d.getTime()) / 1000); if (diff < 60) return 'now'; if (diff < 3600) return Math.floor(diff / 60) + 'm'; if (diff < 86400) return Math.floor(diff / 3600) + 'h'; if (diff < 604800) return Math.floor(diff / 86400) + 'd'; return d.toLocaleDateString(); } function truncate(str, len) { if (!str) return ''; return str.length > len ? str.slice(0, len) + '\u2026' : str; } function currentUserId() { return sw.auth?.user?.id || ''; } function currentDisplayName() { return sw.auth?.user?.display_name || sw.auth?.user?.username || ''; } // ═══════════════════════════════════════════ // ConversationList — left sidebar // ═══════════════════════════════════════════ function ConversationList({ selected, onSelect, onNew, conversations, unread }) { var [searchQuery, setSearchQuery] = useState(''); var [searchResults, setSearchResults] = useState(null); var [searching, setSearching] = useState(false); function handleSearchInput(e) { var q = e.target.value; setSearchQuery(q); if (q.length < 2) { setSearchResults(null); setSearching(false); return; } debounce('search', () => { setSearching(true); api.get('/search?q=' + encodeURIComponent(q)).then(res => { setSearchResults(res || { conversations: [], messages: [] }); setSearching(false); }).catch(() => { setSearching(false); }); }, 300); } function clearSearch() { setSearchQuery(''); setSearchResults(null); setSearching(false); } function selectFromSearch(cid) { clearSearch(); onSelect(cid); } // Render search results var showSearch = searchResults !== null; var sConvs = showSearch ? (searchResults.conversations || []) : []; var sMsgs = showSearch ? (searchResults.messages || []) : []; return html`
Conversations <${Button} size="sm" onClick=${onNew}>New
${showSearch ? html`
${searching && html`
<${Spinner} size="sm" />
`} ${!searching && sConvs.length === 0 && sMsgs.length === 0 && html`
No results
`} ${sConvs.length > 0 && html`
Conversations
${sConvs.map(c => html`
selectFromSearch(c.id)}>
${c.title || 'Untitled'} ${timeAgo(c.updated_at || c.created_at)}
`)}`} ${sMsgs.length > 0 && html`
Messages
${sMsgs.map(m => html`
selectFromSearch(m.conversation_id)}>
${truncate(m.content, 80)}
${timeAgo(m.created_at)}
`)}`}
` : html`
${conversations.length === 0 && html`
No conversations yet
`} ${conversations.map(c => html`
onSelect(c.id)}>
${c.title || 'Untitled'} ${timeAgo(c.updated_at || c.created_at)}
${c.last_message ? truncate(c.last_message.content_type === 'system' ? '\u2022 ' + c.last_message.content : c.last_message.content, 60) : 'No messages yet'} ${(unread[c.id] || 0) > 0 && html` ${unread[c.id]}`}
`)}
`}
`; } // ═══════════════════════════════════════════ // MessageBubble — single message // ═══════════════════════════════════════════ function MessageBubble({ msg, isOwn, onEdit, onDelete }) { var [editing, setEditing] = useState(false); var [editText, setEditText] = useState(''); var [hover, setHover] = useState(false); if (msg._deleted) { return html`
This message was deleted
`; } if (msg.content_type === 'system') { return html`
${msg.content}
`; } function startEdit() { setEditText(msg.content); setEditing(true); } function cancelEdit() { setEditing(false); setEditText(''); } function saveEdit() { if (editText.trim() && editText !== msg.content) { onEdit(msg.id, editText.trim()); } setEditing(false); } function onEditKeyDown(e) { if (e.key === 'Escape') cancelEdit(); if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveEdit(); } } return html`
setHover(true)} onMouseLeave=${() => setHover(false)}> ${!isOwn && html` <${Avatar} name=${msg._display_name || 'Unknown'} size="sm" />`}
${!isOwn && html`${msg._display_name || 'Unknown'}`} ${editing ? html`