/* WAM · interactivity.js — dueño D (núcleo).
Flujos de clic (quick replies, filas de lista, botones de plantilla y de card),
composer libre, indicador "escribiendo…", ticks automáticos y modo
"reproducir demo" paso a paso (con autoPulsar y velocidad). No toca el DOM
al cargar.
Guion frente a sesión: WAM.estado.mensajes es el GUION (persistente, lo que
edita el editor y reproduce la demo). Lo que ocurre al pulsar botones o
escribir en el composer es la SESIÓN (WAM.sesion, solo en memoria): se pinta
con anadirMensajeDOM sin tocar el estado ni localStorage. Cualquier
WAM.render() vuelve al guion: vacía la sesión, cierra la hoja de lista y
cancela flujos y demo en curso. */
window.WAM = window.WAM || {};
(function () {
"use strict";
var CANCELADO = "wam:cancelado";
var TIPOS_VALIDOS = [
"text", "image", "video", "audio", "document", "sticker", "location",
"contact", "template", "buttons", "list", "cta_url", "carousel"
];
var CLAVE_VELOCIDAD = "wam-mockup-v1-velocidad";
var VELOCIDADES = [0.5, 1, 2];
function nada() { /* noop */ }
/* Mensajes de la sesión en curso (efímeros). */
WAM.sesion = [];
var contSesion = 0;
function idSesion() { contSesion++; return "s" + contSesion; }
/* ————————————————— temporizadores cancelables por grupo ————————————————— */
/* Dos grupos: "demo" (reproducción) y "flujo" (clics, composer, ticks).
Cancelar un grupo sube su generación: los pasos pendientes de esa
generación se rechazan con CANCELADO y ya no pueden programar más. */
var grupos = {
demo: { gen: 0, cancelar: [] },
flujo: { gen: 0, cancelar: [] }
};
function grupoDe(nombre) { return grupos[nombre] ? nombre : "flujo"; }
var velocidad = 1;
try {
var guardada = parseFloat(window.localStorage.getItem(CLAVE_VELOCIDAD));
if (VELOCIDADES.indexOf(guardada) !== -1) velocidad = guardada;
} catch (e) { /* sin localStorage */ }
WAM.getVelocidad = function () { return velocidad; };
WAM.setVelocidad = function (v) {
var n = parseFloat(v);
if (VELOCIDADES.indexOf(n) === -1) n = 1;
velocidad = n;
try { window.localStorage.setItem(CLAVE_VELOCIDAD, String(n)); } catch (e) { /* idem */ }
return velocidad;
};
function esperaEn(nombre, ms, gen) {
var g = grupos[grupoDe(nombre)];
return new Promise(function (resolver, rechazar) {
if (gen !== undefined && gen !== g.gen) { rechazar(CANCELADO); return; }
var dur = Math.max(0, (Number(ms) || 0) / velocidad);
var id = setTimeout(function () {
quitar();
if (gen !== undefined && gen !== g.gen) rechazar(CANCELADO);
else resolver();
}, dur);
function cancelar() {
clearTimeout(id);
quitar();
rechazar(CANCELADO);
}
function quitar() {
var i = g.cancelar.indexOf(cancelar);
if (i !== -1) g.cancelar.splice(i, 1);
}
g.cancelar.push(cancelar);
});
}
/* Promise cancelable (SPEC): todo temporizador pasa por aquí.
grupo opcional: "flujo" (por defecto) o "demo". La duración se divide
por la velocidad elegida (0,5x / 1x / 2x). */
WAM.espera = function (ms, grupo) { return esperaEn(grupo, ms); };
/* Contexto de una secuencia: fija grupo y generación al empezar. */
function contexto(nombre) {
var g = grupoDe(nombre);
var gen = grupos[g].gen;
return {
grupo: g,
vivo: function () { return grupos[g].gen === gen; },
esperar: function (ms) { return esperaEn(g, ms, gen); }
};
}
var colas = { demo: Promise.resolve(), flujo: Promise.resolve() };
function cancelarGrupo(nombre) {
var g = grupos[grupoDe(nombre)];
g.gen++;
var copia = g.cancelar.slice();
g.cancelar = [];
for (var i = 0; i < copia.length; i++) copia[i]();
colas[grupoDe(nombre)] = Promise.resolve();
}
function cancelarTodo() {
cancelarGrupo("demo");
cancelarGrupo("flujo");
escritores.clear();
pintarEscribiendo();
}
/* ————————————————— indicador "escribiendo…" (contador de referencias) ————————————————— */
var escritores = new Set();
function pintarEscribiendo() {
if (typeof WAM.setEscribiendo === "function") WAM.setEscribiendo(escritores.size > 0);
}
function empezarEscribir() {
var ficha = {};
escritores.add(ficha);
pintarEscribiendo();
return ficha;
}
function dejarEscribir(ficha) {
if (escritores.delete(ficha)) pintarEscribiendo();
}
async function escribirDurante(ctx, ms) {
var ficha = empezarEscribir();
try { await ctx.esperar(ms); } finally { dejarEscribir(ficha); }
}
/* ————————————————— estado de la demo ————————————————— */
var demoAndando = false;
WAM.demoActiva = function () { return demoAndando; };
function avisarDemo() {
if (typeof WAM.onDemoChange === "function") {
try { WAM.onDemoChange(demoAndando); } catch (e) { /* el hook es opcional */ }
}
}
/* ————————————————— búsqueda de mensajes y filas ————————————————— */
function cuerpoChat() { return document.getElementById("chat-cuerpo"); }
function selectorId(id) {
return '.wa-row[data-id="' + String(id).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]';
}
function filaDe(msg) {
var cuerpo = cuerpoChat();
if (!cuerpo || !msg || typeof msg.id !== "string" || !msg.id) return null;
var filas = cuerpo.querySelectorAll(selectorId(msg.id));
return filas.length ? filas[filas.length - 1] : null;
}
function buscarPorId(id) {
if (!id) return null;
var guion = (WAM.estado && WAM.estado.mensajes) || [];
for (var i = 0; i < guion.length; i++) if (guion[i] && guion[i].id === id) return guion[i];
for (var j = WAM.sesion.length - 1; j >= 0; j--) if (WAM.sesion[j] && WAM.sesion[j].id === id) return WAM.sesion[j];
return null;
}
/* Mensaje de la hoja de lista abierta (guion o sesión). */
var hojaMsg = null;
/* Mensaje al que pertenece un elemento clicable del chat o de la hoja.
Se resuelve por el data-id de su fila (inmune a índices obsoletos y
válido para mensajes de sesión); el índice data-flujo queda de respaldo. */
WAM.mensajeDeElemento = function (el) {
if (!el || !el.closest) return null;
var hoja = el.closest("#sheet");
if (hoja) return hojaMsg;
var fila = el.closest(".wa-row[data-id]");
var msg = fila ? buscarPorId(fila.getAttribute("data-id")) : null;
if (msg) return msg;
var indice = parseInt(el.getAttribute("data-flujo"), 10);
var guion = (WAM.estado && WAM.estado.mensajes) || [];
return isNaN(indice) ? null : (guion[indice] || null);
};
/* Abre la hoja de lista de un mensaje (del guion o de la sesión). */
WAM.abrirLista = function (msg) {
if (!msg || typeof WAM.abrirSheet !== "function") return;
hojaMsg = msg;
var guion = (WAM.estado && WAM.estado.mensajes) || [];
var i = guion.indexOf(msg);
WAM.abrirSheet(i >= 0 ? i : msg);
};
/* ————————————————— pintado incremental ————————————————— */
/* grupo / cola (.grupo / .ultimo) los recalcula anadirMensajeDOM según el DOM. */
function pintar(msg) {
if (typeof WAM.anadirMensajeDOM !== "function") return null;
WAM.anadirMensajeDOM(msg);
var fila = filaDe(msg);
if (fila && msg.cita) pintarCita(fila, msg.cita);
return fila;
}
function anadirSesion(msg) {
WAM.sesion.push(msg);
return pintar(msg);
}
function mensajeOut(texto, cita) {
var msg = {
id: idSesion(),
dir: "out",
tipo: "text",
hora: WAM.horaAhora(),
estado: "sent",
texto: String(texto == null ? "" : texto)
};
if (cita) msg.cita = cita;
return msg;
}
/* Copia una respuesta anidada (Mensaje completo) para poder reutilizarla
cada vez que se vuelve a pulsar el mismo botón. */
function mensajeDesde(origen) {
var copia;
try {
copia = JSON.parse(JSON.stringify(
(origen && typeof origen === "object") ? origen : { tipo: "text", texto: String(origen == null ? "" : origen) }
));
} catch (e) {
copia = { tipo: "text", texto: "" };
}
if (!copia || typeof copia !== "object" || Array.isArray(copia)) copia = { tipo: "text", texto: "" };
if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text";
copia.id = idSesion();
copia.dir = "in";
copia.hora = WAM.horaAhora();
copia.estado = "sent";
delete copia.cita;
return copia;
}
/* ————————————————— cita de la respuesta (quoted reply) ————————————————— */
function vacio(v) { return v === undefined || v === null || String(v).trim() === ""; }
function sinFormato(s) {
return String(s == null ? "" : s)
.replace(/```([\s\S]*?)```/g, "$1")
.replace(/`([^`\n]+)`/g, "$1")
.replace(/\*([^*\n]+)\*/g, "$1")
.replace(/(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g, "$1$2")
.replace(/(^|[^\w])~([^~\n]+)~(?=[^\w]|$)/g, "$1$2")
.trim();
}
function miniaturaSegura(u) {
if (vacio(u)) return "";
var s = String(u);
if (/^data:video\//i.test(s)) return "";
if (typeof WAM.urlSegura === "function" && !WAM.urlSegura(s, "media")) return "";
return s;
}
/* { nombre, titulo, extracto, miniatura, foto } del mensaje original. */
function citaDe(msg, opciones) {
if (!msg) return null;
var cuenta = (WAM.estado && WAM.estado.cuenta) || {};
var cita = {
nombre: vacio(cuenta.nombre) ? "Chat" : String(cuenta.nombre),
titulo: "",
extracto: "",
miniatura: "",
foto: false
};
var op = opciones || {};
var partes = [];
if (msg.tipo === "list") {
cita.titulo = sinFormato(msg.header);
partes = [msg.body, msg.footer];
} else if (msg.tipo === "carousel") {
var card = (msg.cards || [])[op.card] || {};
partes = [vacio(card.texto) ? msg.body : card.texto];
cita.miniatura = miniaturaSegura(card.media);
if (!cita.miniatura && vacio(card.media) && typeof WAM.svgPlaceholder === "function") {
cita.miniatura = WAM.svgPlaceholder("");
}
cita.foto = true;
} else {
if (msg.headerTipo === "texto") cita.titulo = sinFormato(msg.headerTexto);
partes = [msg.body];
if (msg.headerTipo === "imagen") {
cita.miniatura = vacio(msg.headerMedia)
? (typeof WAM.svgPlaceholder === "function" ? WAM.svgPlaceholder("") : "")
: miniaturaSegura(msg.headerMedia);
cita.foto = true;
} else if (msg.headerTipo === "video") {
cita.foto = true;
}
}
cita.extracto = partes.filter(function (p) { return !vacio(p); }).map(sinFormato).join("\n");
if (!cita.extracto && !cita.titulo) cita.extracto = cita.foto ? "Foto" : "Mensaje";
return cita;
}
var SVG_CAMARA = '';
/* Bloque de cita dentro de la burbuja OUT; todo por textContent (sin XSS). */
function pintarCita(fila, cita) {
var burbuja = fila.querySelector(".bubble");
if (!burbuja || burbuja.querySelector(".wa-cita")) return;
var caja = document.createElement("div");
caja.className = "wa-cita";
var txt = document.createElement("div");
txt.className = "wa-cita-txt";
var nombre = document.createElement("div");
nombre.className = "wa-cita-nombre";
nombre.textContent = cita.nombre;
txt.appendChild(nombre);
if (cita.titulo) {
var tit = document.createElement("div");
tit.className = "wa-cita-titulo";
tit.textContent = cita.titulo;
txt.appendChild(tit);
}
if (cita.extracto) {
var ext = document.createElement("div");
ext.className = "wa-cita-extracto";
if (cita.foto) {
var cam = document.createElement("span");
cam.innerHTML = SVG_CAMARA; /* constante propia, sin datos del usuario */
if (cam.firstChild) ext.appendChild(cam.firstChild);
}
ext.appendChild(document.createTextNode(cita.extracto));
txt.appendChild(ext);
}
caja.appendChild(txt);
if (cita.miniatura) {
var img = document.createElement("img");
img.className = "wa-cita-mini";
img.alt = "";
img.draggable = false;
img.src = cita.miniatura;
caja.appendChild(img);
}
burbuja.insertBefore(caja, burbuja.firstChild);
burbuja.classList.add("wa-con-cita");
}
/* ————————————————— ticks automáticos ————————————————— */
/* Ticks sent → delivered → read sobre la burbuja OUT indicada (por data-id,
para que dos envíos solapados avancen cada uno su propio tick). */
function repintarTickDe(msg, estado) {
if (typeof WAM.tick !== "function") return;
var fila = filaDe(msg);
if (!fila || !fila.classList.contains("out")) return;
var tick = fila.querySelector(".tick");
if (tick && tick.parentNode) {
/* WAM.tick devuelve un