| Resumen | Modal de vista previa de temas: abre e interactúa con los temas sin salir de la lista de temas | |
| Vista previa | Theme Creator | |
| Repositorio | GitHub - VaperinaDEV/discourse-topic-preview-modal: Open a topic directly from the topic list in a native Discourse modal, read and interact with the topic, and then continue browsing the list without navigating away from it. · GitHub | |
| Guía de instalación | Cómo instalar un tema o componente de tema | |
| ¿Nuevo en los temas de Discourse? | Guía para principiantes sobre el uso de temas de Discourse |
Instalar este componente de tema
Modal de vista previa de temas: abre e interactúa con los temas sin salir de la lista de temas
He creado un nuevo componente de tema de Discourse llamado Modal de vista previa de temas.
La idea es bastante simple:
Abrir un tema directamente desde la lista de temas en un modal nativo de Discourse, leer e interactuar con el tema y, a continuación, continuar navegando por la lista sin salir de ella.
Todo comenzó en Facebook-style Topic Modal - Is it better?, pero al final requirió una integración bastante extensa con los sistemas de temas, flujo de publicaciones, compositor, modal, marcadores, enrutamiento, presencia, seguimiento de lectura y precarga de Discourse.
¿Por qué?
El flujo normal de Discourse es:
- Estás navegando por una lista de temas.
- Haces clic en un tema.
- Discourse navega a
/t/.... - Lees/contestas/interactúas con el tema.
- Vuelves a la lista de temas.
Para muchos flujos de trabajo, esto es perfectamente adecuado.
Sin embargo, cuando se navega por una lista de temas con mucho tráfico, a veces solo quiero inspeccionar rápidamente un tema, leer algunas publicaciones, comprobar las últimas respuestas, reaccionar a algo o responder a una pregunta rápida.
Para ese caso de uso, salir de la lista de temas resulta innecesariamente costoso.
Por lo tanto, el objetivo de este componente era hacer que la lista de temas se comportara más como una bandeja de entrada:
lista de temas → vista previa → interacción → cerrar → continuar exactamente donde te habías quedado.
Qué hace
La vista previa no es solo un extracto estático.
Representa los componentes de publicación reales de Discourse dentro de un DModal nativo.
Esto significa que los usuarios pueden:
- leer publicaciones
- desplazarse por el tema
- cargar publicaciones anteriores
- cargar más publicaciones debajo
- reaccionar a las publicaciones
- marcar publicaciones con marcador
- citar texto
- responder al tema
- responder a publicaciones individuales
- editar publicaciones cuando esté permitido
- eliminar/recuperar publicaciones cuando esté permitido
- reportar publicaciones
- ver el historial de publicaciones
- realizar varias acciones normales de publicación
- ver la presencia del tema
- seguir enlaces a otras publicaciones dentro del mismo tema
- saltar directamente a la publicación correspondiente
- abrir el tema completo cuando sea necesario
La intención es que la vista previa se sienta lo más parecida posible a abrir realmente el tema.
Dos modos de activación
Hay dos formas de abrir la vista previa.
1. Fila completa de la lista de temas
Este es el valor predeterminado.
Toda la fila de la lista de temas se vuelve clicable, mientras que los elementos interactivos comunes como:
- tarjetas de usuario
- participantes
- enlaces de categoría
- etiquetas
- enlaces de estado del tema
- selección masiva
quedan excluidos del activador del modal.
Esto hace que la experiencia sea muy rápida al navegar por una lista de temas.
2. Botón de expansión explícito
Alternativamente, el componente puede representar un pequeño icono de expansión a través de una salida de complemento de Discourse. Los temas personalizados pueden simplemente crear una nueva <PluginOutlet /> para mostrar el activador.
En este modo, el comportamiento normal de la lista de temas permanece completamente intacto.
El usuario hace clic en el icono de expansión para abrir la vista previa, mientras que hacer clic en el título del tema sigue realizando la navegación normal de Discourse.
Esto es útil si un sitio quiere preservar el modelo de interacción estándar de la lista de temas.
La configuración es:
trigger_style:
row
o:
trigger_style:
button
Al usar el modo de botón, la salida también es configurable.
La vista previa comienza en la posición no leída del usuario
Uno de los detalles importantes es que el modal no carga simplemente la primera publicación.
Cuando un tema ya ha sido leído parcialmente, la vista previa calcula:
last_read_post_number + 1
y se abre alrededor de esa publicación.
Así que si un tema tiene 200 publicaciones y el usuario ha leído hasta la publicación #165, abrir la vista previa comienza alrededor de la #166.
Esto hace que la vista previa sea mucho más útil para la navegación del mundo real.
También significa que el componente tiene que lidiar con ambos lados del flujo de publicaciones:
- cargar publicaciones anteriores cuando sea necesario
- cargar publicaciones más recientes debajo
El botón Publicaciones anteriores se muestra cuando hay publicaciones por encima del rango actualmente cargado, mientras que un centinela IntersectionObserver carga automáticamente más publicaciones cuando el usuario llega al final.
Precarga
Una de las partes más grandes del componente es su sistema de precarga.
El problema con un modal como este es que el usuario espera que se sienta instantáneo.
Si solo comenzamos a cargar el tema después de que el usuario hace clic, el modal aún puede pasar un tiempo notable esperando a la red.
En su lugar, el componente puede precargar proactivamente los temas mientras el usuario navega por la lista.
Cuando una fila de tema se acerca al viewport, un IntersectionObserver puede programar una precarga.
Hay varias salvaguardas para evitar que esto se convierta en tráfico de fondo descontrolado.
Retardo (Debouncing)
Un tema no desencadena inmediatamente una solicitud solo porque apareció brevemente en el viewport.
El componente espera el período de retardo configurado.
Predeterminado:
400 ms
Esto es particularmente útil al desplazarse rápidamente por una lista de temas larga.
Margen raíz
La precarga puede comenzar ligeramente antes de que el tema entre realmente en el viewport.
Predeterminado:
50 px
Esto le da a la solicitud un pequeño adelanto.
Límite de solicitudes concurrentes
El número de precargas simultáneas está limitado.
Predeterminado:
2
La configuración permite entre 1 y 6 precargas concurrentes.
Presupuesto por minuto
También hay un segundo mecanismo de protección:
max_prefetches_per_minute
El valor predeterminado es:
15
Así que incluso si el usuario continúa desplazándose por cientos de temas, el componente no generará continuamente solicitudes especulativas.
0 deshabilita el límite.
La precarga se puede deshabilitar por completo
Si un sitio no quiere ningún tráfico de red especulativo:
enable_prefetch = false
El componente continúa funcionando normalmente. Los temas simplemente se cargan cuando se abre la vista previa.
Los datos de precarga se mantienen separados de la navegación normal de temas
Aquí hay un detalle de implementación importante.
La respuesta precargada no se escribe inmediatamente en la clave de precarga normal topic_<id> de Discourse.
En su lugar, el componente utiliza su propio espacio de nombres:
topic-preview-modal:prefetch:<topicId>
Solo cuando el usuario abre realmente la vista previa, la promesa precargada se promueve a la clave de precarga central del tema.
Esto es intencional.
La vista previa puede estar cargando un tema comenzando desde last_read_post_number + 1, y no quiero que esa respuesta específica de la vista previa se filtre en una navegación de ruta de tema normal.
Así que el ciclo de vida es esencialmente:
el tema entra en el viewport
↓
precarga
↓
almacenamiento de precarga privada
↓
el usuario abre la vista previa
↓
promover precarga
↓
Topic.find()/PostStream utiliza la misma promesa
Esto también significa que el modal no tiene que esperar a que termine la solicitud de precarga antes de abrirse.
El modal puede abrirse inmediatamente con su esqueleto mientras la misma promesa continúa resolviéndose.
Compatibilidad con móviles
De hecho, esta fue una de las razones por las que pasé considerablemente más tiempo en la implementación.
La idea inicial funcionaba razonablemente bien en escritorio, pero los dispositivos móviles revelaron varios problemas relacionados con:
- interacción táctil
- desplazamiento del modal
- enfoque (focus)
- menús anidados
- el compositor
- visibilidad de publicaciones
- carga de imágenes
- rendimiento
Por lo tanto, la implementación final evita tratar el modal como un foro miniatura completamente separado.
En su lugar, reutiliza la mayor parte de la infraestructura existente de Discourse.
Componentes de publicación reales de Discourse
El modal no recrea publicaciones utilizando una plantilla personalizada simplificada.
Representa los componentes reales de Discourse:
Post
PostSmallAction
Esto es importante porque, de lo contrario, la vista previa se convertiría rápidamente en una segunda implementación de la interfaz de usuario de publicaciones.
El componente pasa las acciones relevantes a los componentes de publicación normales, incluyendo cosas como:
- responder
- editar
- eliminar
- recuperar
- reportar
- historial
- marcador
- wiki
- bloquear/desbloquear
- tipo de publicación
- cambios de propiedad
- insignias
- publicaciones ocultas
- citas
- etc.
El resultado es que la vista previa puede comportarse mucho más como un tema normal que como un componente de “vista previa” tradicional.
Respuestas y el compositor
El compositor es una de las partes más complicadas.
La vista previa puede abrir el compositor normal de Discourse para:
Responder al tema
El compositor del tema se abre con el modelo de tema y la información de borrador correcta.
Responder a una publicación específica
La publicación se pasa al compositor para que la respuesta se comporte como una respuesta de publicación normal.
Citar texto seleccionado
El componente también se integra con PostTextSelection.
Esto significa que los usuarios pueden seleccionar texto dentro de la vista previa y utilizar el flujo normal de cita/respuesta de Discourse.
Modales anidados
Otra parte complicada fue el sistema de modales de Discourse.
Las publicaciones pueden abrir otros modales y diálogos:
- reportar
- historial
- diálogos relacionados con insignias
- cambios de propiedad
- confirmaciones de eliminación
- etc.
Si se permitiera que interactuaran con el servicio global de modales normalmente, abrir uno de ellos podría cerrar toda la vista previa del tema.
Para evitar eso, el componente crea un mecanismo de submodal local.
Conceptualmente:
Modal de vista previa de tema
│
├── Modal de reporte
├── Modal de historial
├── Confirmación de eliminación
├── Modal de insignia
└── otros modales relacionados con publicaciones
La vista previa permanece montada debajo.
El componente parchea temporalmente los métodos relevantes del servicio de modales mientras está activo y los restaura cuando se destruye.
Enrutamiento dentro del modal
Otro detalle importante son los enlaces a publicaciones dentro del mismo tema.
Por ejemplo, si una publicación contiene un enlace a:
/t/my-topic/123
la vista previa no necesita cerrarse y navegar lejos.
En su lugar, el componente intercepta la navegación del mismo tema y salta a la publicación solicitada dentro del modal.
Lo mismo aplica a los enlaces que apuntan al tema sin un número de publicación específico.
Esto mantiene al usuario dentro de la vista previa.
Si el enlace apunta a un tema genuinamente diferente, el componente primero restaura sus parches de servicio temporales y se cierra a sí mismo antes de permitir la transición de ruta normal de Discourse.
Esa limpieza es importante porque, de lo contrario, las suscripciones y el rastreador de tiempo de la vista previa podrían permanecer activas mientras se inicializa la ruta de tema real.
Seguimiento de lectura y seguimiento de tiempo
También quería que la vista previa se comportara correctamente desde la perspectiva de Discourse.
Abrir una vista previa no debería significar que el seguimiento de lectura se omita por completo.
Por lo tanto, el componente maneja:
- seguimiento de visitas al tema
- seguimiento de publicaciones visibles
- tiempo de tema
- actualizaciones de la última publicación leída
El rastreador de tiempo utiliza un IntersectionObserver para determinar qué publicaciones son realmente visibles.
Cada 5 segundos, el tiempo de publicaciones visibles se vacía a:
/topics/timings
Cuando se cierra el modal, se realiza una vaciado final para que no se pierdan los últimos segundos.
La implementación también limita un intervalo de tiempo único a 60 segundos.
Mantener sincronizado el estado no leído de la lista de temas
Aquí había otro problema sutil.
Actualizar el estado de seguimiento de temas de Discourse por sí solo no es suficiente para actualizar la insignia no leída que se muestra directamente en una fila de la lista de temas.
Por lo tanto, el componente actualiza el objeto de tema real asociado con la fila después de que se haya vaciado la información de tiempo.
Actualiza valores como:
last_read_post_number
unread_posts
unread
new_posts
cuando corresponde.
Esto significa que, después de leer un tema dentro del modal, la lista de temas puede reflejar inmediatamente el nuevo estado de lectura en lugar de requerir una actualización completa de la página.
Visibilidad de publicaciones
La vista previa utiliza un IntersectionObserver compartido para determinar cuándo se vuelven visibles las publicaciones individuales.
También hay una verificación de visibilidad síncrona cuando se adjunta el observador.
Esto maneja un caso límite donde una publicación ya es visible cuando se monta, pero la primera devolución de llamada asíncrona de IntersectionObserver aún no se ha activado.
Esto es especialmente relevante para temas muy cortos donde todo el tema ya puede ser visible cuando se abre el modal.
Consideraciones de rendimiento
Un objetivo principal era evitar convertir el modal en una página de tema miniatura con un alto consumo de recursos.
Se hacen algunas cosas específicamente para eso.
Representación progresiva
La carga inicial no representa inmediatamente cada publicación.
El componente primero representa suficientes publicaciones para alcanzar la posición objetivo.
Las publicaciones restantes se representan progresivamente utilizando:
requestIdleCallback
cuando está disponible, con una alternativa a setTimeout.
Esto es particularmente útil al abrir un tema largo alrededor de una publicación muy abajo en el flujo.
Contención CSS
Las publicaciones utilizan:
contain: layout;
content-visibility: auto;
contain-intrinsic-size: 1px 180px;
Esto permite que el navegador evite realizar trabajo de representación innecesario para publicaciones que no son visibles actualmente.
Imágenes diferidas
A las imágenes que aún no han especificado un modo de carga se les asigna automáticamente:
loading="lazy"
decoding="async"
Esto evita que un tema largo con muchas imágenes cargue todo de inmediato.
Estado de carga
El modal no solo muestra un área blanca/vacía mientras se realiza la solicitud.
Tiene una interfaz de usuario de esqueleto con:
- marcadores de posición de avatar
- marcadores de posición de nombre de usuario/nombre
- marcadores de posición del cuerpo de la publicación
- animación de brillo (shimmer)
El brillo respeta:
prefers-reduced-motion
por lo que la animación se deshabilita para los usuarios que han solicitado movimiento reducido.
Mantener estable la posición de desplazamiento
Hay algunos lugares donde el componente necesita manipular manualmente la posición de desplazamiento.
Por ejemplo, al cargar publicaciones anteriores, el contenido recién insertado aumenta la altura de desplazamiento.
Simplemente añadir las publicaciones al principio haría que la posición actual del usuario saltara.
Por lo tanto, el componente registra la altura de desplazamiento anterior y compensa la diferencia después de insertar las publicaciones.
Esto mantiene el contenido actualmente visible aproximadamente en el mismo lugar.
Lo mismo aplica al saltar a una publicación en particular.
El componente realiza un paso de posicionamiento posterior a la representación y verifica la posición nuevamente en fotogramas posteriores para tener en cuenta el contenido que aún puede estar estabilizándose.
Presencia del tema
Cuando están disponibles los datos relevantes del tema, la vista previa también puede mostrar la información de presencia del tema de Discourse en la parte inferior del modal.
Así que los usuarios pueden ver quién más está viendo actualmente el tema sin tener que salir de la vista previa.
Interacción con menús móviles y enfoque
Los dispositivos móviles introdujeron otra categoría de problemas.
Algunos elementos de la interfaz de usuario de Discourse utilizan servicios compartidos de modales/menús, y esos servicios no necesariamente saben que la vista previa del tema está actuando actualmente como un contexto de navegación anidado.
Por lo tanto, el componente tiene un manejo adicional alrededor de:
modal.close()- menús de Float Kit
- restauración del enfoque
- el compositor
- controles de teclado de lightbox
- bloqueos de desplazamiento del cuerpo
Por ejemplo, si un menú intenta internamente llamar al método de cierre global del modal, eso no debería cerrar accidentalmente toda la vista previa del tema.
De manera similar, cuando el compositor está abierto, el enfoque debe permanecer dentro del compositor en lugar de ser devuelto al contexto de enfoque de la vista previa.
Configuración
El componente actualmente expone las siguientes configuraciones:
| Configuración | Predeterminado | Descripción |
|---|---|---|
trigger_style |
row |
Hacer que toda la fila sea clicable o usar un botón explícito |
plugin_outlet |
topic-list-after-title |
Salida utilizada por el activador de botón |
enable_prefetch |
true |
Habilitar/deshabilitar la precarga de temas en segundo plano |
max_concurrent_prefetches |
2 |
Máximo de solicitudes de precarga simultáneas |
prefetch_debounce_ms |
400 |
Retardo antes de iniciar una precarga |
prefetch_root_margin_px |
50 |
Comenzar la precarga tantos píxeles antes de que la fila entre en el viewport |
max_prefetches_per_minute |
15 |
Máximo de solicitudes especulativas por minuto |
Los controles de precarga son intencionalmente configurables porque diferentes comunidades pueden tener patrones de tráfico y características de alojamiento/red muy diferentes.
Uno de los principales objetivos de diseño: no romper Discourse normal
Intenté mantener el componente lo más cerca posible de la arquitectura existente de Discourse.
No implementa su propio renderizador de publicaciones, su propio compositor, su propio modelo de tema o su propio flujo de publicaciones completamente separado.
En su lugar, construye un contexto de navegación temporal alrededor de los componentes y servicios existentes de Discourse.
Esta es también la razón por la que algunas partes de la implementación son más complicadas de lo que podrían parecer inicialmente.
El desafío más interesante fue:
¿Puede un tema comportarse casi como un tema normal de Discourse mientras se muestra realmente dentro de otro contexto de interfaz de usuario?
Eso requirió lidiar con los límites entre los servicios globales de Discourse y la vista previa local.


