Skip to main content

Compositor, adjuntos y envío programado

Parte inferior de ChatConversationComponent (ver Hilo de mensajes). Solo se pinta cuando el ticket no es de solo lectura (resuelto, cerrado, cancelado o vista forzada). Todo el envío del agente sale por socket (ChatSocketService), no por REST: ver Tiempo real y estado para el ciclo de vida del socket y Chat para el contexto.

Archivos​

ArchivoRol
chat-conversation/chat-conversation.component.*Todo el compositor: texto, emoji, respuestas rápidas, firma, nota, adjuntos, audio
schedule-send-button/schedule-send-button.component.*Botón "Programar envío" (standalone)
schedule-message-dialog/schedule-message-dialog.component.* y .service.tsDiálogo de fecha, hora y texto (standalone)
scheduled-message-strip/scheduled-message-strip.component.*Fila de envíos programados pendientes del ticket (standalone)
core/services/api/file-api.service.tsSubida y validación de archivos
core/services/api/quick-response.service.tsBúsqueda de respuestas rápidas
core/states/scheduled-message/*Slice de mensajes programados

Barra de herramientas​

Fila de botones a la izquierda del cuadro de texto, en este orden:

BotónEstadoEfecto
EmojishowEmojiPickerCarga bajo demanda emoji-mart (em-emoji-picker, tema oscuro) y añade el emoji al final del texto
Adjuntar (+)showAttachMenuMenú con Imagen, Video y Documento; muestra un spinner mientras sube (isUploadingFile)
Nota internaisNoteModeEl siguiente envío es una nota interna (evento send-note); se desactiva al enviar
FirmasignatureEnabled (activa por defecto)Indica si el mensaje se firma con el nombre del agente
Respuestas rápidasquickResponsesManualOpenAbre el selector sin escribir /
Programar envíoVer más abajoAbre el diálogo de programación

resetConversation restaura nota, firma, selector y respuesta citada al cambiar de ticket.

Envío de texto​

sendMessage ignora el texto vacío y no hace nada si el socket no está autenticado (isConnectionLive), sin borrar lo escrito. Según el modo:

ModoEmisiónPayload
Mensajesend-message{ ticketId, content, signature, parentId? }
Notasend-note{ ticketId, content }

Enter envía; Shift+Enter inserta salto de línea. El textarea crece hasta 120 px. signature viaja siempre; en el backend, un valor ausente se interpreta como "firmar" (SendMessageDto.signature) y false evita la firma. El hilo recibe el mensaje de vuelta por el evento new_message (no hay eco local).

Responder (cita)​

startReply(message) guarda replyingTo; el compositor muestra una barra con el remitente y el texto citado y un botón para cancelar. El parentId se incluye en el mensaje de texto, en los adjuntos y en el envío programado. Se limpia tras enviar.

Respuestas rápidas​

  • Escribir / al inicio del texto dispara una búsqueda con debounce de 200 ms: GET /quick-responses/get-all con search (lo escrito tras /), ticketId, página 1 y límite 10. El backend filtra por el departamento del ticket y solo devuelve las activas; ver Respuestas rápidas y Quick response.
  • El selector muestra /atajo y el mensaje con formato; ArrowUp y ArrowDown navegan, Enter elige, Escape cierra. Elegir una respuesta reemplaza el texto del compositor por el mensaje de la respuesta y devuelve el foco al textarea.
  • Abierto manualmente muestra "sin resultados" si no hay coincidencias.

Adjuntos​

Flujo común de _uploadAndSend(file, filename?):

  1. Validación en el cliente con FileApiService (si falla, toast de error y no se sube).
  2. POST /files/upload (multipart, campo file) que devuelve el archivo con su _id.
  3. Emisión send-file con { ticketId, fileId, parentId?, caption? }. El caption es el texto que hubiera en el compositor en ese momento; después se limpia el texto y se cancela la cita.
TipoTipos MIME permitidosLímite
Imagenimage/jpeg, image/png, image/gif, image/webp20 MB
Videovideo/mp4, video/webm, video/quicktime, video/x-msvideo, video/x-matroska, video/mpeg, video/3gpp100 MB
Documentoapplication/pdf, application/msword, .docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document)10 MB
AudioSin validación (validateAudio siempre acepta)Sin límite

El mensaje de error de video menciona "MP4, WebM, MOV, AVI o MKV" aunque ALLOWED.video también acepta video/mpeg y video/3gpp. Si la subida falla se muestra "Error al subir el archivo." (texto fijo). El backend decide cómo entregar el archivo según el canal; ver Message y File.

Notas de voz​

Usa getUserMedia y MediaRecorder con estados audioState: idle, recording, paused y preview.

  • startRecording pide el micrófono; si falla, toast "No se pudo acceder al micrófono.".
  • Mientras graba muestra un cronómetro (formatRecordingTime, mm:ss) y permite pausar y reanudar.
  • stopRecording genera un Blob audio/webm y pasa a preview con un <audio> para escucharlo.
  • sendAudio lo sube como audio_<marca de tiempo>.webm por el mismo flujo de adjuntos; cancelAudio libera el micrófono y revoca la URL del blob. ngOnDestroy también cancela.

Envío programado​

Superficie principal de la función descrita en Mensajes programados (la pantalla de "mis envíos" es la secundaria).

  • Botón: app-schedule-send-button solo se pinta si scheduledMessagesEnabled$ es verdadero y el usuario tiene MESSAGE_CREATE. El indicador sale de selectScheduledMessagesEnabled, que lee featureFlags.scheduledMessagesEnabled del perfil del usuario (auth.selectors.ts), no de la configuración del tenant. El botón se deshabilita con el texto vacío y solo emite schedule.
  • Diálogo (ScheduleMessageDialogService.open, modal sm, estático y sin cierre por teclado): campos de texto y fecha/hora con DateTimePickerComponent; confirmar exige texto no vacío y fecha futura. Resuelve { body, scheduledAt } o null si se cancela.
  • Crear: onScheduleSend abre el diálogo con el texto actual y, al confirmar, despacha ScheduledMessageActions.create con ticketId, body, scheduledAt (ISO), messageType (note o chat), signature (solo si la firma está activa y no es nota) y parentId?. Limpia el texto, sale del modo nota y cancela la cita. Endpoint: POST /scheduled-messages/create.
  • Pendientes del ticket: app-scheduled-message-strip muestra los pendientes del ticket (selectPendingByTicket), un bloque hermano de la lista de mensajes para no confundirlos con mensajes enviados. Editar reabre el diálogo con el texto y la fecha actuales y despacha update incluyendo scheduledAt solo si cambió; cancelar despacha cancel. Se rehidratan al abrir el hilo (loadByTicket) y la lista de tickets carga los pendientes propios (loadMine con estado pending) para el contador de las tarjetas.

Pruebas​

SpecCubre
chat-conversation.component.spec.tssendMessage con y sin parentId, texto vacío, conservación del texto con el socket caído, onScheduleSend (diálogo, create, nota, parentId, cancelación), visibilidad del botón, adjuntos con leyenda (manda lo escrito, limpia el compositor, no manda leyenda si está vacío)
schedule-send-button.component.spec.tsEmite al hacer clic y no emite deshabilitado
schedule-message-dialog.component.spec.tsPrecarga de texto y fecha, rechazo de fecha pasada o texto vacío, confirmación y cancelación
scheduled-message-strip.component.spec.tsPendientes por ticket, cancelar, editar (con y sin cambio de fecha)
postpone-dialog.component.spec.tsVer Acciones del ticket

No hay specs para grabación de audio, respuestas rápidas del compositor, selector de emoji ni subida de archivos.

Gotchas​

  • La firma, las notas y los adjuntos no tienen directivas de permiso en el cliente; los controla el backend (MESSAGE_* y el gateway). Solo el envío programado exige MESSAGE_CREATE en la UI.
  • Elegir una respuesta rápida sobrescribe el texto en lugar de insertarlo en la posición del cursor.
  • Un adjunto usa como leyenda cualquier texto pendiente del compositor, incluso si el agente lo escribía para otro mensaje.
  • Los textos de varios toasts y tooltips (adjuntos, firma, nota) están en español fijo en el componente.