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