This commit is contained in:
Lizandro Guarnizo
2026-01-26 23:12:18 -05:00
parent 87d253053c
commit 437a99dbcf
2 changed files with 144 additions and 16 deletions
+139 -15
View File
@@ -978,9 +978,9 @@
const icon = type === 'success' ? '✅' : (type === 'danger' ? '⚠️' : (type === 'warning' ? '⚠' : '️'));
toast.innerHTML = `<span class="nt-icon">${icon}</span><div style="flex:1; font-size:13px">${message}</div>`;
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');
}
+5 -1
View File
@@ -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);
}
};