Modal de temas al estilo de Facebook: ¿es mejor?

He estado experimentando para conseguir un aspecto diferente al de otros sitios y he creado un modal de temas al estilo de Facebook. Parece cargar más rápido para mí y parece ser más fluido en el escritorio.

¿Qué opinas?

El enlace para probar es https://www.carptalk-online.co.uk/ (solo funciona en escritorio)

¡bien hecho! me gusta bastante, aunque no sea usuario de Facebook. tienes un sitio genial. :star_struck:

además, no me daba cuenta de que los tiburones pueden ser tan grandes! :fishing_pole:

¡Muy genial! ¿Lo has probado en temas con más de 20 respuestas? Es cuando cargamos más “páginas” de publicaciones al hacer scroll y, por mi experiencia, es una de las mayores complicaciones con este tipo de vista alternativa.

No, lo haré ahora y te informaré

Funciona bien - URL ELIMINADA

Puedo ver que la página se carga como dices, pero funciona como se espera

El tema no aparece en una ventana modal al hacer clic en el enlace directo. Sin embargo, desde la lista de temas sí funciona.

Sí, eso es lo siguiente que necesito investigar, pero me he estado preguntando: ¿importa? Es igual a cómo funciona Facebook: el enlace directo lleva a la publicación real, y si se hace clic mientras se navega, se abre un modal.

Buen concepto, ¿puedes mantener una conexión constante con el bus de mensajes para recibir nuevos mensajes mientras se cargan los mensajes antiguos?

Sí, porque el modal carga la aplicación normal de temas de Discourse dentro de un iframe en lugar de renderizar una copia personalizada del tema. El iframe establece su propia conexión con MessageBus y Discourse sigue manejando el flujo de publicaciones normalmente, incluyendo la carga de publicaciones antiguas y la recepción de publicaciones recién publicadas.

La página principal también permanece activa detrás del modal, por lo que técnicamente hay dos instancias de Discourse y dos conexiones con MessageBus mientras el modal está abierto. Esto funciona, aunque es ligeramente más pesado que una implementación completamente nativa. Cerrar el modal elimina el iframe y su conexión.

No sé si importa, y no uso Facebook :stuck_out_tongue:

Simplemente me sorprendió que hacer clic en tu enlace no mostrara el tema en una ventana modal, cuando lo publicaste precisamente para demostrar que funcionaba en una ventana modal.

Mi publicación original era un enlace a la página de inicio. ¿No leíste mi respuesta a Kris probando los temas, jajaja? ¿Qué tal todo? Hace tiempo que no te veo.

Acabo de terminar la versión móvil y estoy satisfecho con el funcionamiento general. El siguiente paso es encontrar una mejor manera de hacerlo y reducir el tiempo de carga. ¿Alguien tiene alguna sugerencia?

Tras realizar más pruebas, he utilizado la versión sin JavaScript que ve Google, y se carga al instante en el modal. Si pudiera usar de algún modo el contenido de esa versión, esto sería mucho más rápido.

Se ve muy bien en la versión de escritorio,

La velocidad de carga en la versión móvil es un poco lenta; al hacer clic, solo se ve una página vacía y el botón de cerrar en la esquina superior derecha, lo que parece requerir esperar mucho tiempo frente a una pantalla en blanco.

Esto es en lo que estoy trabajando ahora :slight_smile: Por el momento, es solo un concepto que quiero hacer realidad. Sin embargo, en mi dispositivo se carga en 2 segundos en el móvil con el icono de carga del spinner de Discourse, así que no estoy seguro de qué está pasando. Probablemente porque el servidor está en Londres, Reino Unido, y aún no tiene CDN.

那确实距离是太远了,所以网络需要时间传输

Ahora los temas se cargan al instante en el modal, ahora hay que encontrar una manera de usar la primera instancia de Discourse cargada para las respuestas, etc. (esto es en el servidor de prueba)

He desactivado esto en el sitio principal, ya que ahora está recibiendo tráfico con personas que se registran, por lo que lo he trasladado a un servidor de pruebas. Espero tenerlo listo a mediados de agosto, si alguien está interesado en un modal.

Hola :waving_hand:

