diff --git a/conversations.php b/conversations.php index 6a461da..cc6a8c0 100644 --- a/conversations.php +++ b/conversations.php @@ -978,9 +978,9 @@ const icon = type === 'success' ? '✅' : (type === 'danger' ? '⚠️' : (type === 'warning' ? '⚠' : 'ℹ️')); toast.innerHTML = `${icon}
${message}
`; container.appendChild(toast); - // Ensure a sensible minimum duration (3s) so very short toasts don't disappear instantly - const _minToastDuration = 3000; - let _dur = (type === 'success') ? 4000 : 8000; + // Ensure a sensible minimum duration (1s) so very short toasts don't disappear instantly + const _minToastDuration = 1000; + let _dur = (type === 'success') ? 1000 : 1000; _dur = Math.max(_dur, _minToastDuration); setTimeout(() => { if (toast.parentNode) toast.remove(); }, _dur); } catch (e) { @@ -1240,9 +1240,10 @@ } catch (e) { console.warn('showNotificationToast -> showSystemNotificationInChat failed', e); } // Auto remove (shorter for compact notifications) with minimum enforced - const _minToastDuration = 3000; - let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000); - if (!isFinite(timeout) || timeout <= 0) timeout = (notification.cool ? 15000 : 8000); + // Reducir mínimo y valores por defecto a 1 segundo (1000 ms) + const _minToastDuration = 1000; + let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 1000 : 1000); + if (!isFinite(timeout) || timeout <= 0) timeout = (notification.cool ? 1000 : 1000); timeout = Math.max(timeout, _minToastDuration); setTimeout(() => { // on automatic timeout, attempt ack and schedule retry if needed @@ -1721,6 +1722,77 @@ this.sendMessage(); } }); + + // Permitir pegar imágenes desde el portapapeles: convierte el contenido pegado en File y muestra el preview + document.getElementById('message-input').addEventListener('paste', (e) => { + try { + const cd = (e.clipboardData || window.clipboardData); + if (!cd) return; + + // Helper: convertir base64 a Blob + const b64ToBlob = (b64, mime) => { + const bytes = atob(b64); + const len = bytes.length; + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) arr[i] = bytes.charCodeAt(i); + return new Blob([arr], { type: mime }); + }; + + // 1) Si hay archivos directos en clipboard (Chrome/Edge) + if (cd.files && cd.files.length) { + for (const file of cd.files) { + if (file && file.type && file.type.startsWith('image/')) { + e.preventDefault && e.preventDefault(); + const max = this.getMaxFileSize(file.type); + if (file.size > max) { + alert('El archivo es demasiado grande. Máximo: ' + (max / 1024 / 1024).toFixed(0) + 'MB'); + return; + } + this.showMediaPreview(file); + return; + } + } + } + + // 2) Items (Safari / otros) - buscar imagen o HTML con data URI + if (cd.items && cd.items.length) { + for (const item of cd.items) { + try { + if (item.kind === 'file' && item.type && item.type.startsWith('image/')) { + e.preventDefault && e.preventDefault(); + const file = item.getAsFile(); + if (file) { + const max = this.getMaxFileSize(file.type); + if (file.size > max) { alert('El archivo es demasiado grande. Máximo: ' + (max / 1024 / 1024).toFixed(0) + 'MB'); return; } + this.showMediaPreview(file); + return; + } + } else if (item.kind === 'string' && (item.type === 'text/html' || item.type === 'text/plain')) { + // Extraer data URI desde HTML si existe + item.getAsString((s) => { + const m = s && s.match ? s.match(/src=["']data:(image\/[^;]+);base64,([^"']+)["']/i) : null; + if (m) { + try { + const mime = m[1]; + const b64 = m[2]; + const blob = b64ToBlob(b64, mime); + const ext = (mime.split('/')[1] || 'png').split('+')[0]; + const file = new File([blob], 'pasted_image.' + ext, { type: mime }); + const max = this.getMaxFileSize(file.type); + if (file.size > max) { alert('El archivo es demasiado grande. Máximo: ' + (max / 1024 / 1024).toFixed(0) + 'MB'); return; } + this.showMediaPreview(file); + } catch (err) { console.warn('paste image convert failed', err); } + } + }); + // no hacer return inmediato: seguir buscando otros items + } + } catch (inner) { console.warn('clipboard item parse failed', inner); } + } + } + } catch (err) { + console.warn('paste handler failed', err); + } + }); // Adjuntar archivos document.getElementById('attach-btn').addEventListener('click', () => { @@ -3837,21 +3909,28 @@ method: 'POST', body: formData }); - - const uploadResult = await uploadResponse.json(); - + const uploadContentType = uploadResponse.headers.get('content-type') || ''; + let uploadResult; + if (uploadContentType.indexOf('application/json') !== -1) { + uploadResult = await uploadResponse.json(); + } else { + const text = await uploadResponse.text(); + console.error('upload_media returned non-JSON response:\n', text); + throw new Error('Respuesta inválida de upload_media.php. Ver consola para más detalles.'); + } + if (!uploadResult.success) { throw new Error(uploadResult.error || 'Error subiendo archivo'); } - + // 2. Enviar mensaje con el archivo const user = this.conversations.find(c => c.user_id === this.currentUserId); const phone = user ? user.phone_number : null; - + if (!phone) { throw new Error('No se encontró el número de teléfono'); } - + const sendResponse = await fetch('api/send_media_message.php', { method: 'POST', headers: { @@ -3865,9 +3944,54 @@ filename: this.selectedFile.name }) }); - - const sendResult = await sendResponse.json(); - + + const sendContentType = sendResponse.headers.get('content-type') || ''; + let sendResult = null; + + if (sendContentType.indexOf('application/json') !== -1) { + try { + sendResult = await sendResponse.json(); + } catch (err) { + // Content-Type claims JSON but parsing failed: attempt to extract JSON from body + const text = await sendResponse.text(); + console.warn('send_media_message: Content-Type JSON but parse failed. Response body will be inspected for JSON.'); + console.warn(text); + const m = text.match(/(\{[\s\S]*\})/); + if (m) { + try { sendResult = JSON.parse(m[1]); console.warn('send_media_message: extracted JSON from response.'); } catch (e) { console.warn('send_media_message: extracted JSON parse failed', e); } + } + if (!sendResult) { + // If HTTP status is OK, be forgiving: treat as success but log for investigation + if (sendResponse.ok) { + console.warn('send_media_message: non-parseable JSON returned but HTTP ok; treating as success.'); + sendResult = { success: true }; + } else { + throw new Error('Respuesta JSON inválida de send_media_message.php. Ver consola para más detalles.'); + } + } + } + } else { + // Non-JSON content-type: try to find embedded JSON; if HTTP 200, be forgiving + const text = await sendResponse.text(); + console.warn('send_media_message returned non-JSON response:'); + console.warn(text.slice(0, 2000)); + const m = text.match(/(\{[\s\S]*\})/); + if (m) { + try { sendResult = JSON.parse(m[1]); console.warn('send_media_message: extracted JSON from non-JSON response.'); } catch (e) { console.warn('send_media_message: failed to parse extracted JSON', e); } + } + + if (!sendResult) { + if (sendResponse.ok) { + // Message was likely sent successfully despite odd response — avoid showing false error to user + console.warn('send_media_message: non-JSON response but HTTP OK. Treating as success.'); + sendResult = { success: true }; + } else { + console.error('send_media_message returned non-JSON response and HTTP not OK.'); + throw new Error('Respuesta inválida de send_media_message.php. Revisa logs del servidor (send_media_message_debug.log y el servidor PHP) para más detalles.'); + } + } + } + if (!sendResult.success) { throw new Error(sendResult.error || 'Error enviando mensaje'); } diff --git a/index.php b/index.php index f17f908..a43168b 100644 --- a/index.php +++ b/index.php @@ -1078,6 +1078,8 @@ try { actions.appendChild(btn); toast.appendChild(actions); this.toastsContainer.appendChild(toast); + // auto-eliminar toast de sesión expirada después de 1s + setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000); }, showToast(notification) { const toast = document.createElement('div'); @@ -1164,6 +1166,8 @@ try { } n.close(); }; + // cerrar la notificación nativa automáticamente después de 1s + setTimeout(() => { try { n.close(); } catch(e) {} }, 1000); } catch (e) { console.warn('native notification failed', e); } } @@ -1182,7 +1186,7 @@ try { setTimeout(() => { o.stop(); ctx.close(); }, 180); } catch (e) { console.warn('Notification sound failed', e); } - setTimeout(() => { if (toast.parentNode) toast.remove(); }, 18000); + setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000); } };