| Zusammenfassung | Topic Preview Modal – Öffnen und Interagieren mit Themen, 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 | |
| Installationsanleitung | Wie installiere ich ein Theme oder Theme-Komponente | |
| Neu bei Discourse Themes? | Einsteiger-Leitfaden zur Verwendung von Discourse Themes |
Diese Theme-Komponente installieren
Topic Preview Modal – Öffnen und Interagieren mit Themen, ohne die Themenliste zu verlassen
Ich habe eine neue Discourse-Theme-Komponente namens Topic Preview Modal erstellt.
Die Idee ist recht einfach:
Ein Thema direkt aus der Themenliste in einem nativen Discourse-Modal öffnen, lesen und damit interagieren, und dann weiter in der Liste browsen, ohne diese zu verlassen.
Es begann mit Facebook-style Topic Modal - Is it better? , entwickelte sich aber zu einer ziemlich umfangreichen Integration mit Discourses Systemen für Themen, Post-Streams, Composer, Modals, Lesezeichen, Routing, Präsenz, Leseverfolgung und Prefetching.
Warum?
Der normale Discourse-Workflow ist:
- Du durchsuchst eine Themenliste.
- Du klickst auf ein Thema.
- Discourse navigiert zu
/t/.... - Du liest/antwortest/interagierst mit dem Thema.
- Du gehst zurück zur Themenliste.
Für viele Workflows ist das völlig in Ordnung.
Wenn man jedoch durch eine belebte Themenliste browsed, möchte man manchmal nur schnell ein Thema inspizieren, ein paar Beiträge lesen, die neuesten Antworten prüfen, auf etwas reagieren oder eine kurze Frage beantworten.
Für diesen Anwendungsfall fühlt sich das Verlassen der Themenliste unnötig aufwendig an.
Das Ziel dieser Komponente war es daher, die Themenliste mehr wie einen Posteingang zu gestalten:
Themenliste → Vorschau → Interaktion → Schließen → genau dort weitermachen, wo man war.
Was es macht
Die Vorschau ist nicht nur ein statischer Auszug.
Sie rendert die tatsächlichen Discourse-Beitragskomponenten innerhalb eines nativen DModal.
Das bedeutet, dass Nutzer können:
- Beiträge lesen
- durch das Thema scrollen
- frühere Beiträge laden
- weitere Beiträge darunter laden
- auf Beiträge reagieren
- Beiträge als Lesezeichen markieren
- 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
- Themenpräsenz sehen
- Links zu anderen Beiträgen innerhalb desselben Themas folgen
- direkt zum relevanten Beitrag springen
- das volle Thema öffnen, wenn nötig
Die Absicht ist, dass sich die Vorschau so nah wie möglich anfühlt, als würde man das Thema tatsächlich öffnen.
Zwei Auslöser-Modi
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 ausgeschlossen sind.
Dies macht das Erlebnis beim Browsen einer Themenliste sehr schnell.
2. Expliziter Erweitern-Button
Alternativ kann die Komponente ein kleines Erweitern-Symbol über einen Discourse-Plugin-Ausgang rendern. Custom Themes können einfach einen neuen <PluginOutlet /> erstellen, um den Auslöser anzuzeigen.
In diesem Modus bleibt das normale Verhalten der Themenliste vollständig unberührt.
Der Benutzer klickt auf das Erweitern-Symbol, um die Vorschau zu öffnen, während ein Klick auf den Themennamen die normale Discourse-Navigation ausführt.
Dies ist nützlich, wenn eine Site das Standardinteraktionsmodell der Themenliste beibehalten möchte.
Die Einstellung ist:
trigger_style:
row
oder:
trigger_style:
button
Wenn der Button-Modus verwendet wird, ist der Ausgang ebenfalls 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 ein Thema also 200 Beiträge hat und der Benutzer bis Beitrag #165 gelesen hat, startet das Öffnen der Vorschau bei #166.
Dies macht die Vorschau für das reale Browsen viel nützlicher.
Es bedeutet auch, dass die Komponente mit beiden Seiten des Beitragsstroms umgehen muss:
- Laden früherer Beiträge, wenn nötig
- Laden neuerer Beiträge darunter
Der Button Frühere Beiträge 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 der Komponente ist ihr Prefetch-System.
Das Problem mit einem solchen Modal ist, dass der Benutzer erwartet, dass es sich augenblicklich anfühlt.
Wenn wir erst nach dem Klick des Benutzers mit dem Laden des Themas beginnen, kann das Modal spürbare Zeit mit dem Warten auf das Netzwerk verbringen.
Stattdessen kann die Komponente Themen proaktiv prefetchen, während der Benutzer in der Liste browsed.
Wenn eine Themenzeile den Viewport erreicht, kann ein IntersectionObserver einen Prefetch planen.
Es gibt mehrere Schutzmaßnahmen, um zu verhindern, dass dies zu unkontrolliertem Hintergrundverkehr wird.
Debouncing
Ein Thema löst nicht sofort eine Anfrage aus, nur weil es kurz im Viewport erschienen ist.
Die Komponente wartet die konfigurierte Debounce-Periode ab.
Standard:
400 ms
Dies ist besonders nützlich, wenn man schnell durch eine lange Themenliste scrollt.
Root margin
Das Prefetching kann etwas früher 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 Standard ist:
15
Selbst wenn der Benutzer weiterhin durch Hunderte von Themen scrollt, generiert die Komponente nicht kontinuierlich spekulatives Anfragen.
0 deaktiviert das Limit.
Prefetch kann vollständig deaktiviert werden
Wenn eine Site keinen spekulativen Netzwerkverkehr möchte:
enable_prefetch = false
Die Komponente funktioniert weiterhin normal. Themen laden einfach, wenn die Vorschau geöffnet wird.
Prefetch-Daten werden separat von der normalen Themen-Navigation gespeichert
Hier gibt es ein wichtiges Implementierungsdetail.
Die geprefetchte Antwort wird nicht sofort in Discourses normale topic_<id> Preload-Schlüssel geschrieben.
Stattdessen verwendet die Komponente ihren eigenen Namespace:
topic-preview-modal:prefetch:<topicId>
Nur wenn der Benutzer tatsächlich die Vorschau öffnet, wird das geprefetchte Versprechen zum Kern-Themen-Preload-Schlüssel befördert.
Dies ist beabsichtigt.
Die Vorschau lädt möglicherweise ein Thema beginnend bei last_read_post_number + 1, und ich möchte nicht, dass diese vorschau-spezifische Antwort in eine normale Themen-Route-Navigation überläuft.
Der Lebenszyklus ist also im Wesentlichen:
Thema betritt Viewport
↓
prefetch
↓
privater Preload-Speicher
↓
Benutzer öffnet Vorschau
↓
Preload befördern
↓
Topic.find()/PostStream verwendet dasselbe Versprechen
Das bedeutet auch, dass das Modal nicht warten muss, bis die Prefetch-Anfrage abgeschlossen ist, bevor es sich öffnet.
Das Modal kann sich sofort mit seinem Skeleton öffnen, während dasselbe Versprechen weiterhin aufgelöst wird.
Mobile-Unterstützung
Dies war tatsächlich einer der Gründe, warum ich erheblich mehr Zeit in die Implementierung investiert habe.
Die ursprüngliche Idee funktionierte auf dem Desktop recht gut, aber Mobile offenbarte mehrere Probleme bezüglich:
- Touch-Interaktion
- Modal-Scrollen
- Fokus
- verschachtelten Menüs
- dem Composer
- Beitrags-Sichtbarkeit
- Bildladen
- Leistung
Die finale Implementierung behandelt das Modal daher nicht als völlig separates Miniatur-Forum.
Stattdessen wiederverwendet es so viel von Discourses bestehender Infrastruktur wie möglich.
Echte Discourse-Beitragskomponenten
Das Modal erstellt Beiträge nicht neu mit einer vereinfachten benutzerdefinierten Vorlage.
Es rendert Discourses tatsächliche:
Post
PostSmallAction
Komponenten.
Dies ist wichtig, da sonst die Vorschau schnell zu einer zweiten Implementierung der Beitrags-UI werden würde.
Die Komponente übergibt die relevanten Aktionen an die normalen Beitragskomponenten, einschließlich Dinge wie:
- antworten
- bearbeiten
- löschen
- wiederherstellen
- melden
- Verlauf
- Lesezeichen
- Wiki
- sperren/entsperren
- Beitragstyp
- Eigentumsänderungen
- Abzeichen
- versteckte Beiträge
- Zitieren
- usw.
Das Ergebnis ist, dass sich die Vorschau viel mehr wie ein normales Thema verhalten kann als eine traditionelle „Vorschau“-Komponente.
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, sodass die Antwort wie eine normale Beitragsantwort funktioniert.
Zitieren ausgewählten Textes
Die Komponente integriert sich auch mit PostTextSelection.
Das bedeutet, dass Nutzer Text innerhalb der Vorschau auswählen und Discourses normalen Zitat-/Antwort-Workflow verwenden können.
Verschachtelte Modals
Ein weiterer kniffliger Teil war Discourses Modal-System.
Beiträge können andere Modals und Dialoge öffnen:
- Melden
- Verlauf
- Abzeichen-bezogene Dialoge
- Eigentumsänderungen
- Löschbestätigungen
- usw.
Wenn diese normalerweise mit dem globalen Modal-Service interagieren dürften, könnte das Öffnen eines davon das gesamte Themen-Vorschau-Modal schließen.
Um das zu vermeiden, erstellt die Komponente einen lokalen Sub-Modal-Mechanismus.
Konzeptionell:
Topic Preview Modal
│
├── Melden-Modal
├── Verlauf-Modal
├── Löschbestätigung
├── Abzeichen-Modal
└── andere beitragbezogene Modals
Die Vorschau bleibt darunter montiert.
Die Komponente patcht vorübergehend die relevanten Modal-Service-Methoden, während sie aktiv ist, und stellt sie wieder her, wenn sie zerstört wird.
Routing innerhalb des Modals
Ein weiteres wichtiges Detail sind Links zu Beiträgen innerhalb desselben Themas.
Wenn ein Beitrag beispielsweise einen Link zu:
/t/my-topic/123
enthält, muss die Vorschau nicht geschlossen und abgelenkt werden.
Stattdessen fängt die Komponente die Navigation innerhalb desselben Themas ab und springt zum angeforderten Beitrag innerhalb des Modals.
Das Gleiche gilt für Links, die das Thema ohne eine bestimmte Beitragsnummer ansprechen.
Dies hält den Benutzer innerhalb der Vorschau.
Wenn der Link zu einem wirklich anderen Thema führt, stellt die Komponente zuerst ihre temporären Service-Patches wieder her und schließt sich, bevor sie den normalen Discourse-Route-Übergang zulässt.
Diese Bereinigung ist wichtig, da sonst die Abonnements und der Timing-Tracker der Vorschau am Leben bleiben könnten, während die echte Themen-Route initialisiert wird.
Leseverfolgung und Zeitverfolgung
Ich wollte auch, dass sich die Vorschau aus Discourses Sicht korrekt verhält.
Das Öffnen einer Vorschau sollte nicht bedeuten, dass die Leseverfolgung vollständig umgangen wird.
Die Komponente verarbeitet daher:
- Themenbesuchs-Verfolgung
- Sichtbare Beitrags-Verfolgung
- Themen-Timing
- Updates des zuletzt 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 Timing-Intervall auf 60 Sekunden.
Synchronisierung des ungelesenen Status der Themenliste
Hier gab es ein weiteres subtiles Problem.
Das Aktualisieren von Discourses Themen-Tracking-Status allein reicht nicht aus, um das ungelesene Abzeichen direkt in einer Themenlistenzeile zu aktualisieren.
Die Komponente aktualisiert daher das tatsächliche Themenobjekt, das mit der Zeile verbunden ist, nachdem Timing-Informationen gesendet wurden.
Es aktualisiert Werte wie:
last_read_post_number
unread_posts
unread
new_posts
wenn appropriate.
Das bedeutet, dass die Themenliste nach dem Lesen eines Themas im Modal den neuen Lesestatus sofort widerspiegeln kann, anstatt einen vollständigen Seiten-Refresh zu erfordern.
Beitrags-Sichtbarkeit
Die Vorschau verwendet einen gemeinsamen IntersectionObserver, um zu bestimmen, wann einzelne Beiträge sichtbar werden.
Es gibt auch eine synchrone Sichtbarkeitsprüfung, wenn der Observer angehängt wird.
Dies behandelt einen Randfall, in dem ein Beitrag bereits sichtbar ist, wenn er montiert wird, aber der asynchrone erste IntersectionObserver-Callback noch nicht ausgelöst wurde.
Dies ist besonders relevant für sehr kurze Themen, bei denen das gesamte Thema bereits sichtbar sein kann, wenn das Modal geöffnet wird.
Leistungsaspekte
Ein Hauptziel war es, zu verhindern, dass das Modal zu einer leistungsaufwendigen Miniatur-Themen-Seite wird.
Einige Dinge werden speziell dafür getan.
Progressives Rendering
Der initiale Ladevorgang rendert nicht sofort jeden Beitrag.
Die Komponente rendert zuerst genug Beiträge, um die Zielposition zu erreichen.
Die verbleibenden Beiträge werden dann progressiv gerendert unter Verwendung von:
requestIdleCallback
wenn verfügbar, mit einem Fallback zu setTimeout.
Dies ist besonders nützlich, wenn man ein langes Thema um einen Beitrag weit unten im Stream herum öffnet.
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 Lademodus angegeben haben, erhalten automatisch:
loading="lazy"
decoding="async"
Dies verhindert, dass ein langes Thema mit vielen Bildern sofort alles lädt.
Ladezustand
Das Modal zeigt nicht nur einen leeren weißen Bereich an, während die Anfrage gestellt wird.
Es hat eine Skeleton-UI mit:
- Avatar-Platzhaltern
- Benutzername/Namen-Platzhaltern
- Beitragskörper-Platzhaltern
- Shimmer-Animation
Der Shimmer beachtet:
prefers-reduced-motion
so dass die Animation für Benutzer deaktiviert ist, die reduzierte Bewegung angefordert haben.
Stabilisierung der Scroll-Position
Es gibt einige Stellen, an denen die Komponente die Scroll-Position manuell manipulieren muss.
Wenn beispielsweise frühere Beiträge geladen werden, erhöht der neu eingefügte Inhalt die Scroll-Höhe.
Das einfache Voranstellen der Beiträge würde dazu führen, dass die aktuelle Position des Benutzers springt.
Die Komponente zeichnet daher die vorherige Scroll-Höhe auf und kompensiert den Unterschied, nachdem die Beiträge eingefügt wurden.
Dies hält den aktuell sichtbaren Inhalt ungefähr an derselben Stelle.
Das Gleiche gilt beim Springen zu einem bestimmten Beitrag.
Die Komponente führt einen Post-Rendering-Positionierungsschritt durch und überprüft die Position in nachfolgenden Frames erneut, um Inhalte zu berücksichtigen, die sich möglicherweise noch einpendeln.
Themen-Präsenz
Wenn die relevanten Thementdaten verfügbar sind, kann die Vorschau auch Discourses Themen-Präsenzinformationen am unteren Rand des Modals anzeigen.
So können Benutzer sehen, wer else sich das Thema gerade ansieht, ohne die Vorschau verlassen zu müssen.
Interaktion mit mobilen Menüs und Fokus
Mobile führte eine weitere Kategorie von Problemen ein.
Einige Discourse-UI-Elemente verwenden gemeinsame Modal-/Menü-Services, und diese Services wissen nicht unbedingt, dass die Themen-Vorschau derzeit als verschachtelter Browser-Kontext fungiert.
Die Komponente hat daher zusätzliche Handhabung für:
modal.close()- Float Kit-Menüs
- Fokus-Wiederherstellung
- den Composer
- Lightbox-Tastatursteuerungen
- Body-Scroll-Locks
Wenn ein Menü intern versucht, die globale Modal-Schließmethode aufzurufen, sollte das nicht versehentlich das gesamte Themen-Vorschau-Modal schließen.
Wenn der Composer geöffnet ist, muss der Fokus innerhalb des Composers bleiben, anstatt in den Fokus-Kontext der Vorschau gezogen zu werden.
Konfiguration
Die Komponente stellt derzeit die folgenden Einstellungen zur Verfügung:
| Einstellung | Standard | Beschreibung |
|---|---|---|
trigger_style |
row |
Ganze Zeile klickbar machen oder expliziten Button verwenden |
plugin_outlet |
topic-list-after-title |
Ausgang, der 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 Prefetch |
prefetch_root_margin_px |
50 |
Prefetching in dieser vielen Pixeln vor dem Betreten des Viewports starten |
max_prefetches_per_minute |
15 |
Maximale spekulative Anfragen pro Minute |
Die Prefetch-Steuerungen sind absichtlich konfigurierbar, da verschiedene Communities sehr unterschiedliche Verkehrsmuster und Hosting-/Netzwerkmerkmale haben können.
Eines der Hauptdesignziele: Normales Discourse nicht kaputt machen
Ich habe versucht, die Komponente so nah wie möglich an Discourses bestehende Architektur zu halten.
Es implementiert keinen eigenen Beitrags-Renderer, keinen eigenen Composer, kein eigenes Themenmodell oder keinen eigenen völlig separaten Beitragsstrom.
Stattdessen baut es einen temporären Browser-Kontext um Discourses bestehende Komponenten und Services 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 innerhalb eines anderen UI-Kontexts angezeigt wird?
Das erforderte den Umgang mit den Grenzen zwischen Discourses globalen Services und der lokalen Vorschau.


