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">conacceptper PDF, Word, Excel, PPT, TXT, CSV, ZIP e RAR. Voce "📎 Documento" nel menu contestuale. -
3
Upload su Cloudinary con
resource_type=rawI file binari non vanno sotto
imagenévideo— serveraw. 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
📎 NomeFilecome 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.
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.
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.
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:
type === 'image' ? renderImg() : type === 'document' ? renderDoc()) : // ← ) di troppo renderText()
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:
📎 NomeFilenell'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:
- Cloudinary resource_type=raw è obbligatorio per file non-media. Non funziona con
imageovideo. - Salva fileName e fileSize su Firebase al momento dell'upload — non puoi recuperarli dall'URL Cloudinary in seguito senza una chiamata extra.
- 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.
The context
The chat is part of a larger project: a control panel for sales agents
with orders, shifts, and OTP signing. The chat was already working with text, images (via ImgBB upload),
and voice messages (Cloudinary with resource_type=video). The only missing piece was generic document sending.
The plan was to follow the same pattern already used for voice messages: upload to Cloudinary, save the URL to Firebase Realtime Database, render the bubble on the client.
Starting stack: Firebase Realtime DB + Cloudinary + Netlify. No dedicated backend — everything client-side with Cloudinary unsigned upload.
The 5 implementation steps
-
1
CSS — document bubble
Styles for
.chat-doc-bubble: translucent blue background, file-type icon, truncated filename, download button. -
2
File input + button in the + menu
<input id="chatDocInput">withacceptfor PDF, Word, Excel, PPT, TXT, CSV, ZIP, and RAR. A "📎 Document" option in the contextual menu. -
3
Upload to Cloudinary with
resource_type=rawBinary files can't go under
imageorvideo— you needraw. Same unsigned preset already used for voice messages. -
4
Bubble render in
_buildMsgEl()Dynamic icon by type (
📄PDF,📊Excel,📝Word…), filename, size in KB/MB, click to open in a new tab. -
5
Chat list preview update
DM and group previews show
📎 FileNameas the last message, consistent with images and voice messages.
The key code
Upload to Cloudinary — resource_type: raw
The critical bit: Cloudinary won't accept generic binary files under the image type.
You must declare resource_type=raw in the upload URL,
using the same unsigned preset already configured for voice messages.
async function chatHandleDocFile(file) { const fd = new FormData(); fd.append('file', file); fd.append('upload_preset', 'Vocali'); // existing unsigned preset // resource_type=raw — required for non-media files 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; }
Write to Firebase — type: 'document'
The message is written to Firebase with a distinct type so the client knows how to render it.
I also save fileName and fileSize to display them in the bubble without re-fetching the file.
async function _sendChatDoc(docUrl, fileName, fileSize) { const msgData = { type: 'document', docUrl, fileName, fileSize, // e.g. "245 KB" sender: currentUser.uid, ts: Date.now(), }; // push to /messages/{chatId}/ — same pattern as text and images await db.ref(`messages/${chatId}`).push(msgData); }
Bubble render — icon by file type
In _buildMsgEl() I add a branch for type === 'document'.
The icon changes based on the extension, so the user can immediately see what kind of file they're receiving.
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] ?? '📎'; }
Problems I ran into
1. JS syntax: extra bracket in the ternary
After modifying _buildMsgEl() to add the document branch next to the existing image one,
the file wouldn't pass JS validation. The culprit was a nested ternary with one closing bracket too many:
type === 'image' ? renderImg() : type === 'document' ? renderDoc()) : // ← extra ) renderText()
type === 'image' ? renderImg() : type === 'document' ? renderDoc() : renderText()
2. str_replace fails on identical strings
When using an auto-replace tool on a large file, if two code blocks are nearly identical (e.g. two almost-identical consecutive branches), the replace can't tell them apart and fails silently. The fix: use Python with line-index-based substitution instead of text matching.
Watch out with large files: if you use str_replace on JS files with 1000+ lines and repeated patterns, the substitution may hit the wrong line. Always include enough surrounding context in the search pattern.
3. The Cloudinary preset must support resource_type raw
The Vocali preset had originally been created for audio only.
On Cloudinary, an unsigned preset needs to have resource_type set to auto
(or you can create a dedicated raw preset) — otherwise uploading a PDF returns a 400 error.
I fixed it by switching the existing preset from audio to auto in the Cloudinary dashboard.
The final result
With this implementation the chat now supports four message types:
text, image, voice, and document. The Firebase data structure is uniform —
every message always has type, sender, and ts as base fields,
with type-specific fields layered on top.
- Upload: Cloudinary CDN with
resource_type=raw - Supported types: PDF, DOC/X, XLS/X, PPT/X, TXT, CSV, ZIP, RAR, 7z
- Visual feedback: emoji icon by type, truncated filename, size, download arrow
- List preview:
📎 FileNamein the last-message row of the chat list - Push notification: included, with text "Sent a document"
Reusability: the entire pattern (CDN upload → Firebase → bubble render) is identical for all media types. Adding a fifth type — e.g. video — would only require a new branch in the render and the appropriate resource_type on Cloudinary.
Takeaway
Adding document upload to a Firebase chat isn't hard, but three things are worth keeping in mind from the start:
- Cloudinary resource_type=raw is mandatory for non-media files. It won't work under
imageorvideo. - Save fileName and fileSize to Firebase at upload time — you can't retrieve them from the Cloudinary URL later without an extra API call.
- Test JS validation after every change to large files with nested ternaries. An extra bracket doesn't throw a visible build error, but it silently breaks runtime behavior.