Skip to main content

Hilo de mensajes (ChatConversationComponent)

Panel central de la pantalla de Chat de tickets: cabecera del ticket, historial de mensajes del contacto y compositor. Esta página cubre la cabecera y el hilo (carga, tipos de mensaje, menú contextual, selección, reenvío, destacados, visor de medios); el compositor está en Compositor y adjuntos y las acciones del ticket en Acciones del ticket. El componente (chat-conversation.component.ts, unas 1900 líneas) se usa en tres contextos con los mismos inputs: la columna principal, el pop-out y la vista de solo lectura.

Archivos​

ArchivoRol
components/chat/chat-conversation/chat-conversation.component.*Cabecera, hilo y compositor
components/chat/message-context-menu/message-context-menu.component.*Menú por mensaje (overlay CDK)
components/chat/media-viewer/media-viewer.component.*Visor modal de imagen y video
components/chat/forward-tickets-panel/forward-tickets-panel.component.*Elegir ticket destino para reenviar
shared/components/audio-playerReproductor de audio (declarado en ChatModule)
core/services/ui/chat-state-manager.service.ts, chat-scroll-manager.service.ts, chat-date-formatter.service.ts, chat-user-helper.service.ts, chat-message-builder.service.ts, media-download.service.tsAyudantes de paginación, scroll, fechas, usuarios que respondieron y descarga

Inputs y outputs​

NombreEfecto
userId (obligatorio)_id del ticket; al cambiar, sale de la sala anterior, reinicia y recarga
conversationChatConversation con contacto, estado, asignado, bloqueo y canal; si cambia el contacto, recarga el hilo
hideHeaderOculta la cabecera (pop-out y vista de solo lectura)
forceReadOnlyFuerza solo lectura (vista de solo lectura)
showLabelsBarMuestra la barra de etiquetas del contacto
close, openDetailsPanel, ticketAction, labelsQuickAssignEventos hacia la lista

isReadOnly es verdadero con forceReadOnly o cuando ticketStatus es resolved, closed o cancelled: se muestra el banner de solo lectura y no se pinta el compositor.

Cabecera​

  • Avatar o iniciales; clic en el perfil emite openDetailsPanel. En móvil un botón de volver emite close.
  • Nombre del contacto, icono del canal con tooltip (channelOption, channelTooltip), departamento con su color (o "sin departamento") y operador asignado (o "sin asignar") con tooltip assignedTooltip.
  • Indicador de agentes que respondieron: ChatUserHelperService extrae del hilo a los usuarios que han contestado; con más de uno muestra un contador y un desplegable con sus nombres y "en nombre de".
  • Botón Resolver (solo si no es solo lectura; emite la acción resolve) y un menú desplegable con app-ticket-actions-menu, al que se le pasa ticketStatus, lockedBy, isOwner e isLockedByMe. La cabecera no enlaza canTransfer, por lo que "Transferir" siempre aparece aquí (ver Acciones del ticket).
  • Debajo, app-chat-labels-bar con las etiquetas del contacto y botón de asignación rápida (ver Etiquetas).

Carga del historial​

loadMessages pide GET /messages/:contactId con la página actual (ChatStateManagerService, 20 por página): el hilo muestra los mensajes de todos los tickets del contacto. Los resultados se anteponen a los ya cargados. En la primera carga baja al final, emite join-ticket, mark-read y message-delivered con los mensajes del contacto cuyo ack es sent. Al llegar al tope (onScroll) pide la página anterior y restaura la posición de scroll. currentLoadingRequest$ cancela la petición en vuelo cuando cambia el ticket.

ticketMessageToChat mapea un TicketMessage a ChatMessage: el sendType (agent, contact, system) se traduce a fromType (user, player, system); un mensaje de sistema con isInternal === false se trata como enviado por el sistema al contacto (isSystemSent) y se muestra como mensaje de agente. Se conservan media, caption, parent (cita), viewOnce, sharedContacts, ack y failureReason.

Mensajes en vivo​

Se suscribe a newMessage$, newFileMessage$ y newNote$ filtrando por contactId === conversation.otherParticipantId, agrega el mensaje, baja el scroll y, si es del contacto, confirma entrega (markDelivered). messageAckChanged$ (filtrado por ticket) actualiza el ack y el motivo de fallo (unknown por defecto) con applyAck. Tras una reconexión (reconnected$) el hilo se vacía y se vuelve a cargar; antes, cada autenticación vuelve a unirse a la sala del ticket. Mientras el socket no está autenticado el compositor sigue visible pero con estilo desconectado y sendMessage no hace nada, conservando el texto escrito. Detalle en Tiempo real y estado.

Tipos de mensaje que renderiza​

