| Zusammenfassung | Topic Preview Modal – Themen öffnen und damit interagieren, ohne die Themenliste zu verlassen | |
| Vorschau | Theme Creator | |
| Repository | 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 | |
| War es hilfreich? | > ./support --coffee | |
| Installationsanleitung | So installierst du ein Theme oder Theme-Component | |
| Neu bei Discourse Themes? | Einsteigerguide zur Verwendung von Discourse Themes |
Dieses Theme-Component installieren
Topic Preview Modal – Themen öffnen und damit interagieren, ohne die Themenliste zu verlassen
Ich habe ein neues Discourse-Theme-Component namens Topic Preview Modal erstellt.
Die Idee ist ziemlich einfach:
Ein Thema direkt aus der Themenliste in einem nativen Discourse-Modal öffnen, das Thema lesen und damit interagieren und anschließend mit dem Durchblättern der Liste genau dort weitermachen, wo man aufgehört hat, ohne die Seite zu wechseln.
Es begann mit Facebook-style Topic Modal - Is it better? , endete aber damit, dass eine recht umfangreiche Integration mit den Systemen für Themen, Beitragsströme, Composer, Modals, Lesezeichen, Routing, Präsenz, Lese-Tracking und Prefetching in Discourse erforderlich war.
Warum?
Der normale Discourse-Workflow ist:
- Du durchblätterst eine Themenliste.
- Du klickst auf ein Thema.
- Discourse navigiert zu
/t/.... - Du liest/antwortest/interagierst mit dem Thema.
- Du gehst zur Themenliste zurück.
Für viele Workflows ist das völlig in Ordnung.
Wenn man jedoch eine geschäftige Themenliste durchblättert, möchte man manchmal nur schnell ein Thema inspizieren, ein paar Beiträge lesen, die neuesten Antworten prüfen, auf etwas reagieren oder eine schnelle Frage beantworten.
Für diesen Anwendungsfall fühlt es sich unnötig aufwendig an, die Themenliste zu verlassen.
Das Ziel dieses Components war daher, die Themenliste so zu verhalten, wie ein Posteingang:
Themenliste → Vorschau → Interagieren → Schließen → Genau dort weitermachen, wo man war.
Was es tut
Die Vorschau ist kein statischer Auszug.
Sie rendert die tatsächlichen Discourse-Beitragskomponenten in einem nativen DModal.
Das bedeutet, dass Benutzer:
- Beiträge lesen
- durch das Thema scrollen
- frühere Beiträge laden
- weitere Beiträge unten laden
- auf Beiträge reagieren
- Beiträge als Lesezeichen speichern
- Text zitieren
- auf das Thema antworten
- auf einzelne Beiträge antworten
- Beiträge bearbeiten, wenn erlaubt
- Beiträge löschen/wiederherstellen, wenn erlaubt
- Beiträge melden
- Beitragsverlauf anzeigen
- verschiedene normale Beitragsaktionen ausführen
- die Präsenz im Thema sehen
- Links zu anderen Beiträgen innerhalb desselben Themas folgen
- direkt zum relevanten Beitrag springen
- das vollständige Thema öffnen, wenn nötig
Die Absicht ist, dass sich die Vorschau so nah wie möglich an das eigentliche Öffnen des Themas anfühlt.
Zwei Auslösermodi
Es gibt zwei Möglichkeiten, die Vorschau zu öffnen.
1. Ganze Themenlistenzeile
Dies ist die Standardeinstellung.
Die gesamte Themenlistenzeile wird klickbar, während gängige interaktive Elemente wie:
- Benutzerkarten
- Teilnehmer
- Kategorie-Links
- Tags
- Themenstatus-Links
- Mehrfachauswahl
vom Modal-Auslöser ausgenommen sind.
Dies macht die Erfahrung beim Durchblättern einer Themenliste sehr schnell.
2. Expliziter Aufklapp-Button
Alternativ kann das Component ein kleines Aufklapp-Icon über einen Discourse-Plugin-Outlet rendern. Custom-Themes können einfach einen neuen <PluginOutlet /> erstellen, um den Auslöser anzuzeigen.
In diesem Modus bleibt das normale Verhalten der Themenliste völlig unberührt.
Der Benutzer klickt auf das Aufklapp-Icon, um die Vorschau zu öffnen, während das Klicken auf den Themennamen die normale Discourse-Navigation ausführt.
Dies ist nützlich, wenn eine Website das Standard-Interaktionsmodell der Themenliste beibehalten möchte.
Die Einstellung lautet:
trigger_style:
row
oder:
trigger_style:
button
Wenn der Button-Modus verwendet wird, ist auch der Outlet konfigurierbar.
Die Vorschau startet an der ungelesenen Position des Benutzers
Ein wichtiges Detail ist, dass das Modal nicht einfach den ersten Beitrag lädt.
Wenn ein Thema bereits teilweise gelesen wurde, berechnet die Vorschau:
last_read_post_number + 1
und öffnet sich um diesen Beitrag herum.
Wenn also ein Thema 200 Beiträge hat und der Benutzer bis Beitrag #165 gelesen hat, startet das Öffnen der Vorschau bei ca. #166.
Dies macht die Vorschau für das reale Durchblättern viel nützlicher.
Es bedeutet auch, dass das Component mit beiden Seiten des Beitragsstroms umgehen muss:
- Laden früherer Beiträge bei Bedarf
- Laden neuerer Beiträge unten
Der Frühere Beiträge-Button wird angezeigt, wenn es Beiträge oberhalb des aktuell geladenen Bereichs gibt, während ein IntersectionObserver-Sentinel automatisch weitere Beiträge lädt, wenn der Benutzer das Ende erreicht.
Prefetching
Einer der größten Teile des Components ist sein Prefetch-System.
Das Problem mit einem Modal wie diesem ist, dass der Benutzer erwartet, es fühlt sich augenblicklich an.
Wenn wir erst nach dem Klick des Benutzers mit dem Laden des Themas beginnen, kann das Modal immer noch spürbare Zeit mit dem Warten auf das Netzwerk verbringen.
Stattdessen kann das Component Themen proaktiv prefetchen, während der Benutzer die Liste durchblättert.
Wenn eine Themenzeile sich dem Viewport nähert, kann ein IntersectionObserver einen Prefetch planen.
Es gibt mehrere Schutzmechanismen, um zu verhindern, dass dies zu unkontrolliertem Hintergrundverkehr führt.
Debouncing
Ein Thema löst nicht sofort eine Anfrage aus, nur weil es kurz im Viewport erschienen ist.
Das Component wartet auf den konfigurierten Debounce-Zeitraum.
Standard:
400 ms
Dies ist besonders nützlich, wenn schnell durch eine lange Themenliste gescrollt wird.
Root Margin
Das Prefetching kann etwas beginnen, bevor das Thema tatsächlich den Viewport betritt.
Standard:
50 px
Dies gibt der Anfrage einen kleinen Vorsprung.
Begrenzung gleichzeitiger Anfragen
Die Anzahl der gleichzeitigen Prefetches ist begrenzt.
Standard:
2
Die Einstellung erlaubt zwischen 1 und 6 gleichzeitigen Prefetches.
Budget pro Minute
Es gibt auch einen zweiten Schutzmechanismus:
max_prefetches_per_minute
Der Standardwert ist:
15
Selbst wenn der Benutzer weiter durch Hunderte von Themen scrollt, erzeugt das Component nicht kontinuierlich spekulative Anfragen.
0 deaktiviert die Begrenzung.
Prefetching kann vollständig deaktiviert werden
Wenn eine Website keinen spekulativen Netzwerkverkehr möchte:
enable_prefetch = false
Das Component funktioniert weiterhin normal. Themen werden einfach geladen, wenn die Vorschau geöffnet wird.
Prefetch-Daten werden getrennt von der normalen Themenavigation gespeichert
Hier gibt es ein wichtiges Implementierungsdetail.
Die prefetchte Antwort wird nicht sofort in den normalen topic_<id>-Preload-Schlüssel von Discourse geschrieben.
Stattdessen verwendet das Component seinen eigenen Namespace:
topic-preview-modal:prefetch:<topicId>
Erst wenn der Benutzer die Vorschau tatsächlich öffnet, wird der prefetchte Promise auf den Kern-Themen-Preload-Schlüssel hochgestuft.
Dies ist absichtlich so.
Die Vorschau kann ein Thema ab last_read_post_number + 1 laden, und ich möchte nicht, dass diese vorschau-spezifische Antwort in eine normale Themen-Route-Navigation einfließt.
Der Lebenszyklus ist also im Wesentlichen:
Thema betritt Viewport
↓
Prefetch
↓
Privater Preload-Speicher
↓
Benutzer öffnet Vorschau
↓
Preload hochstufen
↓
Topic.find()/PostStream verwendet denselben Promise
Das bedeutet auch, dass das Modal nicht darauf warten muss, dass die Prefetch-Anfrage abgeschlossen ist, bevor es geöffnet wird.
Das Modal kann sofort mit seinem Skeleton-UI geöffnet werden, während derselbe Promise weiterhin aufgelöst wird.
Mobile Unterstützung
Das war tatsächlich einer der Gründe, warum ich deutlich mehr Zeit für die Implementierung aufgewendet habe.
Die ursprüngliche Idee funktionierte auf dem Desktop relativ gut, aber Mobile offenbarte mehrere Probleme im Zusammenhang mit:
- Touch-Interaktion
- Modal-Scrolling
- Fokus
- verschachtelten Menüs
- dem Composer
- Beitragsvisibility
- Bildladen
- Leistung
Die finale Implementierung behandelt das Modal daher nicht als vollständig separate Miniatur-Foren-Instanz.
Stattdessen wird so viel wie möglich der vorhandenen Infrastruktur von Discourse wiederverwendet.
Echte Discourse-Beitragskomponenten
Das Modal erstellt Beiträge nicht mit einer vereinfachten Custom-Vorlage neu.
Es rendert die tatsächlichen:
Post
PostSmallAction
Komponenten von Discourse.
Das ist wichtig, da die Vorschau sonst schnell zu einer zweiten Implementierung der Beitrags-UI werden würde.
Das Component übergibt die relevanten Aktionen an die normalen Beitragskomponenten, einschließlich Dinge wie:
- Antwort
- Bearbeiten
- Löschen
- Wiederherstellen
- Melden
- Verlauf
- Lesezeichen
- Wiki
- Sperren/Entsperren
- Beitragstyp
- Eigentumsänderungen
- Abzeichen
- verborgene Beiträge
- Zitieren
- usw.
Das Ergebnis ist, dass sich die Vorschau viel mehr wie ein normales Thema verhält als wie ein traditionelles „Vorschau“-Component.
Antworten und der Composer
Der Composer ist einer der komplizierteren Teile.
Die Vorschau kann den normalen Discourse-Composer öffnen für:
Antworten auf das Thema
Der Themen-Composer wird mit dem Themenmodell und den richtigen Entwurfsinformationen geöffnet.
Antworten auf einen bestimmten Beitrag
Der Beitrag wird an den Composer übergeben, damit die Antwort wie eine normale Beitragsantwort funktioniert.
Zitieren von ausgewähltem Text
Das Component integriert sich auch mit PostTextSelection.
Das bedeutet, Benutzer können Text in der Vorschau auswählen und den normalen Zitat/Antwort-Workflow von Discourse verwenden.
Verschachtelte Modals
Ein weiterer schwieriger Teil war das Modal-System von Discourse.
Beiträge können andere Modals und Dialoge öffnen:
- Melden
- Verlauf
- abzeichenbezogene Dialoge
- Eigentumsänderungen
- Löschbestätigungen
- usw.
Wenn diese normalerweise mit dem globalen Modal-Service interagieren dürften, könnte das Öffnen eines von ihnen das gesamte Themen-Preview schließen.
Um das zu vermeiden, erstellt das Component einen lokalen Sub-Modal-Mechanismus.
Konzeptionell:
Topic Preview Modal
│
├── Melden-Modal
├── Verlauf-Modal
├── Löschbestätigung
├── Abzeichen-Modal
└── andere beitragsbezogene Modals
Die Vorschau bleibt darunter gemountet.
Das Component patcht die relevanten Modal-Service-Methoden temporär, während es aktiv ist, und stellt sie wieder her, wenn es zerstört wird.
Routing innerhalb des Modals
Ein weiteres wichtiges Detail sind Links zu Beiträgen innerhalb desselben Themas.
Wenn beispielsweise ein Beitrag einen Link zu:
/t/my-topic/123
enthält, muss die Vorschau nicht geschlossen und weg navigiert werden.
Stattdessen fängt das Component Navigationen innerhalb desselben Themas ab und springt zum angeforderten Beitrag innerhalb des Modals.
Gleiches gilt für Links, die auf das Thema ohne eine bestimmte Beitragsnummer abzielen.
Dies hält den Benutzer innerhalb der Vorschau.
Wenn der Link auf ein wirklich anderes Thema zeigt, stellt das Component seine temporären Service-Patches zuerst wieder her und schließt sich selbst, bevor es die normale Discourse-Route-Übergabe erlaubt.
Diese Bereinigung ist wichtig, da andernfalls die Abonnements und der Timing-Tracker der Vorschau aktiv bleiben könnten, während die echte Themen-Route initialisiert wird.
Lese-Tracking und Zeit-Tracking
Ich wollte auch, dass sich die Vorschau aus Sicht von Discourse korrekt verhält.
Das Öffnen einer Vorschau sollte nicht bedeuten, dass das Lese-Tracking vollständig umgangen wird.
Das Component behandelt daher:
- Themenbesuchs-Tracking
- sichtbare Beitrags-Tracking
- Themen-Timing
- Aktualisierung des letzten gelesenen Beitrags
Der Timing-Tracker verwendet einen IntersectionObserver, um zu bestimmen, welche Beiträge tatsächlich sichtbar sind.
Alle 5 Sekunden wird das Timing sichtbarer Beiträge an:
/topics/timings
gesendet.
Wenn das Modal geschlossen wird, wird ein letztes Mal gesendet, damit die letzten Sekunden nicht verloren gehen.
Die Implementierung begrenzt auch ein einzelnes Intervall auf 60 Sekunden.
Den ungelesenen Status der Themenliste synchron halten
Hier gab es ein weiteres subtiles Problem.
Nur den Themen-Tracking-Status von Discourse zu aktualisieren, reicht nicht aus, um das Ungelesen-Badge anzuzeigen, das direkt auf einer Themenlistenzeile angezeigt wird.
Das Component aktualisiert daher das tatsächliche Themenobjekt, das mit der Zeile verbunden ist, nachdem die Timing-Informationen gesendet wurden.
Es aktualisiert Werte wie:
last_read_post_number
unread_posts
unread
new_posts
wenn angemessen.
Das bedeutet, dass die Themenliste nach dem Lesen eines Themas im Modal den neuen gelesenen Status sofort widerspiegeln kann, anstatt einen vollständigen Seiten-Refresh zu erfordern.
Beitragsvisibility
Die Vorschau verwendet einen gemeinsamen IntersectionObserver, um zu bestimmen, wann einzelne Beiträge sichtbar werden.
Es gibt auch einen synchronen Visibility-Check, wenn der Observer angehängt wird.
Dies behandelt einen Edge Case, in dem ein Beitrag bereits sichtbar ist, wenn er gemountet wird, aber der asynchrone erste IntersectionObserver-Callback noch nicht ausgelöst wurde.
Das ist besonders relevant für sehr kurze Themen, bei denen das gesamte Thema bereits sichtbar sein kann, wenn das Modal geöffnet wird.
Leistungsüberlegungen
Ein Hauptziel war es, zu vermeiden, das Modal in eine leistungstechnisch schwere Miniatur-Themenseite zu verwandeln.
Dafür werden einige Dinge speziell unternommen.
Progressives Rendering
Das anfängliche Laden rendert nicht sofort jeden Beitrag.
Das Component rendert zunächst genügend Beiträge, um die Zielposition zu erreichen.
Die verbleibenden Beiträge werden dann progressiv gerendert, wobei verwendet wird:
requestIdleCallback
wenn verfügbar, mit einem Fallback auf setTimeout.
Das ist besonders nützlich, wenn ein langes Thema um einen weit unten im Strom liegenden Beitrag herum geöffnet wird.
CSS-Containment
Beiträge verwenden:
contain: layout;
content-visibility: auto;
contain-intrinsic-size: 1px 180px;
Dies ermöglicht es dem Browser, unnötige Rendering-Arbeit für Beiträge zu vermeiden, die derzeit nicht sichtbar sind.
Lazy Images
Bilder, die noch keinen Lade-Modus angegeben haben, erhalten automatisch:
loading="lazy"
decoding="async"
Das verhindert, dass ein langes Thema mit vielen Bildern sofort alles lädt.
Ladezustand
Das Modal zeigt nicht nur eine leere weiße/leere Fläche an, während die Anfrage gestellt wird.
Es hat ein Skeleton-UI mit:
- Avatar-Platzhaltern
- Benutzername/Name-Platzhaltern
- Beitragskörper-Platzhaltern
- Schimmer-Animation
Der Schimmer respektiert:
prefers-reduced-motion
so dass die Animation für Benutzer, die reduzierte Bewegung angefordert haben, deaktiviert ist.
Scrollposition stabil halten
Es gibt einige Stellen, an denen das Component die Scrollposition manuell manipulieren muss.
Wenn beispielsweise frühere Beiträge geladen werden, erhöht der neu eingefügte Inhalt die Scrollhöhe.
Einfaches Voranfügen der Beiträge würde dazu führen, dass sich die aktuelle Position des Benutzers verschiebt.
Das Component zeichnet daher die vorherige Scrollhöhe auf und kompensiert die Differenz, nachdem die Beiträge eingefügt wurden.
Dies hält den aktuell sichtbaren Inhalt ungefähr an derselben Stelle.
Gleiches gilt, wenn zu einem bestimmten Beitrag gesprungen wird.
Das Component führt einen Positionsbestimmungsschritt nach dem Rendering durch und überprüft die Position erneut in nachfolgenden Frames, um Inhalte zu berücksichtigen, die sich möglicherweise noch einrichten.
Themenpräsenz
Wenn die relevanten Themedaten verfügbar sind, kann die Vorschau auch die Themenpräsenz-Informationen von Discourse unten im Modal anzeigen.
So können Benutzer sehen, wer das Thema gerade noch ansieht, ohne die Vorschau verlassen zu müssen.
Interaktion mit mobilen Menüs und Fokus
Mobile brachte eine weitere Kategorie von Problemen mit sich.
Einige Discourse-UI-Elemente verwenden gemeinsame Modal/Menu-Services, und diese Services wissen nicht unbedingt, dass die Themen-Vorschau derzeit als verschachtelter Kontext für das Durchblättern dient.
Das Component hat daher zusätzliche Handhabung für:
modal.close()- Float Kit-Menüs
- Fokuswiederherstellung
- den Composer
- Lightbox-Tastatursteuerungen
- Body-Scroll-Locks
Wenn beispielsweise ein Menü intern versucht, die globale Modal-Schließmethode aufzurufen, sollte das nicht versehentlich die gesamte Themen-Vorschau schließen.
Ähnlich muss, wenn der Composer geöffnet ist, der Fokus innerhalb des Composers bleiben und nicht in den Fokus-Kontext der Vorschau zurückgezogen werden.
Konfiguration
Das Component bietet derzeit die folgenden Einstellungen an:
| Einstellung | Standard | Beschreibung |
|---|---|---|
trigger_style |
row |
Ganze Zeile klickbar machen oder expliziten Button verwenden |
plugin_outlet |
topic-list-after-title |
Outlet, das vom Button-Auslöser verwendet wird |
enable_prefetch |
true |
Hintergrund-Themen-Prefetching aktivieren/deaktivieren |
max_concurrent_prefetches |
2 |
Maximale gleichzeitige Prefetch-Anfragen |
prefetch_debounce_ms |
400 |
Verzögerung vor dem Start eines Prefetches |
prefetch_root_margin_px |
50 |
Prefetching diese Anzahl an Pixeln vor dem Betreten des Viewports durch die Zeile starten |
max_prefetches_per_minute |
15 |
Maximale spekulative Anfragen pro Minute |
Die Prefetch-Steuerungen sind absichtlich konfigurierbar, da verschiedene Gemeinschaften sehr unterschiedliche Verkehrs- und Netzwerkmerkmale haben können.
Eines der Hauptdesignziele: Normales Discourse nicht kaputt machen
Ich habe versucht, das Component so nah wie möglich an die vorhandene Architektur von Discourse zu halten.
Es implementiert keinen eigenen Beitrags-Renderer, keinen eigenen Composer, kein eigenes Themenmodell oder keinen eigenen, vollständig getrennten Beitragsstrom.
Stattdessen baut es einen temporären Durchblättern-Kontext um die vorhandenen Komponenten und Services von Discourse herum.
Das ist auch der Grund, warum einige Teile der Implementierung komplizierter sind, als sie auf den ersten Blick erscheinen mögen.
Die interessantere Herausforderung war:
Kann sich ein Thema fast wie ein normales Discourse-Thema verhalten, während es tatsächlich in einem anderen UI-Kontext angezeigt wird?
Das erforderte es, die Grenzen zwischen den globalen Services von Discourse und der lokalen Vorschau zu behandeln.





