let conversations = []; let currentConv = null; let currentMessages = []; let selfName = ""; let searchTerm = ""; let searchTimer = null; let matchIndices = []; let currentMatchPos = -1; // Color palette for group chat senders const senderColors = [ "#075e54", "#25d366", "#128c7e", "#00a884", "#6b3fa0", "#d4653f", "#1f97c7", "#c74375", "#e68a2e", "#7b5ea7", "#3d8c5c", "#c44d58" ]; const senderColorMap = {}; let colorIdx = 0; function getSenderColor(name) { if (!senderColorMap[name]) { senderColorMap[name] = senderColors[colorIdx % senderColors.length]; colorIdx++; } return senderColorMap[name]; } async function init() { await fetchConversations(); renderConversationList(); document.getElementById("search-input").addEventListener("input", onSearch); document.getElementById("self-name").addEventListener("change", onSelfChange); } async function fetchConversations(query) { const url = query ? `/api/conversations?q=${encodeURIComponent(query)}` : "/api/conversations"; const resp = await fetch(url); conversations = await resp.json() || []; } function renderConversationList() { const list = document.getElementById("conversation-list"); list.innerHTML = ""; for (const c of conversations) { const div = document.createElement("div"); div.className = "conv-item"; if (currentConv && c.id === currentConv.id) div.className += " active"; div.dataset.id = c.id; const date = c.lastMessageDate ? formatDate(new Date(c.lastMessageDate)) : ""; div.innerHTML = `
${esc(c.name)}
${c.messageCount} messages · ${date}
`; div.onclick = () => loadConversation(c.id); list.appendChild(div); } } async function loadConversation(id) { // Update active state document.querySelectorAll(".conv-item").forEach(el => { el.classList.toggle("active", el.dataset.id === id); }); const resp = await fetch(`/api/conversation/${id}`); currentConv = await resp.json(); currentMessages = currentConv.messages || []; document.getElementById("chat-name").textContent = currentConv.name; // Detect participants and set up self selector const participants = new Set(); for (const m of currentMessages) { if (m.sender) participants.add(m.sender); } const sel = document.getElementById("self-name"); sel.innerHTML = ""; for (const p of participants) { const opt = document.createElement("option"); opt.value = p; opt.textContent = p; sel.appendChild(opt); } // Auto-detect: first sender is likely "self" if (participants.size > 0) { const firstSender = currentMessages.find(m => m.sender)?.sender || ""; selfName = firstSender; sel.value = selfName; } document.getElementById("self-selector").style.display = participants.size > 1 ? "" : "none"; renderMessages(); } function onSelfChange() { selfName = document.getElementById("self-name").value; renderMessages(); } function onSearch() { clearTimeout(searchTimer); searchTimer = setTimeout(async () => { searchTerm = document.getElementById("search-input").value.toLowerCase().trim(); await fetchConversations(searchTerm); renderConversationList(); if (currentConv) { renderMessages(); } }, 200); } function renderMessages() { const container = document.getElementById("chat-messages"); let msgs = currentMessages; let firstMatchIdx = -1; // Detect if group chat (>2 participants) const participants = new Set(); for (const m of currentMessages) { if (m.sender) participants.add(m.sender); } const isGroup = participants.size > 2; let html = ""; let lastDate = ""; let msgIdx = 0; for (const m of msgs) { const d = new Date(m.timestamp); const dateStr = d.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }); if (dateStr !== lastDate) { html += `
${dateStr}
`; lastDate = dateStr; } const isMatch = searchTerm && (m.text || "").toLowerCase().includes(searchTerm); if (isMatch && firstMatchIdx === -1) firstMatchIdx = msgIdx; if (m.isSystem) { html += `
${highlight(esc(m.text), searchTerm)}
`; msgIdx++; continue; } const isSelf = m.sender === selfName; const cls = isSelf ? "self" : "other"; const timeStr = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); let senderHtml = ""; if (!isSelf && isGroup) { const color = getSenderColor(m.sender); senderHtml = `
${esc(m.sender)}
`; } let attachHtml = ""; if (m.attachments) { for (const a of m.attachments) { const url = `/api/media/${currentConv.id}/${encodeURIComponent(a.filename)}`; switch (a.type) { case "image": attachHtml += `${esc(a.filename)}`; break; case "sticker": attachHtml += `sticker`; break; case "video": attachHtml += ``; break; case "audio": attachHtml += ``; break; default: attachHtml += `${esc(a.filename)}`; } } } // Remove attachment filenames from displayed text let displayText = m.text || ""; if (m.attachments) { for (const a of m.attachments) { displayText = displayText.replace(a.filename, "").trim(); } } let textHtml = ""; if (displayText) { textHtml = `
${highlight(esc(displayText), searchTerm)}
`; } html += `
${senderHtml}${attachHtml}${textHtml}
${timeStr}
`; msgIdx++; } container.innerHTML = html; // Build match index list matchIndices = []; container.querySelectorAll(".search-match").forEach(el => { matchIndices.push(parseInt(el.dataset.idx)); }); const nav = document.getElementById("search-nav"); if (searchTerm && matchIndices.length > 0) { nav.classList.remove("hidden"); currentMatchPos = 0; updateSearchNav(); scrollToMatch(currentMatchPos); } else if (searchTerm) { nav.classList.remove("hidden"); currentMatchPos = -1; document.getElementById("search-pos").textContent = "0/0"; } else { nav.classList.add("hidden"); currentMatchPos = -1; container.scrollTop = container.scrollHeight; } } function updateSearchNav() { document.getElementById("search-pos").textContent = `${currentMatchPos + 1}/${matchIndices.length}`; // Remove highlight from all, add to current document.querySelectorAll(".search-current").forEach(el => el.classList.remove("search-current")); if (currentMatchPos >= 0) { const el = document.querySelector(`[data-idx="${matchIndices[currentMatchPos]}"]`); if (el) el.classList.add("search-current"); } } function scrollToMatch(pos) { if (pos < 0 || pos >= matchIndices.length) return; const el = document.querySelector(`[data-idx="${matchIndices[pos]}"]`); if (el) el.scrollIntoView({ block: "center", behavior: "smooth" }); } function searchNext() { if (matchIndices.length === 0) return; currentMatchPos = (currentMatchPos + 1) % matchIndices.length; updateSearchNav(); scrollToMatch(currentMatchPos); } function searchPrev() { if (matchIndices.length === 0) return; currentMatchPos = (currentMatchPos - 1 + matchIndices.length) % matchIndices.length; updateSearchNav(); scrollToMatch(currentMatchPos); } function esc(s) { if (!s) return ""; const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } function highlight(html, term) { if (!term) return html; const re = new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"); return html.replace(re, "$1"); } function formatDate(d) { return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); } function openLightbox(e, url) { e.stopPropagation(); const lb = document.getElementById("lightbox"); document.getElementById("lightbox-img").src = url; lb.classList.remove("hidden"); } function closeLightbox() { document.getElementById("lightbox").classList.add("hidden"); document.getElementById("lightbox-img").src = ""; } document.addEventListener("keydown", e => { if (e.key === "Escape") closeLightbox(); }); init();