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
| Archivo | Rol |
|---|---|
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-player | Reproductor 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.ts | Ayudantes de paginación, scroll, fechas, usuarios que respondieron y descarga |
Inputs y outputs
| Nombre | Efecto |
|---|---|
userId (obligatorio) | _id del ticket; al cambiar, sale de la sala anterior, reinicia y recarga |
conversation | ChatConversation con contacto, estado, asignado, bloqueo y canal; si cambia el contacto, recarga el hilo |
hideHeader | Oculta la cabecera (pop-out y vista de solo lectura) |
forceReadOnly | Fuerza solo lectura (vista de solo lectura) |
showLabelsBar | Muestra la barra de etiquetas del contacto |
close, openDetailsPanel, ticketAction, labelsQuickAssign | Eventos 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 emiteclose. - Nombre del contacto, icono del canal con tooltip (
channelOption,channelTooltip), departamento con su color (o "sin departamento") y operador asignado (o "sin asignar") con tooltipassignedTooltip. - Indicador de agentes que respondieron:
ChatUserHelperServiceextrae 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 conapp-ticket-actions-menu, al que se le pasaticketStatus,lockedBy,isOwnereisLockedByMe. La cabecera no enlazacanTransfer, por lo que "Transferir" siempre aparece aquí (ver Acciones del ticket). - Debajo,
app-chat-labels-barcon 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
| Tipo | Comportamiento |
|---|---|
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 |
| Texto | Burbuja 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) |
| Imagen | Miniatura; clic abre MediaViewerComponent |
| Video | Miniatura sin controles; clic abre el visor |
| Audio | app-audio-player propio |
| Documento y otros archivos | Botó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ón | Tarjeta 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ón | Efecto |
|---|---|
| Reenviar | Entra en modo selección con ese mensaje marcado |
| Responder | startReply: el compositor muestra la barra de cita |
| Seleccionar | Entra en modo selección |
| Destacar / quitar destacado | Alterna con StarredMessageService.toggle(id, 'contact'), optimista y con rollback y toast si falla |
| Descargar | Solo si el mensaje tiene media.url; usa MediaDownloadService |
| Copiar texto | Solo si no tiene medio con URL; copia el texto sin HTML y avisa con un toast |
| Eliminar | Solo 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 conClipboardItemy cae a texto plano si falla. - Descargar: genera
mensajes_<marca de tiempo>.txten el navegador. - Reenviar: abre
ForwardTicketsPanelComponent, que lista los tickets abiertos (GET /tickets/get-allconstatus=open, excluyendo el actual), con búsqueda local por nombre o identificador y una confirmación.onForwardConfirmedemite por socket unsend-messagepor 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
| Spec | Cubre |
|---|---|
chat-conversation.component.spec.ts | Responder (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.ts | Copiar 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.ts | Imagen 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
userIdsolo 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.