(function () { if (window.GIAComposePanel) { window.GIAComposePanel.initAll(document); return; } const PANEL_SELECTOR = ".compose-shell[data-compose-panel='1']"; window.giaComposePanels = window.giaComposePanels || {}; const collectPanels = function (root) { const panels = []; if (!root) { return panels; } if (root.matches && root.matches(PANEL_SELECTOR)) { panels.push(root); } if (root.querySelectorAll) { root.querySelectorAll(PANEL_SELECTOR).forEach(function (panel) { panels.push(panel); }); } return panels; }; const toInt = function (value) { const parsed = parseInt(value || "0", 10); return Number.isFinite(parsed) ? parsed : 0; }; const parseJsonSafe = function (value, fallback) { try { return JSON.parse(String(value || "")); } catch (_err) { return fallback; } }; const createNode = function (tagName, className, text) { const node = document.createElement(tagName); if (className) { node.className = className; } if (text !== undefined && text !== null) { node.textContent = String(text); } return node; }; const normalizeSnippet = function (value) { const compact = String(value || "").replace(/\s+/g, " ").trim(); if (!compact) { return "(no text)"; } if (compact.length <= 120) { return compact; } return compact.slice(0, 117).trimEnd() + "..."; }; const titleCase = function (value) { const raw = String(value || "").trim().toLowerCase(); if (!raw) { return ""; } if (raw === "whatsapp") { return "WhatsApp"; } if (raw === "xmpp") { return "XMPP"; } return raw.charAt(0).toUpperCase() + raw.slice(1); }; const destroyPanelState = function (state) { if (!state) { return; } if (state.pollTimer) { clearInterval(state.pollTimer); } if (state.pendingCommandPoll) { clearInterval(state.pendingCommandPoll); } if (state.socket) { try { state.socket.close(); } catch (_err) { // Ignore socket close failures. } } if (state.eventHandler) { document.body.removeEventListener("composeMessageSent", state.eventHandler); } if (state.sendResultHandler) { document.body.removeEventListener( "composeSendResult", state.sendResultHandler ); } if (state.commandIdHandler) { document.body.removeEventListener( "composeSendCommandId", state.commandIdHandler ); } delete window.giaComposePanels[state.panelId]; }; const initPanel = function (panel) { if (!panel) { return; } const panelId = String(panel.id || "").trim(); if (!panelId) { return; } if (panel.dataset.composeInitialized === "1" && window.giaComposePanels[panelId]) { return; } destroyPanelState(window.giaComposePanels[panelId]); const thread = document.getElementById(panelId + "-thread"); const form = document.getElementById(panelId + "-form"); const textarea = document.getElementById(panelId + "-textarea"); if (!thread || !form || !textarea) { return; } const platformSelect = document.getElementById(panelId + "-platform-select"); const contactSelect = document.getElementById(panelId + "-contact-select"); const metaLine = document.getElementById(panelId + "-meta-line"); const hiddenService = document.getElementById(panelId + "-input-service"); const hiddenIdentifier = document.getElementById(panelId + "-input-identifier"); const hiddenPerson = document.getElementById(panelId + "-input-person"); const replyBanner = document.getElementById(panelId + "-reply-banner"); const replyBannerText = document.getElementById(panelId + "-reply-text"); const replyClearBtn = document.getElementById(panelId + "-reply-clear"); const statusBox = document.getElementById(panelId + "-status"); const typingNode = document.getElementById(panelId + "-typing"); const hiddenReplyTo = form.querySelector('input[name="reply_to_message_id"]'); const manualConfirm = form.querySelector(".manual-confirm"); const armInput = form.querySelector('input[name="failsafe_arm"]'); const confirmInput = form.querySelector('input[name="failsafe_confirm"]'); const sendButton = form.querySelector(".compose-send-btn"); const renderMode = String(panel.dataset.renderMode || "page"); const csrfToken = String(panel.dataset.csrfToken || ""); const sendCapable = String(panel.dataset.capabilitySend || "").toLowerCase() === "true"; panel.dataset.composeInitialized = "1"; const state = { panelId: panelId, pollTimer: null, polling: false, socket: null, websocketReady: false, pendingCommandPoll: null, pendingCommandId: "", pendingCommandAttempts: 0, pendingCommandStartedAt: 0, pendingCommandInFlight: false, eventHandler: null, sendResultHandler: null, commandIdHandler: null, }; window.giaComposePanels[panelId] = state; let lastTs = toInt(thread.dataset.lastTs); let transientCancelButton = null; let persistentCancelWrap = null; const setStatus = function (message, level) { if (!statusBox) { return; } statusBox.innerHTML = ""; if (!message) { return; } statusBox.appendChild( createNode( "p", "compose-status-line is-" + String(level || "info"), String(message) ) ); }; const flashCompose = function (className) { panel.classList.remove("is-send-success", "is-send-fail"); void panel.offsetWidth; panel.classList.add(className); window.setTimeout(function () { panel.classList.remove(className); }, 420); }; const autosize = function () { textarea.style.height = "auto"; textarea.style.height = Math.min(Math.max(textarea.scrollHeight, 44), 128) + "px"; }; const nearBottom = function () { return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 120; }; const scrollToBottom = function (force) { if (force || nearBottom()) { thread.scrollTop = thread.scrollHeight; } }; const queryParams = function (extraParams) { const params = new URLSearchParams(); params.set("service", thread.dataset.service || ""); params.set("identifier", thread.dataset.identifier || ""); if (thread.dataset.person) { params.set("person", thread.dataset.person); } params.set("limit", thread.dataset.limit || "60"); const extras = extraParams && typeof extraParams === "object" ? extraParams : {}; Object.keys(extras).forEach(function (key) { const value = extras[key]; if (value === undefined || value === null || value === "") { return; } params.set(String(key), String(value)); }); return params; }; const normalizeIdentifierForService = function (service, identifier) { const serviceKey = String(service || "").trim().toLowerCase(); const raw = String(identifier || "").trim(); if (serviceKey === "whatsapp" && raw.includes("@")) { return raw.split("@", 1)[0].trim(); } return raw; }; const buildComposeUrl = function (service, identifier, personId) { const serviceKey = String(service || "").trim().toLowerCase(); const identifierValue = normalizeIdentifierForService(serviceKey, identifier); if (!serviceKey || !identifierValue) { return ""; } const params = new URLSearchParams(); params.set("service", serviceKey); params.set("identifier", identifierValue); if (personId) { params.set("person", String(personId || "").trim()); } return (renderMode === "page" ? "/compose/page/" : "/compose/widget/") + "?" + params.toString(); }; const parseServiceMap = function (optionNode) { const fallbackService = String( (optionNode && optionNode.dataset && optionNode.dataset.service) || "" ).trim().toLowerCase(); const fallbackIdentifier = String( (optionNode && optionNode.value) || "" ).trim(); const fallback = {}; if (fallbackService && fallbackIdentifier) { fallback[fallbackService] = fallbackIdentifier; } if (!optionNode || !optionNode.dataset) { return fallback; } const parsed = parseJsonSafe(optionNode.dataset.serviceMap || "{}", fallback); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return fallback; } const normalized = {}; Object.keys(parsed).forEach(function (key) { const serviceKey = String(key || "").trim().toLowerCase(); const identifierValue = String(parsed[key] || "").trim(); if (serviceKey && identifierValue) { normalized[serviceKey] = identifierValue; } }); return Object.keys(normalized).length ? normalized : fallback; }; const postFormJson = async function (url, params) { const response = await fetch(url, { method: "POST", credentials: "same-origin", headers: { "X-CSRFToken": csrfToken, "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: params.toString(), }); if (!response.ok) { throw new Error("Request failed"); } return response.json(); }; const ensureEmptyState = function (messageText) { if (thread.querySelector(".compose-row")) { const empty = thread.querySelector(".compose-empty"); if (empty) { empty.remove(); } return; } let empty = thread.querySelector(".compose-empty"); if (!empty) { empty = createNode("p", "compose-empty"); thread.appendChild(empty); } empty.textContent = String( messageText || "No stored messages for this contact yet." ); }; const rowByMessageId = function (messageId) { const targetId = String(messageId || "").trim(); if (!targetId) { return null; } return thread.querySelector( '.compose-row[data-message-id="' + targetId + '"]' ); }; const clearReplySelectionClass = function () { thread .querySelectorAll(".compose-row.compose-reply-selected") .forEach(function (row) { row.classList.remove("compose-reply-selected"); }); }; const flashReplyTarget = function (row) { if (!row) { return; } row.classList.remove("is-target-flash"); void row.offsetWidth; row.classList.add("is-target-flash"); window.setTimeout(function () { row.classList.remove("is-target-flash"); }, 1000); }; const clearReplyTarget = function () { if (hiddenReplyTo) { hiddenReplyTo.value = ""; } if (replyBanner) { replyBanner.classList.add("is-hidden"); } if (replyBannerText) { replyBannerText.textContent = ""; } clearReplySelectionClass(); }; const setReplyTarget = function (messageId, preview) { const targetId = String(messageId || "").trim(); if (!targetId) { clearReplyTarget(); return; } const row = rowByMessageId(targetId); const snippet = normalizeSnippet( (row && row.dataset ? row.dataset.replySnippet : "") || preview || "" ); if (hiddenReplyTo) { hiddenReplyTo.value = targetId; } if (replyBannerText) { replyBannerText.textContent = snippet; } if (replyBanner) { replyBanner.classList.remove("is-hidden"); } clearReplySelectionClass(); if (row) { row.classList.add("compose-reply-selected"); } }; const bindReplyReferences = function (root) { const scope = root || thread; scope.querySelectorAll(".compose-row").forEach(function (row) { if (!row.dataset.replySnippet) { const body = row.querySelector(".compose-body"); row.dataset.replySnippet = normalizeSnippet(body ? body.textContent : ""); } }); scope.querySelectorAll(".compose-reply-ref").forEach(function (ref) { const button = ref.querySelector(".compose-reply-link"); const targetId = String(ref.dataset.replyTargetId || "").trim(); if (!button || !targetId) { return; } const targetRow = rowByMessageId(targetId); const preview = normalizeSnippet( (targetRow && targetRow.dataset ? targetRow.dataset.replySnippet : "") || ref.dataset.replyPreview || "" ); button.textContent = "Reply to: " + preview; if (button.dataset.bound === "1") { return; } button.dataset.bound = "1"; button.addEventListener("click", function () { const row = rowByMessageId(targetId); if (!row) { return; } row.scrollIntoView({ behavior: "smooth", block: "center" }); flashReplyTarget(row); }); }); }; const appendMetaNode = function (meta, node) { if (!meta || !node) { return; } if (meta.childNodes.length) { meta.appendChild(document.createTextNode(" ")); } meta.appendChild(node); }; const renderEditHistoryDetails = function (bubble, msg) { const rows = Array.isArray(msg && msg.edit_history) ? msg.edit_history : []; if (!rows.length) { return; } const details = createNode("details", "compose-edit-history"); const summary = createNode( "summary", "", "Edited " + rows.length + (rows.length === 1 ? " time" : " times") ); const list = createNode("ul"); details.appendChild(summary); rows.forEach(function (entry) { const item = createNode("li"); const editedDisplay = String((entry && entry.edited_display) || "").trim() || "Unknown time"; const actor = String((entry && entry.actor) || "").trim(); const source = String((entry && entry.source_service) || "").trim(); item.appendChild( document.createTextNode( editedDisplay + (actor ? " · " + actor : "") + (source ? " · " + source.toUpperCase() : "") ) ); const diff = createNode("div", "compose-edit-diff"); diff.appendChild( createNode( "span", "compose-edit-old", String((entry && entry.previous_text) || "(empty)") ) ); diff.appendChild(createNode("span", "compose-edit-arrow", "->")); diff.appendChild( createNode( "span", "compose-edit-new", String((entry && entry.new_text) || "(empty)") ) ); item.appendChild(diff); list.appendChild(item); }); details.appendChild(list); bubble.appendChild(details); }; const renderBubbleReactions = function (bubble, reactions) { const rows = Array.isArray(reactions) ? reactions : []; if (!rows.length) { return; } const wrap = createNode("div", "compose-reactions"); wrap.setAttribute("aria-label", "Message reactions"); rows.forEach(function (reaction) { const emoji = String((reaction && reaction.emoji) || "").trim(); if (!emoji) { return; } const actor = String((reaction && reaction.actor) || "").trim(); const source = String((reaction && reaction.source_service) || "") .trim() .toLowerCase(); const chip = createNode("span", "tag is-light compose-reaction-chip", emoji); chip.dataset.emoji = emoji; chip.dataset.actor = actor; chip.dataset.sourceService = source; chip.title = (actor || "Unknown") + " via " + (source || "unknown").toUpperCase(); wrap.appendChild(chip); }); if (wrap.children.length) { bubble.appendChild(wrap); } }; const appendImageNodes = function (bubble, msg) { const imageUrls = Array.isArray(msg.image_urls) && msg.image_urls.length ? msg.image_urls : (msg.image_url ? [msg.image_url] : []); imageUrls.forEach(function (imageUrl) { const figure = createNode("figure", "compose-media"); const image = createNode("img", "compose-image"); image.src = String(imageUrl); image.alt = "Attachment"; image.referrerPolicy = "no-referrer"; image.loading = "lazy"; image.decoding = "async"; figure.appendChild(image); bubble.appendChild(figure); }); }; const buildMessageRow = function (msg) { const row = createNode( "div", "compose-row " + (msg.outgoing ? "is-out" : "is-in") ); if (msg.is_deleted) { row.classList.add("is-deleted"); } row.dataset.ts = String(msg.ts || 0); row.dataset.messageId = String(msg.id || ""); row.dataset.replySnippet = normalizeSnippet( msg.display_text || msg.text || "" ); if (msg.reply_to_id) { row.dataset.replyToId = String(msg.reply_to_id || ""); } const bubble = createNode( "article", "compose-bubble " + (msg.outgoing ? "is-out" : "is-in") ); if (msg.reply_to_id) { const replyRef = createNode("div", "compose-reply-ref"); replyRef.dataset.replyTargetId = String(msg.reply_to_id || ""); replyRef.dataset.replyPreview = String(msg.reply_preview || ""); const replyLink = createNode( "button", "compose-reply-link" ); replyLink.type = "button"; replyLink.title = "Jump to referenced message"; replyRef.appendChild(replyLink); bubble.appendChild(replyRef); } if (msg.source_label) { const wrap = createNode("div", "compose-source-badge-wrap"); const badge = createNode( "span", "tag is-light compose-source-badge source-" + String(msg.source_service || "web").toLowerCase(), String(msg.source_label || "") ); wrap.appendChild(badge); bubble.appendChild(wrap); } appendImageNodes(bubble, msg); if (!msg.hide_text) { bubble.appendChild( createNode( "p", "compose-body", String(msg.display_text || msg.text || "(no text)") ) ); } else { bubble.appendChild( createNode( "p", "compose-body compose-image-fallback is-hidden", "(no text)" ) ); } renderEditHistoryDetails(bubble, msg); renderBubbleReactions(bubble, msg.reactions); const meta = createNode("p", "compose-msg-meta"); meta.appendChild( document.createTextNode( String(msg.display_ts || msg.ts || "") + (msg.author ? " · " + String(msg.author) : "") ) ); if (msg.is_edited) { appendMetaNode( meta, createNode("span", "tag is-light compose-msg-flag is-edited", "edited") ); } if (msg.is_deleted) { appendMetaNode( meta, createNode("span", "tag is-light compose-msg-flag is-deleted", "deleted") ); } bubble.appendChild(meta); const replyButton = createNode( "button", "button is-white is-small compose-reply-btn" ); replyButton.type = "button"; replyButton.title = "Reply to this message"; replyButton.setAttribute("aria-label", "Reply to this message"); replyButton.innerHTML = '' + 'Reply'; bubble.appendChild(replyButton); row.appendChild(bubble); return row; }; const insertRowByTs = function (row) { const newTs = toInt(row.dataset.ts); const rows = Array.from(thread.querySelectorAll(".compose-row")); if (!rows.length) { thread.appendChild(row); return; } for (let index = rows.length - 1; index >= 0; index -= 1) { const existing = rows[index]; if (toInt(existing.dataset.ts) <= newTs) { if (existing.nextSibling) { thread.insertBefore(row, existing.nextSibling); } else { thread.appendChild(row); } return; } } thread.insertBefore(row, rows[0]); }; const appendMessage = function (msg) { const messageId = String((msg && msg.id) || "").trim(); if (messageId) { const existing = rowByMessageId(messageId); if (existing) { existing.remove(); } } const empty = thread.querySelector(".compose-empty"); if (empty) { empty.remove(); } insertRowByTs(buildMessageRow(msg)); lastTs = Math.max(lastTs, toInt(msg && msg.ts)); thread.dataset.lastTs = String(lastTs); }; const appendMessages = function (messages, forceScroll) { const rows = Array.isArray(messages) ? messages : []; const shouldStick = forceScroll || nearBottom(); rows.forEach(function (msg) { appendMessage(msg); }); if (rows.length) { bindReplyReferences(thread); scrollToBottom(shouldStick); } ensureEmptyState(); }; const applyTyping = function (payload) { if (!typingNode) { return; } const typingPayload = payload && typeof payload === "object" ? payload : {}; if (!typingPayload.typing) { typingNode.classList.add("is-hidden"); return; } const displayName = String(typingPayload.display_name || "").trim(); typingNode.textContent = (displayName || "Contact") + " is typing..."; typingNode.classList.remove("is-hidden"); }; const cancelSendRequest = function (commandId) { return postFormJson( String(panel.dataset.cancelSendUrl || ""), queryParams({ command_id: String(commandId || "") }) ); }; const hideTransientCancelButton = function () { if (!transientCancelButton) { return; } transientCancelButton.remove(); transientCancelButton = null; }; const showTransientCancelButton = function () { if (!statusBox || transientCancelButton) { return; } transientCancelButton = createNode( "button", "button is-danger is-light is-small compose-cancel-send-btn", "Cancel Send" ); transientCancelButton.type = "button"; transientCancelButton.addEventListener("click", async function () { try { await cancelSendRequest(""); } catch (_err) { // Ignore cancel failures. } finally { hideTransientCancelButton(); } }); statusBox.appendChild(transientCancelButton); }; const hidePersistentCancelButton = function () { if (!persistentCancelWrap) { return; } persistentCancelWrap.remove(); persistentCancelWrap = null; }; const stopPendingCommandPolling = function () { if (state.pendingCommandPoll) { clearInterval(state.pendingCommandPoll); state.pendingCommandPoll = null; } state.pendingCommandId = ""; state.pendingCommandAttempts = 0; state.pendingCommandStartedAt = 0; state.pendingCommandInFlight = false; }; const showPersistentCancelButton = function (commandId) { hidePersistentCancelButton(); if (!statusBox) { return; } persistentCancelWrap = createNode("div", "compose-persistent-cancel"); const button = createNode( "button", "button is-danger is-light is-small compose-persistent-cancel-btn", "Cancel Queued Send" ); button.type = "button"; button.addEventListener("click", async function () { try { await cancelSendRequest(commandId); stopPendingCommandPolling(); hidePersistentCancelButton(); setStatus("Send cancelled.", "warning"); await poll(true); } catch (_err) { hidePersistentCancelButton(); } }); persistentCancelWrap.appendChild(button); statusBox.appendChild(persistentCancelWrap); }; const pollPendingCommandResult = async function (commandId) { const url = new URL( String(panel.dataset.commandResultUrl || ""), window.location.origin ); url.searchParams.set("service", thread.dataset.service || ""); url.searchParams.set("command_id", commandId); url.searchParams.set("format", "json"); const response = await fetch(url.toString(), { credentials: "same-origin", headers: { "HX-Request": "true" }, }); if (!response.ok || response.status === 204) { return null; } return response.json(); }; const startPendingCommandPolling = function (commandId) { if (!commandId) { return; } stopPendingCommandPolling(); state.pendingCommandId = commandId; state.pendingCommandStartedAt = Date.now(); showPersistentCancelButton(commandId); state.pendingCommandPoll = setInterval(async function () { if (state.pendingCommandInFlight) { return; } state.pendingCommandAttempts += 1; if ( state.pendingCommandAttempts > 14 || (Date.now() - state.pendingCommandStartedAt) > 45000 ) { stopPendingCommandPolling(); hidePersistentCancelButton(); setStatus( "Send timed out waiting for runtime result. Please retry.", "warning" ); return; } try { state.pendingCommandInFlight = true; const payload = await pollPendingCommandResult(commandId); if (!payload || payload.pending !== false) { return; } const result = payload.result || {}; stopPendingCommandPolling(); hidePersistentCancelButton(); if (result.ok) { setStatus("", "success"); textarea.value = ""; clearReplyTarget(); autosize(); flashCompose("is-send-success"); await poll(true); return; } setStatus(String(result.error || "Send failed."), "danger"); flashCompose("is-send-fail"); await poll(true); } catch (_err) { // Ignore transient failures; the next poll can recover. } finally { state.pendingCommandInFlight = false; } }, 3500); }; const poll = async function (forceScroll) { if (state.polling || state.websocketReady) { return; } state.polling = true; try { const response = await fetch( thread.dataset.pollUrl + "?" + queryParams({ after_ts: String(lastTs) }), { method: "GET", credentials: "same-origin", headers: { Accept: "application/json" }, } ); if (!response.ok) { return; } const payload = await response.json(); appendMessages(payload.messages || [], forceScroll); applyTyping(payload.typing); if (payload.last_ts !== undefined && payload.last_ts !== null) { lastTs = Math.max(lastTs, toInt(payload.last_ts)); thread.dataset.lastTs = String(lastTs); } } catch (err) { console.debug("compose poll error", err); } finally { state.polling = false; } }; const setupWebSocket = function () { const wsPath = String(thread.dataset.wsUrl || "").trim(); if (!wsPath || !window.WebSocket) { return; } try { const protocol = window.location.protocol === "https:" ? "wss://" : "ws://"; const socket = new WebSocket(protocol + window.location.host + wsPath); state.socket = socket; socket.onopen = function () { state.websocketReady = true; try { socket.send(JSON.stringify({ kind: "sync", last_ts: lastTs })); } catch (_err) { // Ignore sync send errors. } }; socket.onmessage = function (event) { const payload = parseJsonSafe(event.data || "{}", {}); appendMessages(payload.messages || [], false); applyTyping(payload.typing); if (payload.last_ts !== undefined && payload.last_ts !== null) { lastTs = Math.max(lastTs, toInt(payload.last_ts)); thread.dataset.lastTs = String(lastTs); } }; socket.onclose = function () { state.websocketReady = false; if (state.socket === socket) { state.socket = null; } }; socket.onerror = function () { state.websocketReady = false; }; } catch (_err) { state.websocketReady = false; state.socket = null; } }; const switchThreadContext = function ( nextService, nextIdentifier, nextPersonId, nextUrl ) { const service = String(nextService || "").trim().toLowerCase(); const identifier = normalizeIdentifierForService(service, nextIdentifier); const personId = String(nextPersonId || "").trim(); if (!service || !identifier) { return; } if (renderMode === "page" && nextUrl) { window.location.assign(String(nextUrl)); return; } if ( String(thread.dataset.service || "").toLowerCase() === service && String(thread.dataset.identifier || "") === identifier && String(thread.dataset.person || "") === personId ) { return; } thread.dataset.service = service; thread.dataset.identifier = normalizeIdentifierForService(service, identifier); if (personId) { thread.dataset.person = personId; } else { delete thread.dataset.person; } if (hiddenService) { hiddenService.value = service; } if (hiddenIdentifier) { hiddenIdentifier.value = normalizeIdentifierForService(service, identifier); } if (hiddenPerson) { hiddenPerson.value = personId; } if (metaLine) { metaLine.textContent = titleCase(service) + " · " + normalizeIdentifierForService(service, identifier); } clearReplyTarget(); stopPendingCommandPolling(); hidePersistentCancelButton(); hideTransientCancelButton(); if (state.socket) { try { state.socket.close(); } catch (_err) { // Ignore close failures. } state.socket = null; } state.websocketReady = false; lastTs = 0; thread.dataset.lastTs = "0"; thread.innerHTML = ""; ensureEmptyState("Loading messages..."); applyTyping({ typing: false }); poll(true); setupWebSocket(); }; autosize(); textarea.addEventListener("input", autosize); if (manualConfirm) { manualConfirm.addEventListener("change", function () { const confirmed = !!manualConfirm.checked; if (armInput) { armInput.value = confirmed ? "1" : "0"; } if (confirmInput) { confirmInput.value = confirmed ? "1" : "0"; } if (sendButton) { sendButton.disabled = !sendCapable || !confirmed; } }); manualConfirm.dispatchEvent(new Event("change")); } if (replyClearBtn) { replyClearBtn.addEventListener("click", function () { clearReplyTarget(); textarea.focus(); }); } if (platformSelect) { platformSelect.addEventListener("change", function () { const selected = platformSelect.options[platformSelect.selectedIndex]; if (!selected) { return; } const targetUrl = buildComposeUrl( selected.value || "", selected.dataset.identifier || "", selected.dataset.person || "" ); switchThreadContext( selected.value || "", normalizeIdentifierForService(selected.value || "", selected.dataset.identifier || ""), selected.dataset.person || "", targetUrl ); }); } if (contactSelect) { contactSelect.addEventListener("change", function () { const selected = contactSelect.options[contactSelect.selectedIndex]; if (!selected) { return; } const serviceMap = parseServiceMap(selected); const currentService = String(thread.dataset.service || "").toLowerCase(); const availableServices = Object.keys(serviceMap); let selectedService = currentService || String(selected.dataset.service || ""); let selectedIdentifier = String(serviceMap[selectedService] || "").trim(); if (!selectedIdentifier) { selectedService = String(selected.dataset.service || selectedService).trim().toLowerCase(); selectedIdentifier = String(serviceMap[selectedService] || "").trim(); } if (!selectedIdentifier && availableServices.length) { selectedService = availableServices[0]; selectedIdentifier = String(serviceMap[selectedService] || "").trim(); } const targetUrl = buildComposeUrl( selectedService, selectedIdentifier, selected.dataset.person || "" ); switchThreadContext( selectedService, normalizeIdentifierForService(selectedService, selectedIdentifier), selected.dataset.person || "", targetUrl ); }); } thread.addEventListener("click", function (event) { const replyButton = event.target.closest && event.target.closest(".compose-reply-btn"); if (replyButton) { const row = replyButton.closest(".compose-row"); if (row) { setReplyTarget(row.dataset.messageId || "", row.dataset.replySnippet || ""); textarea.focus(); } } }); textarea.addEventListener("keydown", function (event) { if (event.key !== "Enter" || event.shiftKey) { return; } event.preventDefault(); if (sendButton && sendButton.disabled) { setStatus("Enable send confirmation before sending.", "warning"); return; } form.requestSubmit(); }); form.addEventListener("submit", function () { if (sendButton && sendButton.disabled) { return; } showTransientCancelButton(); }); form.addEventListener("htmx:afterRequest", function () { hideTransientCancelButton(); textarea.focus(); }); state.eventHandler = function (event) { const detail = (event && event.detail) || {}; const sourcePanelId = String(detail.panel_id || ""); if (sourcePanelId && sourcePanelId !== panelId) { return; } poll(true); }; document.body.addEventListener("composeMessageSent", state.eventHandler); state.sendResultHandler = function (event) { const detail = (event && event.detail) || {}; const sourcePanelId = String(detail.panel_id || ""); if (sourcePanelId && sourcePanelId !== panelId) { return; } hideTransientCancelButton(); if (detail.ok) { flashCompose("is-send-success"); textarea.value = ""; clearReplyTarget(); autosize(); poll(true); } else { flashCompose("is-send-fail"); if (detail.message) { setStatus(detail.message, detail.level || "danger"); } } textarea.focus(); }; document.body.addEventListener("composeSendResult", state.sendResultHandler); state.commandIdHandler = function (event) { const detail = (event && event.detail) || {}; const commandId = String( detail.command_id || (detail.composeSendCommandId && detail.composeSendCommandId.command_id) || "" ).trim(); if (commandId) { startPendingCommandPolling(commandId); } }; document.body.addEventListener("composeSendCommandId", state.commandIdHandler); bindReplyReferences(thread); applyTyping(parseJsonSafe(panel.dataset.initialTyping || "{}", {})); ensureEmptyState(); scrollToBottom(true); setupWebSocket(); state.pollTimer = setInterval(function () { if (!document.getElementById(panelId)) { destroyPanelState(state); return; } poll(false); }, 4000); }; const initAll = function (root) { collectPanels(root).forEach(function (panel) { initPanel(panel); }); }; window.GIAComposePanel = { initAll: initAll, initPanel: initPanel, destroyPanel: function (panelId) { destroyPanelState(window.giaComposePanels[panelId]); }, }; document.addEventListener("DOMContentLoaded", function () { initAll(document); }); if (document.body) { document.body.addEventListener("htmx:afterSwap", function (event) { initAll((event && event.target) || document); }); } initAll(document); })();