| 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 | |
| Ti è stato utile? | > ./support --coffee | |
| Guida all’installazione | Come installare un tema o un componente di tema | |
| Nuovo ai temi Discourse? | Guida per principianti all’uso dei temi Discourse |
Installa questo componente di tema
Topic Preview Modal – apri e interagisci con gli argomenti senza lasciare l’elenco degli argomenti
Ho creato un nuovo componente di tema per Discourse chiamato Topic Preview Modal.
L’idea è piuttosto semplice:
Apri un argomento direttamente dall’elenco degli argomenti in una modale nativa di Discourse, leggi e interagisci con l’argomento, quindi continua a sfogliare l’elenco senza navigare altrove.
È nato da Facebook-style Topic Modal - Is it better? , ma alla fine ha richiesto un’integrazione piuttosto ampia con i sistemi di Discourse per argomenti, flusso di post, compositore, modali, segnalibri, routing, presenza, tracciamento della lettura e pre-fetching.
Perché?
Il flusso normale di Discourse è:
- Stai sfogliando un elenco di argomenti.
- Fai clic su un argomento.
- Discourse naviga a
/t/.... - Leggi/rispondi/interagisci con l’argomento.
- Torni all’elenco degli argomenti.
Per molti flussi di lavoro, questo va benissimo.
Tuttavia, quando si sfoglia un elenco di argomenti molto attivo, a volte si vuole solo ispezionare rapidamente un argomento, leggere alcuni post, controllare le ultime risposte, reagire a qualcosa o rispondere a una domanda veloce.
Per quel caso d’uso, lasciare l’elenco degli argomenti sembra un costo ingiustificato.
L’obiettivo di questo componente era quindi far sì che l’elenco degli argomenti si comportasse più come una casella di posta:
elenco argomenti → anteprima → interazione → chiusura → continua esattamente da dove eri.
Cosa fa
L’anteprima non è solo un estratto statico.
Rende i componenti post reali di Discourse all’interno di una DModal nativa.
Ciò significa che gli utenti possono:
- leggere i post
- scorrere l’argomento
- caricare post precedenti
- caricare altri post sotto
- reagire ai post
- mettere in segnalibro i post
- citare il testo
- rispondere all’argomento
- rispondere a singoli post
- modificare i post se consentito
- eliminare/recuperare i post se consentito
- segnalare i post
- visualizzare la cronologia dei post
- eseguire varie normali azioni sui post
- vedere la presenza dell’argomento
- seguire i link ad altri post all’interno dello stesso argomento
- saltare direttamente al post rilevante
- aprire l’argomento completo se necessario
L’intenzione è che l’anteprima dovrebbe sentirsi il più possibile come l’apertura effettiva dell’argomento.
Due modalità di attivazione
Ci sono due modi per aprire l’anteprima.
1. Riga dell’elenco argomenti intera
Questo è il comportamento predefinito.
L’intera riga dell’elenco argomenti diventa cliccabile, mentre gli elementi interattivi comuni come:
- schede utente
- partecipanti
- link alle categorie
- tag
- link di stato dell’argomento
- selezione multipla
sono esclusi dal trigger della 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 plugin outlet di Discourse. I temi personalizzati possono semplicemente creare un nuovo <PluginOutlet /> per mostrare il trigger.
In questa modalità, il comportamento normale dell’elenco argomenti rimane completamente intatto.
L’utente fa clic sull’icona di espansione per aprire l’anteprima, mentre fare clic sul titolo dell’argomento esegue ancora la normale navigazione di Discourse.
Questo è utile se un sito vuole preservare il modello di interazione standard dell’elenco argomenti.
L’impostazione è:
trigger_style:
row
o:
trigger_style:
button
Quando si usa la modalità pulsante, anche l’outlet è configurabile.
L’anteprima inizia dalla posizione non letta dell’utente
Uno dei dettagli importanti è che la modale non carica semplicemente il primo post.
Quando un argomento è stato già 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 di post:
- caricare i post precedenti quando necessario
- caricare post più recenti sotto
Il pulsante Post precedenti viene visualizzato quando ci sono post sopra l’intervallo attualmente caricato, mentre un sentinella IntersectionObserver carica automaticamente altri post quando l’utente raggiunge il fondo.
Pre-fetching
Una delle parti più grandi del componente è il suo sistema di pre-fetching.
Il problema con una modale come questa è che l’utente si aspetta che si senta istantanea.
Se iniziamo a caricare l’argomento solo dopo che l’utente fa clic, la modale può ancora impiegare un tempo notevole ad attendere la rete.
Invece, il componente può pre-fetchare proattivamente gli argomenti mentre l’utente sfoglia l’elenco.
Quando una riga di argomento si avvicina al viewport, un IntersectionObserver può programmare un pre-fetch.
Ci sono diversi meccanismi di sicurezza per impedire che questo si trasformi in traffico di sfondo incontrollato.
Debouncing
Un argomento non innesca immediatamente una richiesta solo perché è apparso brevemente nel viewport.
Il componente attende per il periodo di debouncing configurato.
Predefinito:
400 ms
Questo è particolarmente utile quando si scorre rapidamente attraverso un lungo elenco di argomenti.
Margine radice
Il pre-fetching può iniziare leggermente prima che l’argomento entri effettivamente nel viewport.
Predefinito:
50 px
Questo dà alla richiesta un piccolo vantaggio.
Limite di richieste simultanee
Il numero di pre-fetch simultanei è limitato.
Predefinito:
2
L’impostazione consente da 1 a 6 pre-fetch 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-fetching può essere disabilitato completamente
Se un sito non vuole 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-fetch sono mantenuti separati dalla normale navigazione degli argomenti
C’è un importante dettaglio di implementazione qui.
La risposta pre-fetchata non viene immediatamente scritta nella chiave di preload normale topic_<id> di Discourse.
Invece, il componente usa il proprio namespace:
topic-preview-modal:prefetch:<topicId>
Solo quando l’utente apre effettivamente l’anteprima, la promessa pre-fetchata viene promossa alla chiave di preload dell’argomento principale.
Questo è intenzionale.
L’anteprima potrebbe stare caricando un argomento a partire da last_read_post_number + 1, e non voglio che quella risposta specifica dell’anteprima finisca in una normale navigazione della rotta dell’argomento.
Quindi il ciclo di vita è essenzialmente:
c'è un argomento nel viewport
↓
pre-fetch
↓
archiviazione di preload privata
↓
l'utente apre l'anteprima
↓
promozione del preload
↓
Topic.find()/PostStream usa la stessa promessa
Questo significa anche che la modale non deve aspettare che la richiesta di pre-fetch finisca prima di aprirsi.
La modale può aprirsi immediatamente con il suo scheletro mentre la stessa promessa continua a risolvere.
Supporto mobile
Questo è in realtà stato uno dei motivi per cui ho speso considerevolmente più tempo sull’implementazione.
L’idea iniziale funzionava ragionevolmente bene su desktop, ma il mobile ha evidenziato diversi problemi riguardanti:
- interazione touch
- scorrimento della modale
- focus
- menu annidati
- il compositore
- visibilità dei post
- caricamento delle immagini
- prestazioni
L’implementazione finale evita quindi di trattare la modale come un mini forum completamente separato.
Invece, riutilizza la maggior parte possibile dell’infrastruttura esistente di Discourse.
Componenti post reali di Discourse
La modale non ricrea i post usando un template personalizzato semplificato.
Rende i componenti reali di Discourse:
Post
PostSmallAction
Questo è importante perché altrimenti l’anteprima diventerebbe rapidamente una seconda implementazione dell’interfaccia dei post.
Il componente passa le azioni rilevanti ai normali componenti post, inclusi elementi come:
- risposta
- modifica
- eliminazione
- recupero
- segnalazione
- cronologia
- segnalibro
- wiki
- blocco/sblocco
- tipo di post
- cambi di proprietà
- badge
- post nascosti
- citazione
- ecc.
Il risultato è che l’anteprima può comportarsi molto più come un argomento normale che come un tradizionale componente di “anteprima”.
Risposte e compositore
Il compositore è una delle parti più complicate.
L’anteprima può aprire il normale compositore di Discourse per:
Rispondere all’argomento
Il compositore dell’argomento viene aperto con il modello dell’argomento e le informazioni corrette sulla bozza.
Rispondere a un post specifico
Il post viene passato al compositore in modo che la risposta si comporti come una normale risposta a un post.
Citare il testo selezionato
Il componente si integra anche con PostTextSelection.
Ciò significa che gli utenti possono selezionare il testo all’interno dell’anteprima e usare il normale flusso di citazione/risposta di Discourse.
Modali annidate
Un’altra parte difficile è stato il sistema di modali di Discourse.
I post possono aprire altre modali e dialoghi:
- segnalazione
- cronologia
- dialoghi relativi ai badge
- cambi di proprietà
- conferme di eliminazione
- ecc.
Se questi fossero permessi di interagire normalmente con il servizio di modale globale, l’apertura di uno di essi potrebbe chiudere l’intera anteprima dell’argomento.
Per evitare ciò, il componente crea un meccanismo di sotto-modale locale.
Concettualmente:
Modale Anteprima Argomento
│
├── Modale Segnalazione
├── Modale Cronologia
├── Conferma Eliminazione
├── Modale Badge
└── altra modale relativa ai post
L’anteprima rimane montata sotto.
Il componente applica temporaneamente patch ai metodi rilevanti del servizio di modale mentre è attivo e li ripristina quando viene distrutto.
Routing all’interno della modale
Un altro dettaglio importante sono i link a 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 all’interno dello stesso argomento e salta al post richiesto all’interno della 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 temporanee del servizio e si chiude prima di consentire la normale transizione di rotta di Discourse.
Quella pulizia è importante perché altrimenti le sottoscrizioni e il tracker di tempi dell’anteprima potrebbero rimanere attivi mentre la rotta dell’argomento reale viene inizializzata.
Tracciamento della lettura 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 della lettura viene completamente eluso.
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 usa un IntersectionObserver per determinare quali post sono effettivamente visibili.
Ogni 5 secondi, la tempistica dei post visibili viene inviata a:
/topics/timings
Quando la modale si chiude, viene eseguito un ultimo invio in modo che gli ultimi secondi non vadano persi.
L’implementazione limita anche un singolo intervallo di tempistica a 60 secondi.
Mantenere lo stato non letto dell’elenco argomenti sincronizzato
C’era un altro problema sottile qui.
Aggiornare solo lo stato di tracciamento degli argomenti di Discourse non è sufficiente per aggiornare il badge non letto mostrato direttamente su una riga dell’elenco 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 della modale, l’elenco argomenti può riflettere immediatamente il nuovo stato di lettura senza richiedere un aggiornamento completo della pagina.
Visibilità dei post
L’anteprima usa 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 la prima callback asincrona di IntersectionObserver non è ancora stata eseguita.
Questo è particolarmente rilevante per argomenti molto brevi in cui l’intero argomento potrebbe già essere visibile quando la modale si apre.
Considerazioni sulle prestazioni
Un obiettivo principale era evitare di trasformare la modale in una pagina argomento miniaturizzata pesante in termini di prestazioni.
Vengono fatte alcune cose 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 usando:
requestIdleCallback
quando disponibile, con un fallback a setTimeout.
Questo è particolarmente utile quando si apre un argomento lungo intorno a un post in fondo al flusso.
Containment CSS
I post usano:
contain: layout;
content-visibility: auto;
contain-intrinsic-size: 1px 180px;
Questo consente al browser di evitare di eseguire lavoro di rendering non necessario per i post che non sono attualmente visibili.
Immagini lazy
Le immagini che non hanno già specificato una modalità di caricamento vengono automaticamente dotate di:
loading="lazy"
decoding="async"
Questo impedisce a un argomento lungo con molte immagini di caricare immediatamente tutto.
Stato di caricamento
La modale non mostra solo un’area bianca/vuota mentre la richiesta viene eseguita.
Ha un’interfaccia a scheletro con:
- segnaposto per avatar
- segnaposto per nome utente/nome
- segnaposto per il corpo del post
- animazione shimmer
Lo shimmer rispetta:
prefers-reduced-motion
in modo che l’animazione sia disabilitata per gli utenti che hanno richiesto la 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 inserito aumenta l’altezza di scorrimento.
Semplicemente anteporre 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 di nuovo la posizione nei frame successivi per tenere conto del contenuto che potrebbe ancora stabilizzarsi.
Presenza dell’argomento
Quando i dati rilevanti dell’argomento sono disponibili, l’anteprima può anche visualizzare le informazioni di presenza dell’argomento di Discourse nella parte inferiore della modale.
In questo modo 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 dell’interfaccia di Discourse usano servizi di modale/menu condivisi, e quei servizi non sanno necessariamente che l’anteprima dell’argomento sta attualmente agendo come un contesto di navigazione annidato.
Il componente ha quindi una gestione aggiuntiva intorno a:
modal.close()- menu Float Kit
- ripristino del focus
- il compositore
- controlli da tastiera del lightbox
- blocchi di scorrimento del corpo
Ad esempio, se un menu tenta internamente di chiamare il metodo di chiusura della modale globale, ciò non dovrebbe chiudere accidentalmente l’intera anteprima dell’argomento.
Similmente, quando il compositore è aperto, il focus deve rimanere all’interno del compositore invece di essere richiamato nel contesto di focus dell’anteprima.
Configurazione
Il componente espone attualmente le seguenti impostazioni:
| Impostazione | Predefinito | Descrizione |
|---|---|---|
trigger_style |
row |
Rende l’intera riga cliccabile o usa un pulsante esplicito |
plugin_outlet |
topic-list-after-title |
Outlet usato dal trigger del pulsante |
enable_prefetch |
true |
Abilita/disabilita il pre-fetching di sfondo degli argomenti |
max_concurrent_prefetches |
2 |
Numero massimo di richieste di pre-fetch simultanee |
prefetch_debounce_ms |
400 |
Ritardo prima di iniziare un pre-fetch |
prefetch_root_margin_px |
50 |
Inizia il pre-fetching a questo numero di pixel prima che la riga entri nel viewport |
max_prefetches_per_minute |
15 |
Numero massimo di richieste speculative al minuto |
I controlli di pre-fetching sono intenzionalmente configurabili perché comunità diverse possono avere modelli di traffico e caratteristiche di hosting/rete molto diverse.
Uno degli obiettivi di progettazione principali: non rompere il Discourse normale
Ho cercato di mantenere il componente il più vicino possibile all’architettura esistente di Discourse.
Non implementa il proprio renderer di post, il proprio compositore, il proprio modello di argomento o il proprio 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 normale argomento Discourse mentre viene effettivamente visualizzato all’interno di un altro contesto di interfaccia utente?
Ciò ha richiesto di gestire i confini tra i servizi globali di Discourse e l’anteprima locale.





