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.
← Torna al Blog