Un enfoque diferente al de @Damian_Boon, aunque: sin iframe, todo se ejecuta como componentes nativos de Glimmer reutilizando el propio mecanismo de Post/post-stream de Discourse. Como surgieron algunas preguntas específicas en el hilo, aquí explico cómo terminé resolviendo cada una.


Por qué no usar un iframe

Un iframe es, con mucho, la forma más rápida de lograr esto: ruta completa del tema, compositor, moderación, todo gratis. El costo, que es lo que pregunta @nicolsdennis sobre MessageBus, es que terminas con dos instancias vivas de Discourse en la página: dos post-streams, dos conexiones de MessageBus, dos compositores.

Yo fui por el otro camino. El modal es un DModal que renderiza los componentes reales Post/PostSmallAction contra un postStream construido desde store.createRecord("topic", …), misma aplicación, misma conexión de MessageBus, mismo compositor. Eso significa que un puñado de servicios singleton principales (modal, bookmarkApi, DiscourseURL.routeTo, el controller:topic ambiental) necesitan apuntar al tema del modal mientras esté abierto:

// Los componentes principales (ej. PostBookmarkManager) leen/escriben topic.bookmarks
// desde controller:topic. Apúntalo a nuestro topicModel mientras el modal esté abierto.
this.topicController = getOwner(this).lookup("controller:topic");
if (this.topicController) {
  this.originalTopicControllerModel = this.topicController.model;
}

Cada parche se deshace mediante un restoreServicePatches() idempotente al cerrar, destruir o navegar fuera, así que nada se filtra al resto de la aplicación una vez que el modal desaparece.


Enlaces directos y enrutamiento interno

Dos partes. Primero, el modal debería abrirse donde el lector realmente se quedó, no siempre en el post 1:

const lastRead = this.topic.last_read_post_number ?? 0;
const highestPostNumber = this.topic.highest_post_number ?? 1;
const initialPostNumber = Math.max(
  1,
  Math.min(lastRead + 1, highestPostNumber)
);

Segundo, los enlaces dentro de los posts que apuntan de vuelta al mismo tema no deberían escapar del modal. DiscourseURL.routeTo se parchea durante el tiempo que esté abierto:

this.originalRouteTo = DiscourseURL.routeTo.bind(DiscourseURL);
DiscourseURL.routeTo = (path, opts) => {
  if (typeof path === "string") {
    const match = path.match(/^\/t\/(?:[^/]+\/)?(\d+)(?:\/(\d+))?/);
    if (match && parseInt(match[1], 10) === this.topicId) {
      this.jumpToPost(match[2] ? parseInt(match[2], 10) : 1);
      return Promise.resolve();
    }
  }
  this.restoreServicePatches();
  return this.originalRouteTo(path, opts);
};

Cualquier cosa que apunte a otro lugar restaura los parches primero y cae en una navegación normal, así que salir del modal nunca deja un enrutamiento obsoleto atrás.


El caso de respuesta (20+ respuestas)

Desplazamiento infinito abajo mediante un centinela + IntersectionObserver, un “cargar anteriores” simétrico arriba (una vista previa puede abrirse a mitad del hilo), ambos impulsando el exacto postStream.appendMore() / prependMore() que usa la ruta real del tema:

sentinel = modifier((element) => {
  const obs = new IntersectionObserver(
    (entries) => {
      if (entries.some((e) => e.isIntersecting)) {
        this.loadBelow();
      }
    },
    {
      root: document.querySelector(".topic-preview-modal .d-modal__body"),
      rootMargin: "200px",
    }
  );
  obs.observe(element);
  return () => obs.disconnect();
});

Primera pintura

Un esqueleto (marcadores de posición de avatar + líneas, brillo) llena la primera pintura en lugar de un spinner, y la lista de posts en sí se renderiza en etapas en lugar de todo a la vez, justo suficientes posts para llegar al post objetivo primero, luego cinco más a la vez vía requestIdleCallback hasta que el resto se ponga al día:

