Skip to content

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:

typescript
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;
}

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:

  1. Registrare l'oggetto messages sul window globale
  2. Fornire le funzioni che l'iframe può chiamare per gestire il contatore dei messaggi
  3. Usare lo store Zustand per aggiornare lo stato (che persiste tra le pagine)
  4. Mostrare automaticamente il modal dei messaggi nell'iframe quando richiesto
  5. Pulire l'oggetto messages quando il componente viene smontato

Type Declaration

Il file app/types/window.d.ts estende l'interfaccia Window per includere l'oggetto messages:

typescript
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;
};

Accesso allo Stato

Lo stato dei messaggi è accessibile tramite lo store Zustand:

typescript
// Nel componente
const counter = useMessagesStore((state) => state.counter);
const hasUnread = useMessagesStore((state) => state.hasUnread);

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:

typescript
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>
    );
}

Nell'Iframe (PHP/JavaScript)

L'iframe può chiamare le funzioni messages tramite window.parent:

1. Impostare il numero di messaggi

javascript
// 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 modal

2. Decrementare il contatore (messaggio letto)

javascript
window.parent.messages.readMessage();

3. Incrementare il contatore (nuovo messaggio)

javascript
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:

  1. Cerca l'elemento iframe con id main-iframe
  2. Controlla se iframe.contentWindow.messages è definito
  3. Se non è definito: aggiunge un event listener per mostrare il modal al caricamento
  4. Se è definito: chiama immediatamente iframe.contentWindow.messages.showModal()

Questo comportamento replica la logica del componente legacy Messages.js.

Esempio Completo

PHP nell'iframe

php
<!-- 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>

Migrazione dal Legacy

Il componente legacy Messages.js utilizzava:

javascript
// OLD (MobX class component)
this.setState({ counter: 5, evid: true });

// NEW (Hook)
window.parent.messages.setMessages('5-1');

Differenze principali:

LegacyNuovo Sistema
Component state localeHook con window.messages
MobX inject/observerReact hooks (useState/useEffect)
evid booleanhasUnread boolean
Chiamate dirette al componentChiamate 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 ghost del Button
  • Solo icona Bell
  • Tooltip: "Messaggi"

Messaggi non letti (counter > 0)

  • Variante destructive del Button (sfondo rosso)
  • Icona Bell con Badge numerico
  • Badge: variante secondary, size sm
  • Tooltip: "5 messaggi" (o "1 messaggio" se counter = 1)

Codice JSX

tsx
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>
    );
}

Test

Test Unitari

Il file tests/useTopbarMessages.test.ts contiene i test per verificare:

  • ✅ Registrazione dell'oggetto messages sul 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:

  1. Imposta 5 messaggi (con modal) - chiama setMessages('5-0')
  2. Imposta 3 messaggi (senza modal) - chiama setMessages('3-1')
  3. Leggi messaggio (-1) - chiama readMessage()
  4. Aggiungi messaggio (+1) - chiama addMessage()
  5. Reset messaggi (0) - chiama setMessages('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/messages a /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:

typescript
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]);
}

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"

Utilizzo

L'hook viene chiamato automaticamente nell'AuthenticatedLayout:

typescript
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);

    // ...
}

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 messages viene rimosso quando il componente viene smontato per evitare memory leaks
  • Gli errori nell'accesso al contentWindow dell'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'iframe

Questi log sono utili per il debugging durante lo sviluppo.

Documentazione Elerama Frontend