feat: chat izq/der en tickets, correos con ultimo mensaje, dashboard filtrable solo admin/supervisor
This commit is contained in:
1 parent
01173d973e
commit
8b59680588
7 files changed
+526
-41
No files matched your search
@@ -0,0 +1,158 @@
|
||||
// Utilidades para mostrar y enviar solo el mensaje actual de un correo,
|
||||
// sin el historial citado (respuestas encadenadas, forwards, firmas).
|
||||
//
|
||||
// Se usa en:
|
||||
// - Hilo del ticket: correos enviados/recibidos muestran solo el último
|
||||
// mensaje con botón "Ver mensaje completo".
|
||||
// - Respuesta del agente: antes de POST /comments se limpia el texto para
|
||||
// enviar/guardar solo lo escrito (revisión futura de casos).
|
||||
|
||||
const QUOTE_SPLIT_PATTERNS: RegExp[] = [
|
||||
// "On Mon, 1 Jan 2024 at 10:00, Juan <j@x.cl> wrote:"
|
||||
/^\s*On\s.+wrote:\s*$/i,
|
||||
// "El 1 ene 2024, Juan <j@x.cl> escribió:"
|
||||
/^\s*El\s.+escribi[óo]:\s*$/i,
|
||||
// Outlook / Gmail headers del mensaje original
|
||||
/^\s*-{2,}\s*Mensaje original\s*-{2,}\s*$/i,
|
||||
/^\s*-{2,}\s*Original Message\s*-{2,}\s*$/i,
|
||||
/^\s*-{2,}\s*Forwarded message\s*-{2,}\s*$/i,
|
||||
/^\s*-{2,}\s*Mensaje reenviado\s*-{2,}\s*$/i,
|
||||
/^\s*_{2,}\s*$/,
|
||||
// Cabeceras típicas citadas
|
||||
/^\s*De:\s+.+$/i,
|
||||
/^\s*From:\s+.+$/i,
|
||||
/^\s*Enviado:\s+.+$/i,
|
||||
/^\s*Sent:\s+.+$/i,
|
||||
/^\s*Para:\s+.+$/i,
|
||||
/^\s*To:\s+.+$/i,
|
||||
/^\s*Asunto:\s+.+$/i,
|
||||
/^\s*Subject:\s+.+$/i,
|
||||
];
|
||||
|
||||
const MAX_PREVIEW_CHARS = 800;
|
||||
|
||||
export function stripHtmlToText(html: string): string {
|
||||
if (typeof window !== "undefined" && typeof DOMParser !== "undefined") {
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
// Elimina bloques típicos de historial citado antes de extraer texto
|
||||
doc
|
||||
.querySelectorAll(
|
||||
"blockquote, .gmail_quote, .gmail_attr, .moz-cite-prefix, #is_reply, .quoted, [data-quoted]"
|
||||
)
|
||||
.forEach((el) => el.remove());
|
||||
// <br> y párrafos como saltos de línea
|
||||
doc.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
|
||||
doc.querySelectorAll("p, div, li, tr").forEach((el) => {
|
||||
el.prepend("\n");
|
||||
});
|
||||
return (doc.body.textContent || "").replace(/\n{3,}/g, "\n\n").trim();
|
||||
} catch {
|
||||
// cae al regex de abajo
|
||||
}
|
||||
}
|
||||
return html
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n")
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Quita firmas comunes tipo "-- \nJuan" al final. */
|
||||
function stripSignature(lines: string[]): string[] {
|
||||
const idx = lines.findIndex((l) => l.trim() === "--" || l.trim() === "—");
|
||||
if (idx >= 0 && idx > Math.max(0, lines.length - 8)) {
|
||||
return lines.slice(0, idx);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el último mensaje (actual) de un cuerpo de correo con historial.
|
||||
* Corta en la primera línea que parezca inicio de cita y descarta líneas
|
||||
* que empiezan con ">". Si no hay cita detectable, recorta por longitud.
|
||||
*/
|
||||
export function extractLatestMessage(body: string): {
|
||||
preview: string;
|
||||
hasMore: boolean;
|
||||
} {
|
||||
const text = (body || "").replace(/\r\n/g, "\n");
|
||||
if (!text.trim()) return { preview: "", hasMore: false };
|
||||
|
||||
const lines = stripSignature(text.split("\n"));
|
||||
const kept: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Línea citada estilo ">"
|
||||
if (/^>/.test(trimmed) || /^>/.test(trimmed)) break;
|
||||
// Inicio de bloque citado ("On ... wrote:", "De:", "----- Mensaje original -----", ...)
|
||||
if (QUOTE_SPLIT_PATTERNS.some((re) => re.test(line))) break;
|
||||
kept.push(line);
|
||||
}
|
||||
|
||||
let preview = kept.join("\n").trim();
|
||||
const hadQuoteCut = kept.length < lines.length;
|
||||
|
||||
// Sin corte por cita pero texto larguísimo: recorte por longitud
|
||||
if (!hadQuoteCut && preview.length > MAX_PREVIEW_CHARS) {
|
||||
const cut = preview.slice(0, MAX_PREVIEW_CHARS);
|
||||
const lastSpace = cut.lastIndexOf(" ");
|
||||
preview = (lastSpace > 400 ? cut.slice(0, lastSpace) : cut).trim() + "…";
|
||||
return { preview, hasMore: true };
|
||||
}
|
||||
|
||||
if (!preview) {
|
||||
// Todo parecía cita: muestra un recorte del original
|
||||
const fallback = text.trim().slice(0, MAX_PREVIEW_CHARS).trim();
|
||||
return {
|
||||
preview: fallback.length < text.trim().length ? fallback + "…" : fallback,
|
||||
hasMore: fallback.length < text.trim().length,
|
||||
};
|
||||
}
|
||||
|
||||
return { preview, hasMore: hadQuoteCut || preview.length < text.trim().length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia HTML de correo enviado dejando solo el mensaje actual:
|
||||
* elimina <blockquote> y contenedores de cita conocidos. Devuelve HTML.
|
||||
*/
|
||||
export function extractLatestHtml(html: string): { previewHtml: string; hasMore: boolean } {
|
||||
if (typeof window === "undefined" || typeof DOMParser === "undefined") {
|
||||
const text = stripHtmlToText(html);
|
||||
const { preview, hasMore } = extractLatestMessage(text);
|
||||
// Sin DOM no podemos recortar HTML con fidelidad: mostramos texto plano
|
||||
return { previewHtml: "", hasMore: hasMore || preview.length < text.length };
|
||||
}
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
const quoted = doc.querySelectorAll(
|
||||
"blockquote, .gmail_quote, .gmail_attr, .moz-cite-prefix, #is_reply, .quoted, [data-quoted]"
|
||||
);
|
||||
const hasMore = quoted.length > 0;
|
||||
quoted.forEach((el) => el.remove());
|
||||
// También corta marcadores textuales dentro del HTML ("On ... wrote:", etc.)
|
||||
const text = (doc.body.textContent || "").replace(/\r\n/g, "\n");
|
||||
const { hasMore: textCut } = extractLatestMessage(text);
|
||||
const previewHtml = doc.body.innerHTML.trim();
|
||||
return { previewHtml, hasMore: hasMore || textCut };
|
||||
} catch {
|
||||
return { previewHtml: html, hasMore: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia lo que el agente escribió antes de enviar/guardar:
|
||||
* quita historial pegado y líneas citadas. Solo va el mensaje nuevo.
|
||||
*/
|
||||
export function cleanReplyForSend(text: string): string {
|
||||
const { preview } = extractLatestMessage(text || "");
|
||||
return preview.trim();
|
||||
}
|
||||
Reference in new issue
Block a user