| Riepilogo | Modale anteprima argomento – apri e interagisci con gli argomenti senza lasciare l’elenco degli argomenti | |
| Anteprima | 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 | |
| Guida all’installazione | Come installare un tema o un componente del tema | |
| Nuovo nei temi di Discourse? | Guida per principianti all’uso dei temi di Discourse |
Installa questo componente del tema
Modale anteprima argomento – apri e interagisci con gli argomenti senza lasciare l’elenco degli argomenti
Ho creato un nuovo componente del tema per Discourse chiamato Modale anteprima argomento.
L’idea è piuttosto semplice:
Apri un argomento direttamente dall’elenco degli argomenti in un modale nativo di Discourse, leggi e interagisci con l’argomento, e poi continua a sfogliare l’elenco senza allontanarti da esso.
È iniziato da Facebook-style Topic Modal - Is it better? , ma alla fine ha richiesto una discreta integrazione con i sistemi di Discourse per gli argomenti, il flusso dei post, il compositore, il modale, i segnalibri, il routing, la presenza, il tracciamento delle letture e il pre-caricamento.
Perché?
Il flusso normale di Discourse è:
- Stai sfogliando un elenco di argomenti.
- Clicchi su un argomento.
- Discourse naviga verso
/t/.... - Leggi/rispondi/interagisci con l’argomento.
- Torni all’elenco degli argomenti.
Per molti flussi di lavoro, questo va perfettamente bene.
Tuttavia, quando si sfoglia un elenco di argomenti molto attivo, a volte voglio solo ispezionare rapidamente un argomento, leggere alcuni post, controllare le ultime risposte, reagire a qualcosa o rispondere a una domanda veloce.
Per questo caso d’uso, lasciare l’elenco degli argomenti sembra un costo inutile.
L’obiettivo di questo componente era quindi rendere l’elenco degli argomenti simile a una casella di posta:
elenco argomenti → anteprima → interazione → chiusura → continua esattamente da dove eri rimasto.
Cosa fa
L’anteprima non è solo un estratto statico.
Rende effettivi i componenti dei post di Discourse all’interno di un DModal nativo.
Questo significa che gli utenti possono:
- leggere i post
- scorrere l’argomento
- caricare post precedenti
- caricare più post in basso
- reagire ai post
- segnalare i post con segnalibro
- citare il testo
- rispondere all’argomento
- rispondere a singoli post
- modificare i post quando consentito
- eliminare/recuperare i post quando consentito
- segnalare i post
- visualizzare la cronologia dei post
- eseguire varie azioni normali sui post
- vedere la presenza nell’argomento
- seguire i link ad altri post nello stesso argomento
- saltare direttamente al post rilevante
- aprire l’argomento completo quando necessario
L’intenzione è che l’anteprima si senta il più possibile simile all’apertura effettiva dell’argomento.
Due modalità di attivazione
Ci sono due modi per aprire l’anteprima.
1. Riga dell’elenco argomenti intera
Questa è l’impostazione predefinita.
L’intera riga dell’elenco degli argomenti diventa cliccabile, mentre elementi interattivi comuni come:
- schede utente
- partecipanti
- link alle categorie
- tag
- link allo stato dell’argomento
- selezione multipla
sono esclusi dall’attivazione del modale.
Questo rende l’esperienza molto veloce quando si sfoglia un elenco di argomenti.
2. Pulsante di espansione esplicito
In alternativa, il componente può rendere un’icona di espansione piccola tramite un’uscita del plugin Discourse. I temi personalizzati possono semplicemente creare un nuovo <PluginOutlet /> per mostrare l’attivatore.
In questa modalità, il comportamento normale dell’elenco degli argomenti rimane completamente intatto.
L’utente clicca sull’icona di espansione per aprire l’anteprima, mentre cliccare sul titolo dell’argomento esegue ancora la navigazione normale di Discourse.
Questo è utile se un sito vuole preservare il modello di interazione standard dell’elenco degli argomenti.
L’impostazione è:
trigger_style:
row
oppure:
trigger_style:
button
Quando si utilizza la modalità pulsante, anche l’uscita è configurabile.
L’anteprima inizia dalla posizione non letta dell’utente
Uno dei dettagli importanti è che il modale non carica semplicemente il primo post.
Quando un argomento è già stato letto parzialmente, l’anteprima calcola:
last_read_post_number + 1
e si apre intorno a quel post.
Quindi, se un argomento ha 200 post e l’utente ha letto fino al post #165, l’apertura dell’anteprima inizia intorno al #166.
Questo rende l’anteprima molto più utile per la navigazione reale.
Significa anche che il componente deve gestire entrambi i lati del flusso dei post:
- caricare post precedenti quando necessario
- caricare post più recenti in basso
Il pulsante Post precedenti viene visualizzato quando ci sono post sopra l’intervallo attualmente caricato, mentre un sentinel IntersectionObserver carica automaticamente più post quando l’utente raggiunge il fondo.
Pre-caricamento
Una delle parti più grandi del componente è il suo sistema di pre-caricamento.
Il problema con un modale del genere è che l’utente si aspetta che sia istantaneo.
Se iniziamo a caricare l’argomento solo dopo che l’utente ha cliccato, il modale può ancora impiegare un tempo notevole ad aspettare la rete.
Invece, il componente può pre-caricare proattivamente gli argomenti mentre l’utente sfoglia l’elenco.
Quando una riga dell’argomento si avvicina alla viewport, un IntersectionObserver può pianificare un pre-caricamento.
Ci sono diverse salvaguardie per impedire che questo si trasformi in traffico di background incontrollato.
Debouncing
Un argomento non attiva immediatamente una richiesta solo perché è apparso brevemente nella viewport.
Il componente aspetta per il periodo di debounce configurato.
Predefinito:
400 ms
Questo è particolarmente utile quando si scorre rapidamente un lungo elenco di argomenti.
Margine radice
Il pre-caricamento può iniziare leggermente prima che l’argomento entri effettivamente nella viewport.
Predefinito:
50 px
Questo dà alla richiesta un piccolo vantaggio iniziale.
Limite di richieste simultanee
Il numero di pre-caricamenti simultanei è limitato.
Predefinito:
2
L’impostazione consente tra 1 e 6 pre-caricamenti simultanei.
Budget per minuto
C’è anche un secondo meccanismo di protezione:
max_prefetches_per_minute
Il predefinito è:
15
Quindi, anche se l’utente continua a scorrere centinaia di argomenti, il componente non genererà continuamente richieste speculative.
0 disabilita il limite.
Il pre-caricamento può essere disabilitato completamente
Se un sito non vuole alcun traffico di rete speculativo:
enable_prefetch = false
Il componente continua a funzionare normalmente. Gli argomenti vengono semplicemente caricati quando l’anteprima viene aperta.
I dati pre-caricati sono mantenuti separati dalla navigazione normale degli argomenti
C’è un importante dettaglio di implementazione qui.
La risposta pre-caricata non viene scritta immediatamente nella chiave di preload normale topic_<id> di Discourse.
Invece, il componente utilizza il proprio namespace:
topic-preview-modal:prefetch:<topicId>
Solo quando l’utente apre effettivamente l’anteprima, la promessa pre-caricata viene promossa alla chiave di preload dell’argomento principale.
Questo è intenzionale.
L’anteprima potrebbe stare caricando un argomento partendo da last_read_post_number + 1, e non voglio che quella risposta specifica dell’anteprima finisca nella navigazione di una rotta argomento normale.
Quindi il ciclo di vita è essenzialmente:
l'argomento entra nella viewport
↓
pre-caricamento
↓
magazzinaggio preload privato
↓
l'utente apre l'anteprima
↓
promuovi il preload
↓
Topic.find()/PostStream usa la stessa promessa
Questo significa anche che il modale non deve aspettare che la richiesta di pre-caricamento finisca prima di aprirsi.
Il modale può aprirsi immediatamente con il suo scheletro mentre la stessa promessa continua a risolversi.
Supporto mobile
Questo è stato in realtà uno dei motivi per cui ho dedicato considerevolmente più tempo all’implementazione.
L’idea iniziale funzionava ragionevolmente bene sul desktop, ma il mobile ha esposto diversi problemi legati a:
- interazione touch
- scorrimento del modale
- focus
- menu nidificati
- il compositore
- visibilità dei post
- caricamento immagini
- prestazioni
L’implementazione finale evita quindi di trattare il modale come un mini forum completamente separato.
Invece, riutilizza il più possibile l’infrastruttura esistente di Discourse.
Componenti post reali di Discourse
Il modale non ricrea i post utilizzando un modello personalizzato semplificato.
Rende i componenti effettivi di Discourse:
Post
PostSmallAction
Questo è importante perché altrimenti l’anteprima diventerebbe rapidamente una seconda implementazione dell’interfaccia utente del post.
Il componente passa le azioni rilevanti ai normali componenti dei post, includendo cose come:
- risposta
- modifica
- eliminazione
- recupero
- segnalazione
- cronologia
- segnalibro
- wiki
- blocco/sblocco
- tipo di post
- cambiamenti di proprietà
- badge
- post nascosti
- citazione
- ecc.
Il risultato è che l’anteprima può comportarsi molto più come un argomento normale che come un componente “anteprima” tradizionale.
Risposte e il compositore
Il compositore è una delle parti più complicate.
L’anteprima può aprire il compositore normale di Discourse per:
Rispondere all’argomento
Il compositore dell’argomento viene aperto con il modello dell’argomento e le informazioni della bozza corrette.
Rispondere a un post specifico
Il post viene passato al compositore in modo che la risposta si comporti come una risposta normale a un post.
Citare il testo selezionato
Il componente si integra anche con PostTextSelection.
Questo significa che gli utenti possono selezionare il testo all’interno dell’anteprima e utilizzare il flusso normale di citazione/risposta di Discourse.
Modali nidificati
Un’altra parte delicata è stato il sistema di modali di Discourse.
I post possono aprire altri modali e dialoghi:
- segnalazione
- cronologia
- dialoghi relativi ai badge
- cambiamenti di proprietà
- conferme di eliminazione
- ecc.
Se questi fossero stati lasciati interagire normalmente con il servizio modale globale, l’apertura di uno di essi avrebbe potuto chiudere l’intero anteprima dell’argomento.
Per evitare questo, il componente crea un meccanismo di sotto-modale locale.
Concettualmente:
Modale anteprima argomento
│
├── Modale segnalazione
├── Modale cronologia
├── Conferma eliminazione
├── Modale badge
└── altro modale relativo al post
L’anteprima rimane montata sotto.
Il componente patcha temporaneamente i metodi rilevanti del servizio modale mentre è attivo e li ripristina quando viene distrutto.
Routing all’interno del modale
Un altro dettaglio importante sono i link ai post all’interno dello stesso argomento.
Ad esempio, se un post contiene un link a:
/t/my-topic/123
l’anteprima non ha bisogno di chiudersi e navigare altrove.
Invece, il componente intercetta la navigazione dello stesso argomento e salta al post richiesto all’interno del modale.
Lo stesso vale per i link che puntano all’argomento senza un numero di post specifico.
Questo mantiene l’utente all’interno dell’anteprima.
Se il link punta a un argomento genuinamente diverso, il componente prima ripristina le sue patch di servizio temporanee e si chiude prima di permettere la transizione di rotta normale di Discourse.
Questa pulizia è importante perché altrimenti le sottoscrizioni dell’anteprima e il tracker di tempo potrebbero rimanere vive mentre la rotta dell’argomento reale viene inizializzata.
Tracciamento delle letture e tracciamento del tempo
Volevo anche che l’anteprima si comportasse correttamente dal punto di vista di Discourse.
Aprire un’anteprima non dovrebbe significare che il tracciamento delle letture è completamente ignorato.
Il componente gestisce quindi:
- tracciamento delle visite all’argomento
- tracciamento dei post visibili
- tempistica dell’argomento
- aggiornamenti dell’ultimo post letto
Il tracker di tempistica utilizza un IntersectionObserver per determinare quali post sono effettivamente visibili.
Ogni 5 secondi, la tempistica dei post visibili viene inviata a:
/topics/timings
Quando il modale si chiude, viene eseguita un’ultima invio in modo che gli ultimi secondi non vengano persi.
L’implementazione limita anche un singolo intervallo di tempistica a 60 secondi.
Mantenere sincronizzato lo stato non letto dell’elenco degli argomenti
C’era un altro problema sottile qui.
Aggiornare lo stato di tracciamento degli argomenti di Discourse da solo non è sufficiente per aggiornare il badge non letto visualizzato direttamente su una riga dell’elenco degli argomenti.
Il componente aggiorna quindi l’oggetto argomento effettivo associato alla riga dopo che le informazioni di tempistica sono state inviate.
Aggiorna valori come:
last_read_post_number
unread_posts
unread
new_posts
quando appropriato.
Questo significa che dopo aver letto un argomento all’interno del modale, l’elenco degli argomenti può riflettere immediatamente il nuovo stato di lettura invece di richiedere un aggiornamento completo della pagina.
Visibilità dei post
L’anteprima utilizza un IntersectionObserver condiviso per determinare quando i singoli post diventano visibili.
C’è anche un controllo di visibilità sincrono quando l’osservatore è collegato.
Questo gestisce un caso limite in cui un post è già visibile quando viene montato, ma il primo callback asincrono di IntersectionObserver non è ancora stato attivato.
Questo è particolarmente rilevante per argomenti molto brevi in cui l’intero argomento potrebbe già essere visibile quando il modale si apre.
Considerazioni sulle prestazioni
Un obiettivo principale era evitare di trasformare il modale in una pagina argomento miniatura pesante per le prestazioni.
Alcune cose sono fatte specificamente per questo.
Rendering progressivo
Il caricamento iniziale non rende immediatamente ogni post.
Il componente rende prima abbastanza post per raggiungere la posizione target.
I post rimanenti vengono quindi resi progressivamente utilizzando:
requestIdleCallback
quando disponibile, con un fallback a setTimeout.
Questo è particolarmente utile quando si apre un argomento lungo intorno a un post lontano nel flusso.
Contenimento CSS
I post utilizzano:
contain: layout;
content-visibility: auto;
contain-intrinsic-size: 1px 180px;
Questo permette al browser di evitare di fare lavoro di rendering non necessario per i post che non sono attualmente visibili.
Immagini pigre
Le immagini che non hanno già specificato una modalità di caricamento ricevono automaticamente:
loading="lazy"
decoding="async"
Questo impedisce a un argomento lungo con molte immagini di caricare tutto immediatamente.
Stato di caricamento
Il modale non mostra solo un’area bianca/vuota mentre la richiesta viene effettuata.
Ha un’interfaccia utente scheletrica con:
- placeholder avatar
- placeholder nome utente/nome
- placeholder corpo del post
- animazione shimmer
Lo shimmer rispetta:
prefers-reduced-motion
quindi l’animazione è disabilitata per gli utenti che hanno richiesto una riduzione del movimento.
Mantenere la posizione di scorrimento stabile
Ci sono alcuni punti in cui il componente deve manipolare manualmente la posizione di scorrimento.
Ad esempio, quando si caricano post precedenti, il contenuto appena inserito aumenta l’altezza di scorrimento.
Prependere semplicemente i post farebbe saltare la posizione attuale dell’utente.
Il componente registra quindi l’altezza di scorrimento precedente e compensa la differenza dopo che i post sono stati inseriti.
Questo mantiene il contenuto attualmente visibile approssimativamente nello stesso posto.
Lo stesso vale quando si salta a un post particolare.
Il componente esegue un passaggio di posizionamento post-rendering e verifica nuovamente la posizione nei frame successivi per tenere conto del contenuto che potrebbe ancora stabilizzarsi.
Presenza nell’argomento
Quando i dati rilevanti dell’argomento sono disponibili, l’anteprima può anche visualizzare le informazioni sulla presenza dell’argomento di Discourse in fondo al modale.
Quindi gli utenti possono vedere chi altro sta attualmente visualizzando l’argomento senza dover lasciare l’anteprima.
Interazione con menu mobili e focus
Il mobile ha introdotto un’altra categoria di problemi.
Alcuni elementi UI di Discourse utilizzano servizi modale/menu condivisi, e questi servizi non sanno necessariamente che l’anteprima dell’argomento sta attualmente agendo come un contesto di navigazione nidificato.
Il componente ha quindi una gestione aggiuntiva intorno a:
modal.close()- menu Float Kit
- ripristino del focus
- il compositore
- controlli della tastiera lightbox
- blocchi di scorrimento del corpo
Ad esempio, se un menu cerca internamente di chiamare il metodo di chiusura modale globale, questo non dovrebbe accidentalmente chiudere l’intera anteprima dell’argomento.
Similmente, quando il compositore è aperto, il focus deve rimanere all’interno del compositore invece di essere tirato indietro nel contesto di focus dell’anteprima.
Configurazione
Il componente espone attualmente le seguenti impostazioni:
| Impostazione | Predefinito | Descrizione |
|---|---|---|
trigger_style |
row |
Rendi l’intera riga cliccabile o usa un pulsante esplicito |
plugin_outlet |
topic-list-after-title |
Uscita utilizzata dall’attivatore del pulsante |
enable_prefetch |
true |
Abilita/disabilita il pre-caricamento di background degli argomenti |
max_concurrent_prefetches |
2 |
Numero massimo di richieste di pre-caricamento simultanee |
prefetch_debounce_ms |
400 |
Ritardo prima di iniziare un pre-caricamento |
prefetch_root_margin_px |
50 |
Inizia il pre-caricamento questo numero di pixel prima che la riga entri nella viewport |
max_prefetches_per_minute |
15 |
Numero massimo di richieste speculative al minuto |
I controlli di pre-caricamento sono intenzionalmente configurabili perché comunità diverse possono avere modelli di traffico e caratteristiche di hosting/rete molto diversi.
Uno degli obiettivi di design principali: non rompere Discourse normale
Ho cercato di mantenere il componente il più vicino possibile all’architettura esistente di Discourse.
Non implementa il suo proprio renderer di post, il suo compositore, il suo modello argomento o il suo flusso di post completamente separato.
Invece, costruisce un contesto di navigazione temporaneo intorno ai componenti e servizi esistenti di Discourse.
Questo è anche il motivo per cui alcune parti dell’implementazione sono più complicate di quanto potrebbero apparire inizialmente.
La sfida più interessante è stata:
Un argomento può comportarsi quasi come un argomento normale di Discourse mentre viene effettivamente visualizzato all’interno di un altro contesto UI?
Questo ha richiesto di gestire i confini tra i servizi globali di Discourse e l’anteprima locale.