TipoComportamiento
Sistema (fromType === 'system')Burbuja de sistema con icono de documento, hora y contenido saneado
Nota interna (messageType === 'note')Burbuja propia con la etiqueta de notas (notes.title, icono de ojo tachado), contenido saneado y autor
TextoBurbuja con renderMessageMarkup (formato tipo WhatsApp y enlaces); si supera 250 caracteres (CHARACTERS_PER_EXPANSION) se corta con "ver más"
Cita (parent)Bloque con remitente y texto citado; clic salta al mensaje original (jumpToMessage)
ImagenMiniatura; clic abre MediaViewerComponent
VideoMiniatura sin controles; clic abre el visor
Audioapp-audio-player propio
Documento y otros archivosBotón con nombre y tamaño (formatFileSize); clic descarga con MediaDownloadService. Mientras no hay media.url se muestra el texto del mensaje como marcador
UbicaciónTarjeta con título y URL
Contacto compartido (sharedContacts)Una tarjeta por contacto con nombre y teléfono; el botón busca el contacto por teléfono (GET /contacts/get-all con phone) y abre ContactEditModalComponent para ver o agregar (crear con createContact o actualizar con updateContact y assignLabels)
Vista única (viewOnce)Texto fijo "vista única" en lugar del contenido

Además: separadores de fecha, hora por mensaje, tilde de acuse en los propios (sent, delivered, read, failed con tooltip del motivo; ver Estado failed) y, debajo del hilo, app-scheduled-message-strip con los envíos programados pendientes del agente (hermano de la lista de mensajes, nunca mezclado con ella; ver Mensajes programados).

Menú contextual (MessageContextMenuComponent)​

Overlay CDK abierto desde el botón de cada burbuja; se cierra con clic fuera o Escape. Opciones:

OpciónEfecto
ReenviarEntra en modo selección con ese mensaje marcado
ResponderstartReply: el compositor muestra la barra de cita
SeleccionarEntra en modo selección
Destacar / quitar destacadoAlterna con StarredMessageService.toggle(id, 'contact'), optimista y con rollback y toast si falla
DescargarSolo si el mensaje tiene media.url; usa MediaDownloadService
Copiar textoSolo si no tiene medio con URL; copia el texto sin HTML y avisa con un toast
EliminarSolo mensajes propios y con MESSAGE_DELETE; confirmación con ConfirmDialogComponent y DELETE /messages/remove/:id; se quita optimistamente y se restaura con toast de error si falla

Selección, copia, descarga y reenvío​

El modo selección permite marcar varios mensajes (toggleMessageSelection). Sobre la selección, ordenada por fecha:

  • Copiar: arma una línea por mensaje (_formatMessageLine), copia como texto y HTML con ClipboardItem y cae a texto plano si falla.
  • Descargar: genera mensajes_<marca de tiempo>.txt en el navegador.
  • Reenviar: abre ForwardTicketsPanelComponent, que lista los tickets abiertos (GET /tickets/get-all con status=open, excluyendo el actual), con búsqueda local por nombre o identificador y una confirmación. onForwardConfirmed emite por socket un send-message por cada mensaje de texto al ticket elegido; los mensajes con medio se reenvían como el texto fijo [archivo]. Después sale del modo selección.

Mensajes destacados y salto​

Al abrir el hilo se cargan los ids destacados del contacto (GET /starred-messages/ids?contactId=) para pintar la estrella. jumpToMessage(id) desplaza y resalta el mensaje durante 2 s (STARRED_MESSAGE_HIGHLIGHT_DURATION_MS) si ya está cargado; si no, pide su ventana con GET /messages/:id/window, la fusiona ordenada por fecha y entonces resalta. Falla en silencio si la petición falla. Lo usan la pestaña de destacados de la lista, las citas y el enlace profundo ?chatId=&highlightMessageId=. La página global de destacados está en Perfil.

Visor de medios (MediaViewerComponent)​

Modal NgbModal (size: 'xl') con url, kind (image o video) y filename. Se cierra con Escape o clic en el fondo (no al hacer clic sobre el medio ni sobre los controles del video) y ofrece descargar.

Pruebas​

SpecCubre
chat-conversation.component.spec.tsResponder (con y sin parentId), mensaje vacío, indicador de envío programado en el estado de la tienda, rehidratación de pendientes al abrir y al cambiar de ticket, reunión a la sala en cada reautenticación, conservación del texto con el socket caído, recarga tras reconexión, botón de programar, onScheduleSend, mapeo de parent, adjuntos con leyenda, acciones de descarga de medios, destacados (siembra, alternar con rollback, salto con y sin ventana), getters de dueño y bloqueo, enlaces del menú con el bloqueo, channelOption, insignia de departamento e indicador del operador asignado
message-context-menu.component.spec.tsCopiar frente a descargar, tipos de medio, caída a copiar sin URL, evento de descarga, eliminar solo en mensajes propios y etiqueta de destacado
media-viewer.component.spec.tsImagen sin reproductor, video con controles y cierre por Escape o clic de fondo

No hay specs para forward-tickets-panel, la selección múltiple ni el modal de contacto compartido.

Gotchas​

  • Reenviar solo envía texto: un archivo pasa a ser el literal [archivo].
  • El hilo agrupa por contacto: al ver el historial se mezclan mensajes de tickets distintos del mismo contacto, y el ticket userId solo determina la sala del socket y a dónde se envía.
  • Eliminar un mensaje es optimista: el mensaje desaparece antes de la respuesta y reaparece al final de la lista si el backend rechaza la petición.
  • Los textos de varios toasts (copiado, reenviado, error al subir) están escritos en español fijo en el componente y no pasan por i18n.