const renderRemainingPosts = () => {
  const totalPosts = this.postStream?.posts?.length || 0;
  if (this.renderLimit < totalPosts) {
    this.renderLimit = Math.min(this.renderLimit + 5, totalPosts);
    if (this.renderLimit < totalPosts) {
      if (window.requestIdleCallback) {
        window.requestIdleCallback(renderRemainingPosts, { timeout: 300 });
      } else {
        setTimeout(renderRemainingPosts, 30);
      }
      return;
    }
  }
  this.renderLimit = Number.MAX_SAFE_INTEGER;
};

Cada contenedor de post también obtiene content-visibility: auto con un contain-intrinsic-size aproximado, así que el navegador omite el diseño por completo para posts que están fuera de pantalla:

.topic-post {
  contain: layout;
  content-visibility: auto;
  contain-intrinsic-size: 1px 180px;
}

Esta es la parte en la que insistiría más. En lugar de reutilizar una instancia después del clic, hago prefetch antes del clic. Cada fila de la lista de temas observa su propia visibilidad, hace debounce de 200ms para que un desplazamiento rápido no dispare una docena de solicitudes, y lo delega a una pequeña cola compartida limitada a 2 solicitudes concurrentes:

function createTopicPrefetchQueue(maxConcurrent) {
  const pending = [];
  const activeIds = new Set();
  function drain() {
    while (activeIds.size < maxConcurrent && pending.length) {
      const { id, task } = pending.shift();
      activeIds.add(id);
      Promise.resolve()
        .then(task)
        .finally(() => {
          activeIds.delete(id);
          drain();
        });
    }
  }
  return {
    schedule(id, task) {
      if (activeIds.has(id) || pending.some((item) => item.id === id)) {
        return;
      }
      pending.push({ id, task });
      drain();
    },
    cancel(id) {
      const index = pending.findIndex((item) => item.id === id);
      if (index !== -1) {
        pending.splice(index, 1);
      }
    },
  };
}

El prefetch se almacena bajo una clave privada de PreloadStore, deliberadamente no la clave principal topic_${id}, escribir allí filtraría un JSON parcial con alcance last_read+1 en una navegación real de tema completo. En el clic real se promociona a la clave que espera el cargador del modal:

promoteTopicPrefetch(topic) {
  if (!topic?.id) {
    return;
  }
  try {
    const key = this.prefetchStoreKey(topic.id);
    const prefetched = PreloadStore.get?.(key);
    if (prefetched != null) {
      PreloadStore.remove?.(key);
      PreloadStore.store(`topic_${topic.id}`, prefetched);
    }
  } catch {
    // mejor esfuerzo - el modal vuelve a una carga fresca
  }
}

Si una fila se desplaza fuera de la vista antes de que se haga clic, la solicitud pendiente se cancela y cualquier resultado en caché se descarta, así que solo retiene datos para temas que realmente están en pantalla en ese momento.


Uno más, no mencionado en el hilo aún: modales anidados

Algunos flujos de modal (los que el núcleo abre por sí mismo en lugar de a través de una propiedad de devolución de llamada explícita conectada a showSubModal()) llaman directamente al servicio modal. En lugar de permitir que esos cierren el modal de vista previa debajo del lector, modal.show() se parchea para redirigir selectivamente los modales objetivo a un pequeño espacio de sub-modal local local mientras la vista previa esté abierta:

this.originalModalShow = this.modal.show.bind(this.modal);
this.modal.show = (component, opts = {}) => {
  return this.showSubModal(component, opts.model);
};

así que esos flujos objetivo se superponen a la vista previa en lugar de reemplazarla, mientras que el seguimiento de lectura (/topics/timings) sigue funcionando debajo mediante una descarga periódica de un conjunto de números de post visibles rastreados por IntersectionObserver, así que los conteos de no leídos siguen siendo correctos después de cerrar, igual que una visita normal a un tema.


Es un buen trozo de código a través del modal, el disparador de la lista de temas y el estilo (~1700 líneas en total), esto cubre quizás un tercio de ello. Las partes más difíciles por mucho fueron el parcheo de servicios y hacer que content-visibility se comporte una vez que las alturas de los posts cambian después de que las imágenes terminen de cargar. Feliz de entrar en más detalle sobre cualquiera de ellos, o hacerlo de código abierto, si hay interés.