Comunicazione Iframe - Messages
Panoramica
Questo documento descrive il sistema di comunicazione tra l'iframe presente nella pagina 'r' e il parent window per gestire i messaggi non letti.
Architettura
Store: useMessagesStore (Zustand)
Lo store Zustand gestisce lo stato dei messaggi in modo persistente:
interface MessagesStore {
counter: number; // Numero di messaggi non letti
hasUnread: boolean; // true se ci sono messaggi non letti
setCounter: (counter: number) => void;
incrementCounter: () => void;
decrementCounter: () => void;
}2
3
4
5
6
7
Vantaggi dello store:
- ✅ Stato persiste durante la navigazione tra le pagine
- ✅ Non si resetta quando il componente si rimonta
- ✅ Accessibile da qualsiasi componente
Hook: useTopbarMessages
L'hook useTopbarMessages è responsabile di:
- Registrare l'oggetto
messagessulwindowglobale - Fornire le funzioni che l'iframe può chiamare per gestire il contatore dei messaggi
- Usare lo store Zustand per aggiornare lo stato (che persiste tra le pagine)
- Mostrare automaticamente il modal dei messaggi nell'iframe quando richiesto
- Pulire l'oggetto
messagesquando il componente viene smontato
Type Declaration
Il file app/types/window.d.ts estende l'interfaccia Window per includere l'oggetto messages:
messages?: {
/**
* Decrementa il contatore dei messaggi non letti
*/
readMessage: () => void;
/**
* Incrementa il contatore dei messaggi non letti
*/
addMessage: () => void;
/**
* Imposta il contatore dei messaggi dal formato "count-flag"
* @param n_msg - Stringa nel formato "count-flag" (es. "5-0")
*/
setMessages: (n_msg: string) => void;
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
Accesso allo Stato
Lo stato dei messaggi è accessibile tramite lo store Zustand:
// Nel componente
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);2
3
Importante: L'hook useTopbarMessages() deve essere chiamato una volta (tipicamente nell'AuthenticatedLayout) per registrare le funzioni su window.messages, ma lo stato viene letto direttamente dallo store per evitare che si resetti durante la navigazione.
Utilizzo
Nel Parent Window (React App)
L'hook useTopbarMessages viene utilizzato nell'AuthenticatedLayout:
export function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) {
// ... altri hook ...
// Registra l'oggetto messages sul window (chiamato una volta)
useTopbarMessages();
// Leggi lo stato dallo store (persiste durante la navigazione)
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);
// Usa lo stato per mostrare l'icona dei messaggi
return (
<div>
<button className={hasUnread ? "bg-red-600" : ""}>
✉️
{counter > 0 && (
<span>{counter}</span>
)}
</button>
{/* ... resto del layout ... */}
</div>
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Nell'Iframe (PHP/JavaScript)
L'iframe può chiamare le funzioni messages tramite window.parent:
1. Impostare il numero di messaggi
// Formato: "count-flag"
// count: numero di messaggi non letti
// flag: se < 1, mostra automaticamente il modal dei messaggi
window.parent.messages.setMessages('5-0'); // 5 messaggi, mostra modal
window.parent.messages.setMessages('3-1'); // 3 messaggi, NON mostrare modal2
3
4
5
2. Decrementare il contatore (messaggio letto)
window.parent.messages.readMessage();3. Incrementare il contatore (nuovo messaggio)
window.parent.messages.addMessage();Comportamento del Modal
Quando viene chiamato setMessages() con un flag < 1, l'hook tenta di mostrare automaticamente il modal dei messaggi nell'iframe:
- Cerca l'elemento iframe con id
main-iframe - Controlla se
iframe.contentWindow.messagesè definito - Se non è definito: aggiunge un event listener per mostrare il modal al caricamento
- Se è definito: chiama immediatamente
iframe.contentWindow.messages.showModal()
Questo comportamento replica la logica del componente legacy Messages.js.
Esempio Completo
PHP nell'iframe
<!-- Imposta il numero di messaggi dall'API -->
<script>
const messageCount = <?= $unread_messages ?>;
const showModal = <?= $show_modal_on_load ? 0 : 1 ?>;
window.parent.messages.setMessages(messageCount + '-' + showModal);
</script>
<!-- Quando un messaggio viene letto -->
<script>
function markAsRead(messageId) {
// ... logica per segnare come letto ...
window.parent.messages.readMessage();
}
</script>
<!-- Quando arriva un nuovo messaggio (es. via WebSocket) -->
<script>
socket.on('new_message', function() {
window.parent.messages.addMessage();
});
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Migrazione dal Legacy
Il componente legacy Messages.js utilizzava:
// OLD (MobX class component)
this.setState({ counter: 5, evid: true });
// NEW (Hook)
window.parent.messages.setMessages('5-1');2
3
4
5
Differenze principali:
| Legacy | Nuovo Sistema |
|---|---|
| Component state locale | Hook con window.messages |
| MobX inject/observer | React hooks (useState/useEffect) |
evid boolean | hasUnread boolean |
| Chiamate dirette al component | Chiamate via window.parent.messages |
Stile dell'Icona
L'icona dei messaggi cambia aspetto in base allo stato usando i componenti da @elerama/ui e @elerama/icons:
Nessun messaggio (counter = 0)
- Variante
ghostdel Button - Solo icona Bell
- Tooltip: "Messaggi"
Messaggi non letti (counter > 0)
- Variante
destructivedel Button (sfondo rosso) - Icona Bell con Badge numerico
- Badge: variante
secondary, sizesm - Tooltip: "5 messaggi" (o "1 messaggio" se counter = 1)
Codice JSX
import { useMessagesStore } from "@/store/messages/messages.store";
import { Bell } from "@elerama/icons";
import { Badge, Button } from "@elerama/ui";
export function Messages() {
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);
return (
<Button
variant={hasUnread ? "destructive" : "ghost"}
size="icon"
onClick={() => navigate("/r/messages")}
className="relative"
tooltip={
counter === 0
? "Messaggi"
: counter === 1
? "1 messaggio"
: `${counter} messaggi`
}
>
<Bell />
{counter > 0 && (
<Badge
variant="secondary"
size="sm"
className="absolute -top-2 -right-2"
>
{counter}
</Badge>
)}
</Button>
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Test
Test Unitari
Il file tests/useTopbarMessages.test.ts contiene i test per verificare:
- ✅ Registrazione dell'oggetto
messagessul window - ✅ Rimozione dell'oggetto quando l'hook viene smontato
- ✅ Stato iniziale (counter = 0, hasUnread = false)
- ✅
readMessage()decrementa il contatore - ✅
addMessage()incrementa il contatore - ✅
setMessages()imposta il contatore dal formato "count-flag" - ✅ Modal viene mostrato quando flag < 1
- ✅ Scenario completo di operazioni
- ✅ Stabilità delle funzioni tra i render
Test Manuali
Usa la pagina /test-iframe per testare manualmente:
Imposta 5 messaggi (con modal)- chiamasetMessages('5-0')Imposta 3 messaggi (senza modal)- chiamasetMessages('3-1')Leggi messaggio (-1)- chiamareadMessage()Aggiungi messaggio (+1)- chiamaaddMessage()Reset messaggi (0)- chiamasetMessages('0-1')
Persistenza dello Stato
⚠️ Importante: Lo stato dei messaggi è gestito da Zustand e persiste durante la navigazione.
Questo significa che:
- ✅ Se hai 5 messaggi non letti e navighi da
/r/messagesa/welcome, il contatore rimane a 5 - ✅ Il badge rosso con il numero rimane visibile su tutte le pagine
- ✅ Lo stato si aggiorna solo quando l'iframe chiama le funzioni
messages.* - ⚠️ Lo stato non persiste tra refresh del browser (non usa localStorage)
Se in futuro si volesse persistere anche tra i refresh, si può usare il middleware persist di Zustand come per l'autenticazione.
Sincronizzazione Cross-Tab
Lo stato dei messaggi è sincronizzato automaticamente tra tutte le tab aperte.
Come Funziona
Quando una tab aggiorna il contatore dei messaggi (es. l'iframe chiama setMessages("5-0")), tutte le altre tab ricevono automaticamente l'aggiornamento tramite BroadcastChannel.
Hook: useMessagesSyncListener
L'hook useMessagesSyncListener gestisce la sincronizzazione:
export function useMessagesSyncListener() {
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);
const { postMessage } = useBroadcastChannel<MessagesSyncMessage>(
"messages-sync",
(message) => {
if (message.type === "messages:update") {
// Aggiorna lo store locale con i dati ricevuti
useMessagesStore.setState({
counter: message.counter,
hasUnread: message.hasUnread,
});
}
}
);
// Quando il contatore cambia, notifica le altre tab
useEffect(() => {
postMessage({
type: "messages:update",
counter,
hasUnread,
});
}, [counter, hasUnread, postMessage]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Esempio di Sincronizzazione
📱 Tab A: L'iframe chiama window.parent.messages.setMessages("5-0")
↓
🔄 Tab A: useMessagesStore aggiorna counter = 5
↓
📡 Tab A: useMessagesSyncListener invia messaggio via BroadcastChannel
↓
📨 Tab B: Riceve il messaggio
↓
🔄 Tab B: useMessagesStore aggiorna counter = 5
↓
✅ Entrambe le tab mostrano il badge con "5"2
3
4
5
6
7
8
9
10
11
Utilizzo
L'hook viene chiamato automaticamente nell'AuthenticatedLayout:
export function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) {
// Registra l'oggetto messages sul window
useTopbarMessages();
// Sincronizza lo stato tra le tab
useMessagesSyncListener();
// Leggi lo stato dallo store
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);
// ...
}2
3
4
5
6
7
8
9
10
11
12
13
Test
I test per la sincronizzazione sono in tests/useMessagesSyncListener.test.ts:
- ✅ Invio messaggio quando il contatore cambia
- ✅ Aggiornamento dello store quando riceve un messaggio
- ✅ Sincronizzazione incrementCounter/decrementCounter
- ✅ Sincronizzazione reset a 0
- ✅ Nessun memory leak dopo unmount
Risultato: 7/7 test passati ✅
Note sulla Sicurezza
- La comunicazione avviene tramite
window.parent, quindi funziona solo se l'iframe è sullo stesso dominio o configurato con CORS - Il contatore non può andare sotto 0 (protezione con
Math.max(0, ...)) - L'oggetto
messagesviene rimosso quando il componente viene smontato per evitare memory leaks - Gli errori nell'accesso al
contentWindowdell'iframe vengono gestiti con try/catch
Logging
L'hook logga automaticamente tutte le operazioni nella console:
[useTopbarMessages] Hook montato, registrazione messages...
[useTopbarMessages] ✅ Oggetto messages registrato sul window
[useTopbarMessages] 🎯 setMessages chiamata dall'iframe!
[useTopbarMessages] 📦 n_msg: 5-0
[useTopbarMessages] 📊 Parsed: { count: 5, flag: 0 }
[useTopbarMessages] ✅ Messaggi impostati: { count: 5, hasUnread: true }
[useTopbarMessages] 🔔 Flag < 1: tentativo di mostrare modal nell'iframe2
3
4
5
6
7
Questi log sono utili per il debugging durante lo sviluppo.