/** * — Automerge-backed persistent chat for rMaps rooms. * Subscribes to MapsLocalFirstClient changes and renders a scrollable message list + input. */ import type { MapsLocalFirstClient } from "../local-first-client"; import type { MapChatMessage } from "../schemas"; class MapChatPanel extends HTMLElement { client: MapsLocalFirstClient | null = null; participantId = ""; participantName = ""; participantEmoji = ""; private _unsub: (() => void) | null = null; private _messages: MapChatMessage[] = []; connectedCallback() { this.render(); if (this.client) { this._unsub = this.client.onChange((doc) => { this._messages = Object.values(doc.messages || {}).sort((a, b) => a.createdAt - b.createdAt); this.renderMessages(); }); // Initial load const doc = this.client.getDoc(); if (doc) { this._messages = Object.values(doc.messages || {}).sort((a, b) => a.createdAt - b.createdAt); this.renderMessages(); } } } disconnectedCallback() { this._unsub?.(); this._unsub = null; } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } private relativeTime(ts: number): string { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return "now"; const m = Math.floor(s / 60); if (m < 60) return `${m}m`; const h = Math.floor(m / 60); if (h < 24) return `${h}h`; return `${Math.floor(h / 24)}d`; } private render() { this.innerHTML = `
`; const input = this.querySelector("#chat-input") as HTMLInputElement; const sendBtn = this.querySelector("#chat-send") as HTMLButtonElement; const send = () => { const text = input.value.trim(); if (!text || !this.client) return; const msg: MapChatMessage = { id: crypto.randomUUID(), authorId: this.participantId, authorName: this.participantName, authorEmoji: this.participantEmoji, text, createdAt: Date.now(), }; this.client.addMessage(msg); input.value = ""; input.focus(); }; sendBtn.addEventListener("click", send); input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }); } private renderMessages() { const container = this.querySelector("#chat-msgs"); if (!container) return; if (this._messages.length === 0) { container.innerHTML = '
No messages yet.
Start the conversation!
'; return; } let html = ""; let lastTs = 0; for (const msg of this._messages) { // Time gap separator (>5 min) if (lastTs && msg.createdAt - lastTs > 5 * 60 * 1000) { const d = new Date(msg.createdAt); const timeStr = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); html += `
${timeStr}
`; } lastTs = msg.createdAt; html += `
${this.esc(msg.authorEmoji)}
${this.esc(msg.authorName)} ${this.relativeTime(msg.createdAt)}
${this.esc(msg.text)}
`; } container.innerHTML = html; // Auto-scroll to bottom container.scrollTop = container.scrollHeight; // Store last-viewed timestamp for unread counting if (this._messages.length > 0) { const lastMsg = this._messages[this._messages.length - 1]; localStorage.setItem(`rmaps_chat_seen_${this.getAttribute("room") || ""}`, String(lastMsg.createdAt)); } } } customElements.define("map-chat-panel", MapChatPanel); export { MapChatPanel };