Facebook-ähnliches Themen-Modal – Ist es besser?

Ich habe herumexperimentiert, um ein anderes Aussehen als andere Sites zu erzielen, und habe ein Facebook-ähnliches Themen-Modal erstellt. Es scheint bei mir schneller zu laden und wirkt auf dem Desktop flüssiger.

Was denkt ihr darüber?

Link zum Testen ist https://www.carptalk-online.co.uk/ (funktioniert nur auf dem Desktop)

schön gemacht! das gefällt mir ganz gut, obwohl ich kein Facebook-Nutzer bin. du hast eine coole Seite. :star_struck:

außerdem habe ich nicht gewusst, dass Karpfen so riesig werden können! :fishing_pole:

Sehr cool! Hast du es in Themen mit 20 oder mehr Antworten getestet? Genau dann laden wir beim Scrollen weitere „Seiten“ mit Beiträgen, und aus meiner Erfahrung ist das eine der größten Herausforderungen bei dieser Art von alternativer Ansicht.

Nein, ich mache das jetzt und melde mich wieder.

Funktioniert einwandfrei – URL GELÖSCHT

Ich kann sehen, dass die Seite wie von dir beschrieben lädt, aber sie funktioniert wie erwartet.

Das Thema wird nicht in einem Modal angezeigt, wenn man auf den direkten Link klickt. Über die Themenliste funktioniert es jedoch.

Ja, das ist das nächste, was ich mir ansehen muss, aber ich habe darüber nachgedacht: Macht das einen Unterschied? Das verhält sich genauso wie Facebook: Der direkte Link führt zum eigentlichen Beitrag, und wenn man während der Navigation darauf klickt, öffnet sich ein Modal.

Nettes Konzept. Kann man während des Ladens älterer Nachrichten eine stabile Verbindung zum Message Bus für neue Nachrichten aufrechterhalten?

Ja, denn das Modal lädt die normale Discourse-Themenanwendung in einem iframe, anstatt eine eigene Kopie des Themas zu rendern. Der iframe stellt seine eigene MessageBus-Verbindung her, und Discourse verarbeitet den Beitragsstrom weiterhin wie gewohnt, einschließlich des Ladens älterer Beiträge und des Empfangs neu veröffentlichter Beiträge.

Die übergeordnete Seite bleibt auch dahinter aktiv, sodass es technisch gesehen zwei Discourse-Instanzen und zwei MessageBus-Verbindungen gibt, während das Modal geöffnet ist. Das funktioniert, ist jedoch etwas ressourcenintensiver als eine vollständig native Implementierung. Beim Schließen des Modals werden der iframe und seine Verbindung entfernt.

Ich weiß nicht, ob das einen Unterschied macht, und ich nutze Facebook nicht :stuck_out_tongue:

Ich war nur überrascht, dass das Klicken auf deinen Link das Thema nicht in einem Modalfenster anzeigte, obwohl du es genau dazu gepostet hast, um zu zeigen, dass es in einem Modalfenster funktioniert.

Mein ursprünglicher Beitrag war ein Link zur Startseite. Hast du meinen Kommentar zu Krys Test der Themen gelesen? Haha. Wie geht’s dir denn so? Ich hab dich schon eine Weile nicht mehr gesehen.

Habe gerade die mobile Version fertiggestellt und bin zufrieden, wie alles funktioniert. Als Nächstes muss ich einen besseren Weg dafür finden und die Ladezeit verkürzen. Hat jemand Vorschläge?

Nach weiteren Tests habe ich die nicht-JS-Version verwendet, die Google sieht, und sie lädt sofort im Modal. Wenn ich irgendwie den Inhalt davon nutzen könnte, würde das den Prozess beschleunigen.

Auf dem Desktop sieht es wirklich gut aus.

Die Ladezeit auf mobilen Geräten ist jedoch etwas langsam. Nach dem Klick sieht man nur eine leere Seite und die Schaltfläche „Schließen“ in der oberen rechten Ecke. Man scheint eine lange Zeit vor einer blanken Seite warten zu müssen.

Das ist, woran ich gerade arbeite :slight_smile: Im Moment ist es nur ein Konzept, das ich Wirklichkeit werden lassen möchte. Auf meinem Gerät wird es jedoch innerhalb von 2 Sekunden auf dem Mobiltelefon mit dem Discourse-Lade-Spinner geladen, also bin ich mir nicht sicher, was los ist. Wahrscheinlich liegt das daran, dass der Server in London, UK steht und bisher kein CDN verwendet wird.

Dann ist die Entfernung tatsächlich zu groß, daher braucht das Netzwerk Zeit für die Übertragung.

Ich habe es jetzt geschafft, dass die Themen im Modal sofort geladen werden. Nun muss ich nur noch einen Weg finden, die zuerst geladene Discourse-Instanz für die Antworten usw. zu nutzen. (Dies betrifft den Testserver)

Ich habe dies auf der Hauptseite deaktiviert, da sie jetzt Traffic durch registrierende Nutzer erhält, und es wurde auf einen Testserver verlegt. Hoffentlich ist dies Mitte August abgeschlossen, falls jemand an einem Modal interessiert ist.

Hallo :waving_hand:

Ein anderer Ansatz als der von @Damian_Boon: Kein iframe, alles läuft als native Glimmer-Komponenten und nutzt Discourses eigenen Post/post-stream-Mechanismus. Da im Thread einige spezifische Fragen aufkamen, hier, wie ich jedes Problem gelöst habe.


Warum kein iframe

