Il contesto

La chat fa già parte di un progetto più grande: un pannello di controllo per agenti commerciali con ordini, turni e firma OTP. La parte chat era già funzionante con testo, immagini (upload via ImgBB) e messaggi vocali (Cloudinary con resource_type=video). Mancava solo l'invio di documenti generici.

Il piano era seguire lo stesso pattern già usato per i vocali: upload su Cloudinary, salvataggio URL su Firebase Realtime Database, render della bubble nel client.

ℹ️

Stack di partenza: Firebase Realtime DB + Cloudinary + Netlify. Nessun backend dedicato — tutto client-side con Cloudinary unsigned upload.

I 5 step dell'implementazione

  • 1
    CSS — bubble documento

    Stili per .chat-doc-bubble: sfondo blu traslucido, icona tipo file, nome troncato, tasto download.

  • 2
    Input file + pulsante nel menu +

    <input id="chatDocInput"> con accept per PDF, Word, Excel, PPT, TXT, CSV, ZIP e RAR. Voce "📎 Documento" nel menu contestuale.

  • 3
    Upload su Cloudinary con resource_type=raw

    I file binari non vanno sotto imagevideo — serve raw. Stesso preset unsigned già usato per i vocali.

  • 4
    Render bubble in _buildMsgEl()

    Icona dinamica per tipo (📄 PDF, 📊 Excel, 📝 Word…), nome file, dimensione in KB/MB, click per aprire in nuova scheda.

  • 5
    Aggiornamento preview lista chat

    Le anteprime DM e gruppi mostrano 📎 NomeFile come ultimo messaggio, coerente con il comportamento di immagini e vocali.

Il codice chiave

Upload su Cloudinary — resource_type: raw

Il punto più critico: Cloudinary non accetta file binari generici sotto il tipo image. Bisogna dichiarare resource_type=raw nell'URL di upload, usando lo stesso preset unsigned già configurato per i messaggi vocali.

javascript
async function chatHandleDocFile(file) {
  const fd = new FormData();
  fd.append('file', file);
  fd.append('upload_preset', 'Vocali'); // preset unsigned già esistente

  // resource_type=raw — obbligatorio per file non-media
  const res = await fetch(
    `https://api.cloudinary.com/v1_1/CLOUD_NAME/raw/upload`,
    { method: 'POST', body: fd }
  );
  const data = await res.json();
  return data.secure_url;
}

Scrittura su Firebase — type: 'document'

Il messaggio viene scritto su Firebase con un tipo distinto, così il client sa come renderizzarlo. Salvo anche fileName e fileSize per mostrarli nella bubble senza riscaricare il file.

javascript
async function _sendChatDoc(docUrl, fileName, fileSize) {
  const msgData = {
    type:     'document',
    docUrl,
    fileName,
    fileSize, // es. "245 KB"
    sender:   currentUser.uid,
    ts:       Date.now(),
  };
  // push in /messages/{chatId}/ — stesso pattern di testo e immagini
  await db.ref(`messages/${chatId}`).push(msgData);
}

Render della bubble — icona per tipo file

Nel metodo _buildMsgEl() aggiungo un branch per type === 'document'. L'icona cambia in base all'estensione, così l'utente capisce subito che tipo di file sta ricevendo.

javascript
function getDocIcon(fileName) {
  const ext = fileName.split('.').pop().toLowerCase();
  const icons = {
    pdf: '📄', doc: '📝', docx: '📝',
    xls: '📊', xlsx: '📊',
    ppt: '📑', pptx: '📑',
    zip: '🗜️', rar: '🗜️', '7z': '🗜️',
    txt: '📃', csv: '📃',
  };
  return icons[ext] ?? '📎';
}

// Nel branch di _buildMsgEl() per type === 'document':
el.innerHTML = `
  <div class="chat-doc-bubble" onclick="window.open('${msg.docUrl}','_blank')">
    <span class="chat-doc-icon">${getDocIcon(msg.fileName)}</span>
    <div class="chat-doc-info">
      <span class="chat-doc-name">${msg.fileName}</span>
      <span class="chat-doc-size">${msg.fileSize}</span>
    </div>
    <span class="chat-doc-dl">↓</span>
  </div>
`;

I problemi che ho incontrato

1. Sintassi JS: parentesi di troppo nel ternario

