/** * Chat — Surface Entry Point (v0.3.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 — shell topbar API */ (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; // 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` `; } // ═══════════════════════════════════════════ // 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`Select a conversation or start a new one