Ein iframe ist mit Abstand der schnellste Weg, dies zu erreichen: vollständige Topic-Route, Composer, Moderation, alles kostenlos. Der Nachteil, auf den @nicolsdennis’ Frage zu MessageBus hinausläuft, ist, dass du am Ende zwei aktive Discourse-Instanzen auf der Seite hast: zwei post-streams, zwei MessageBus-Verbindungen, zwei Composer.

Ich habe den anderen Weg gewählt. Der Modal ist ein DModal, das die echten Post/PostSmallAction-Komponenten gegen einen postStream rendert, der aus store.createRecord("topic", …) aufgebaut ist. Gleiche App, gleiche MessageBus-Verbindung, gleicher Composer. Das bedeutet, dass eine Handvoll Kern-Singleton-Services (modal, bookmarkApi, DiscourseURL.routeTo, der umgebende controller:topic) solange auf das Topic des Modals zeigen müssen, wie es offen ist:

// Kernkomponenten (z. B. PostBookmarkManager) lesen/schreiben topic.bookmarks
// von controller:topic. Zeige es auf unser topicModel, während der Modal offen ist.
this.topicController = getOwner(this).lookup("controller:topic");
if (this.topicController) {
  this.originalTopicControllerModel = this.topicController.model;
}

Jeder Patch wird durch eine idempotente restoreServicePatches() beim Schließen, Zerstören oder Navigieren rückgängig gemacht, sodass nichts in den Rest der App austritt, sobald der Modal verschwunden ist.


Direkte Links & internes Routing

Zwei Teile. Erstens sollte der Modal dort öffnen, wo der Leser tatsächlich aufgehört hat, nicht immer bei 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)
);

Zweitens sollten Links innerhalb von Posts, die zurück in dasselbe Topic zeigen, den Modal nicht verlassen. DiscourseURL.routeTo wird für die Dauer, in der er offen ist, gepatcht:

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);
};

Alles, was anderswohin zeigt, stellt die Patches zuerst wieder her und fällt auf eine normale Navigation zurück, sodass das Verlassen des Modals keine veralteten Routing-Zustände hinterlässt.


Der Antwort-Fall (20+ Antworten)

Endlos-Scrollen unten via Sentinel + IntersectionObserver, ein symmetrisches „frühere laden“ oben (eine Vorschau kann mitten im Thread öffnen), beide treiben genau die postStream.appendMore() / prependMore(), die die echte Topic-Route verwendet:

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();
});

Erster Render (First Paint)

Ein Skeleton (Avatar + Platzhalterlinien, Shimmer) füllt den ersten Render statt eines Spinners, und die Post-Liste selbst rendert in Stufen statt alles auf einmal, gerade genug Posts, um zuerst zum Zielpost zu gelangen, dann fünf weitere auf einmal via requestIdleCallback, bis der Rest nachgeholt hat:

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;
};

Jeder Post-Wrapper erhält auch content-visibility: auto mit einer groben contain-intrinsic-size, sodass der Browser das Layout für Posts, die außerhalb des Bildschirms liegen, vollständig überspringt:

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

Das ist der Teil, auf den ich am meisten drängen würde. Anstatt eine Instanz nach dem Klick wiederzuverwenden, prefetche ich vor dem Klick. Jede Topic-Liste-Zeile beobachtet ihre eigene Sichtbarkeit, debouncet 200ms, damit ein schnelles Scrollen nicht ein Dutzend Anfragen auslöst, und übergibt an eine kleine gemeinsame Warteschlange, die auf 2 gleichzeitige Fetches begrenzt ist:

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);
      }
    },
  };
}

Der Prefetch wird unter einem privaten PreloadStore-Schlüssel zwischengespeichert, bewusst nicht unter dem Kern-topic_${id}-Schlüssel, denn ein Schreiben dort würde ein last_read+1-skaliertes partielles JSON in eine echte Full-Topic-Navigation lecken. Beim tatsächlichen Klick wird er in den Schlüssel befördert, den der Loader des Modals erwartet:

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 {
    // best-effort - modal falls back to a fresh load
  }
}

Wenn eine Zeile aus dem Sichtfeld scrollt, bevor sie geklickt wird, wird der ausstehende Fetch abgebrochen und jedes zwischengespeicherte Ergebnis verworfen, sodass es nur Daten für Topics hält, die gerade auf dem Bildschirm sind.


Noch eins, das im Thread noch nicht angesprochen wurde: verschachtelte Modals

Einige Modal-Flows (die, die das Kernsystem selbst öffnet, anstatt durch eine explizite Callback-Prop, die an showSubModal() verkabelt ist) rufen den modal-Service direkt auf. Anstatt diese den Preview-Modal unter dem Leser weg zu schließen, wird modal.show() gepatcht, um gezielte Modals selektiv in einen kleinen lokalen Sub-Modal-Slot umzuleiten, solange die Vorschau offen ist:

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

Damit werden diese gezielten Flows über der Vorschau geschichtet, anstatt sie zu ersetzen, während das Read-Tracking (/topics/timings) weiterhin im Hintergrund läuft, via periodischem Flush einer IntersectionObserver-verfolgten Menge sichtbarer Post-Nummern, sodass die Unread-Counts nach dem Schließen immer noch korrekt sind, genau wie bei einem normalen Topic-Besuch.


Es ist ein ziemlicher Code-Anteil über den Modal, den Topic-Liste-Trigger und das Styling (~1700 Zeilen zusammen), das deckt vielleicht ein Drittel davon ab. Die schwierigsten Teile waren bei weitem das Service-Patching und das Verhalten von content-visibility, sobald sich die Post-Höhen ändern, nachdem Bilder fertig geladen sind. Gerne gehe ich auf mehr Details ein oder open-source es, wenn Interesse besteht.