Dopo aver modificato _buildMsgEl() aggiungendo il branch documento a quello già esistente per immagini, il file non passava la validazione JS. Il problema era un ternario annidato con una parentesi chiusa di troppo:

javascript — prima (errore)
type === 'image' ? renderImg() :
type === 'document' ? renderDoc()) : // ← ) di troppo
renderText()
javascript — dopo (corretto)
type === 'image' ? renderImg() :
type === 'document' ? renderDoc() :
renderText()

2. str_replace fallisce su stringhe identiche

Quando si usa uno strumento di replace automatico su un file grande, se due blocchi di codice sono identici (es. due branch consecutivi quasi uguali) il replace non riesce a distinguerli e fallisce silenziosamente. La soluzione: usare Python con una sostituzione per indice di linea invece di fare match testuale.

⚠️

Attenzione con i file grandi: se usi str_replace su file JS sopra i 1000+ righe con pattern ripetuti, la sostituzione può colpire la riga sbagliata. Aggiungi sempre abbastanza contesto circostante nel pattern di ricerca.

3. Il preset Cloudinary deve supportare resource_type raw

Il preset Vocali era stato creato originariamente solo per audio. Su Cloudinary, un preset unsigned deve avere resource_type impostato su auto (oppure creare un preset dedicato per raw) — altrimenti l'upload di un PDF restituisce un errore 400. Ho risolto modificando il preset esistente da audio ad auto nel pannello Cloudinary.

Il risultato finale

Con questa implementazione la chat ora supporta quattro tipi di messaggio: testo, immagine, vocale e documento. La struttura dei dati su Firebase è omogenea — ogni messaggio ha sempre type, sender e ts come campi base, con i campi specifici del tipo aggiunti sopra.

  • Upload: Cloudinary CDN con resource_type=raw
  • Tipi supportati: PDF, DOC/X, XLS/X, PPT/X, TXT, CSV, ZIP, RAR, 7z
  • Feedback visivo: icona emoji per tipo, nome file troncato, dimensione, freccia download
  • Preview lista: 📎 NomeFile nell'ultima riga della chat in lista
  • Push notification: inclusa, con testo "Ha inviato un documento"
💡

Riutilizzabilità: l'intero pattern (upload CDN → Firebase → render bubble) è identico per tutti i tipi di media. Aggiungere un quinto tipo — es. video — richiederebbe solo un nuovo branch nel render e un resource_type appropriato su Cloudinary.


Takeaway

Implementare l'upload di documenti in una chat Firebase non è difficile, ma ci sono tre cose da tenere a mente fin dall'inizio:

  1. Cloudinary resource_type=raw è obbligatorio per file non-media. Non funziona con image o video.
  2. Salva fileName e fileSize su Firebase al momento dell'upload — non puoi recuperarli dall'URL Cloudinary in seguito senza una chiamata extra.
  3. Testa la validazione JS dopo ogni modifica a file grandi con ternari annidati. Un parentesi di troppo non genera errore di build visibile, ma rompe il comportamento runtime.

Domande frequenti

Perché Cloudinary richiede resource_type=raw per i documenti invece di image o video?

Cloudinary tratta i file binari generici (PDF, Word, Excel, ZIP) come un tipo a parte rispetto a immagini e video. Senza dichiarare resource_type=raw nell'URL di upload, il file non viene accettato correttamente — serve anche un preset unsigned con resource_type impostato su auto lato pannello Cloudinary.

Perché conviene salvare fileName e fileSize su Firebase al momento dell'upload invece di recuperarli dopo?

L'URL Cloudinary da solo non porta queste informazioni: recuperarle in un secondo momento richiederebbe una chiamata API extra ogni volta che si mostra il messaggio. Salvandole subito nello stesso oggetto del messaggio, la bubble del documento si renderizza istantaneamente senza richieste aggiuntive.

Perché un ternario annidato con una parentesi di troppo non genera un errore di build visibile?

Un errore di sintassi in un ternario annidato può comunque produrre codice JavaScript sintatticamente valido ma con una logica diversa da quella intesa, quindi non blocca sempre la build — rompe però il comportamento a runtime. Per questo è importante testare manualmente la validazione JS dopo ogni modifica a branch condizionali simili, specialmente su file di grandi dimensioni con pattern ripetuti.

← Torna al Blog