Update conversations.php

This commit is contained in:
Lizandro Guarnizo
2026-01-24 01:27:47 -05:00
parent 71ae1cc9a4
commit 8a5137374c
+198 -82
View File
@@ -798,7 +798,7 @@
<div class="chat-input" style="position:relative;">
<input type="file" id="file-input" accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx" style="display: none;">
<button class="btn" id="attach-btn" title="Adjuntar">
<i class="fas fa-paperclip" aria-hidden="true"></i>
📎
</button>
<div id="attach-menu" class="attach-menu" style="display:none;">
<button class="attach-option" data-action="file">Enviar archivo</button>
@@ -912,7 +912,13 @@
this.conversationsLimit = 50;
this.hasMoreConversations = true;
this.loadingConversations = false;
// Track whether the user is actively scrolling/reading to avoid forcing scroll-to-bottom
this._userScrolling = false;
this._userScrollTimer = null;
// Track last known messages count to show "Nuevos mensajes" indicator when new messages arrive while user is reading
this._lastRenderMessageCount = 0;
this.init();
}
@@ -1312,10 +1318,16 @@
if (micBtn) {
micBtn.addEventListener('click', (ev) => {
ev.preventDefault();
// Ensure recording functions are available (they are initialized when opening a conversation
// but can be invoked earlier; initialize lazily if needed)
if (typeof self.startRecording !== 'function' && typeof self.initRecordingHandlers === 'function') {
try { self.initRecordingHandlers(); } catch(e) { console.warn('initRecordingHandlers failed', e); }
}
if (self._mediaRecorder && self._mediaRecorder.state === 'recording') {
self.stopRecording();
try { self.stopRecording(); } catch (e) { console.warn('stopRecording failed', e); }
} else {
self.startRecording();
try { self.startRecording(); } catch (e) { console.warn('startRecording failed', e); alert('No se pudo iniciar la grabación.'); }
}
});
}
@@ -1971,6 +1983,13 @@
if (chatContainer) {
if (!chatContainer._infiniteScrollAdded) {
chatContainer.addEventListener('scroll', async () => {
// Mark that the user is actively scrolling/reading so we don't yank the viewport to bottom
try {
this._userScrolling = true;
if (this._userScrollTimer) clearTimeout(this._userScrollTimer);
this._userScrollTimer = setTimeout(() => { this._userScrolling = false; this._userScrollTimer = null; }, 1500);
} catch(e) { /* ignore */ }
if (chatContainer.scrollTop <= 60 && this.hasMoreMessages && !this.loadingMessages && this.currentUserId == userId) {
await this.loadMessages(userId, false);
}
@@ -1987,81 +2006,15 @@
tplContainer.style.display = (document.getElementById('message-type').value === 'template') ? 'inline-block' : 'none';
}
// Recording support state
this._mediaRecorder = null;
this._recordingInterval = null;
this._recordingStart = null;
// Recording handlers
this.startRecording = async function() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._mediaRecorder = new MediaRecorder(stream);
const chunks = [];
this._mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
this._mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
let file = new File([blob], `record_${Date.now()}.webm`, { type: 'audio/webm' });
// Try convert if FFmpeg available
try {
if (window && window.ensureFFmpeg) {
await window.ensureFFmpeg();
const converted = await window.convertWebmToOgg(file);
file = converted;
}
} catch (convErr) { console.warn('conversion failed', convErr); }
// set as selected file and show preview
this.selectedFile = file;
this.showMediaPreview(file);
};
this._mediaRecorder.start();
this._recordingStart = Date.now();
document.getElementById('recording-indicator').style.display = 'block';
document.getElementById('mic-btn').classList.add('recording');
// Ensure UI shows mic and hides send while recording
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
const update = () => {
if (!this._recordingStart) return;
const elapsed = Math.floor((Date.now() - this._recordingStart) / 1000);
const m = Math.floor(elapsed/60); const s = elapsed % 60;
document.getElementById('recording-time').textContent = `${m}:${String(s).padStart(2,'0')}`;
};
update();
this._recordingInterval = setInterval(update, 1000);
} catch (err) {
console.error('startRecording error', err);
alert('No se pudo acceder al micrófono: ' + (err.message||err));
}
};
this.stopRecording = function() {
if (this._mediaRecorder && this._mediaRecorder.state !== 'inactive') {
this._mediaRecorder.stop();
}
if (this._recordingInterval) clearInterval(this._recordingInterval);
this._recordingInterval = null;
this._recordingStart = null;
document.getElementById('recording-indicator').style.display = 'none';
document.getElementById('mic-btn').classList.remove('recording');
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
};
this.cancelRecording = function() {
if (this._mediaRecorder && this._mediaRecorder.state !== 'inactive') {
this._mediaRecorder.stop();
}
// Initialize recording handlers so the mic works even before opening a conversation
if (typeof this.initRecordingHandlers === 'function') {
try { this.initRecordingHandlers(); } catch(e) { console.warn('initRecordingHandlers() failed', e); }
} else {
// fallback: define minimal state
this._mediaRecorder = null;
this.selectedFile = null;
if (this._recordingInterval) clearInterval(this._recordingInterval);
this._recordingInterval = null;
this._recordingStart = null;
document.getElementById('recording-indicator').style.display = 'none';
document.getElementById('mic-btn').classList.remove('recording');
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
};
}
}
async loadconversations(userId, showLoading = true) {
@@ -2271,7 +2224,7 @@
const prependEls = []; // elements to insert at top if we loaded older messages
const wasNearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 150;
const nearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 150;
for (let i = 0; i < this.conversations.length; i++) {
const msg = this.conversations[i];
@@ -2297,9 +2250,40 @@
let existingSrc = null;
if (existingMedia) existingSrc = existingMedia.getAttribute('src') || existingMedia.getAttribute('data-src');
const newMediaUrl = mediaPresent ? (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url) : null;
if (!(existingSrc && newMediaUrl && String(existingSrc).includes(newMediaUrl))) {
const newContent = mediaPresent ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (msg.content || '[Mensaje]')) : (msg.content || '[Mensaje vacío]');
body.innerHTML = newContent;
// If media url unchanged, keep existing element (preserve playback)
if (existingMedia && newMediaUrl && existingSrc && String(existingSrc).includes(newMediaUrl)) {
// nothing to change
} else if (existingMedia && newMediaUrl && (!existingSrc || !String(existingSrc).includes(newMediaUrl))) {
// Replace src but preserve playback state for audio/video
try {
const tag = existingMedia.tagName && existingMedia.tagName.toLowerCase();
if (tag === 'audio' || tag === 'video') {
const currentTime = existingMedia.currentTime || 0;
const wasPaused = existingMedia.paused;
existingMedia.src = newMediaUrl;
existingMedia.addEventListener('loadedmetadata', () => {
try {
if (typeof existingMedia.duration === 'number' && !isNaN(existingMedia.duration)) {
existingMedia.currentTime = Math.min(currentTime, existingMedia.duration || currentTime);
}
} catch (e) { /* ignore */ }
try { if (!wasPaused) existingMedia.play().catch(()=>{}); } catch(e){}
}, { once: true });
} else {
// image or other: just replace src
try { existingMedia.src = newMediaUrl; } catch(e) { body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]'); }
}
} catch (e) {
console.warn('preserve media update failed', e);
body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]');
}
} else if (!existingMedia && newMediaUrl) {
// No existing media element, render new media block without touching other parts
try { body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]'); } catch(e) { /* ignore */ }
} else if (existingMedia && !newMediaUrl) {
// Media removed in new message: replace body with text/content
try { body.innerHTML = window.escapeHtml(msg.content || '[Mensaje]'); } catch(e) { /* ignore */ }
}
}
@@ -2369,8 +2353,140 @@
// remove any remaining old elements that weren't updated
existing.forEach((el) => el.remove());
// If user was near bottom before update, keep it at bottom to follow conversation
if (wasNearBottom) this.scrollToBottom();
// If user was near bottom before update and not actively scrolling, keep it at bottom to follow conversation
const nearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 150;
const shouldScroll = initial ? true : (nearBottom && !this._userScrolling);
// Determine if we received new messages since last render
const prevCount = this._lastRenderMessageCount || 0;
const newCount = this.conversations.length || 0;
const added = Math.max(0, newCount - prevCount);
if (shouldScroll) {
this.scrollToBottom();
// hide indicator if visible
if (added) this.hideNewMessagesIndicator();
} else {
if (added > 0) this.showNewMessagesIndicator(added);
}
// Save for next render
this._lastRenderMessageCount = newCount;
}
// Initialize recording handlers (so mic works before openConversation) and helper to manage recording state
initRecordingHandlers() {
if (this._recordingHandlersInitialized) return;
this._recordingHandlersInitialized = true;
this._mediaRecorder = null;
this._recordingInterval = null;
this._recordingStart = null;
this.startRecording = async function() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._mediaRecorder = new MediaRecorder(stream);
const chunks = [];
this._mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
this._mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
let file = new File([blob], `record_${Date.now()}.webm`, { type: 'audio/webm' });
// Try convert if FFmpeg available
try {
if (window && window.ensureFFmpeg) {
await window.ensureFFmpeg();
const converted = await window.convertWebmToOgg(file);
file = converted;
}
} catch (convErr) { console.warn('conversion failed', convErr); }
// set as selected file and show preview
this.selectedFile = file;
this.showMediaPreview(file);
};
this._mediaRecorder.start();
this._recordingStart = Date.now();
const ind = document.getElementById('recording-indicator'); if (ind) ind.style.display = 'block';
const micEl = document.getElementById('mic-btn'); if (micEl) micEl.classList.add('recording');
// Ensure UI shows mic and hides send while recording
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
const update = () => {
if (!this._recordingStart) return;
const elapsed = Math.floor((Date.now() - this._recordingStart) / 1000);
const m = Math.floor(elapsed/60); const s = elapsed % 60;
const rt = document.getElementById('recording-time'); if (rt) rt.textContent = `${m}:${String(s).padStart(2,'0')}`;
};
update();
this._recordingInterval = setInterval(update, 1000);
} catch (err) {
console.error('startRecording error', err);
alert('No se pudo acceder al micrófono: ' + (err.message||err));
}
};
this.stopRecording = function() {
if (this._mediaRecorder && this._mediaRecorder.state !== 'inactive') {
this._mediaRecorder.stop();
}
if (this._recordingInterval) clearInterval(this._recordingInterval);
this._recordingInterval = null;
this._recordingStart = null;
const ind = document.getElementById('recording-indicator'); if (ind) ind.style.display = 'none';
const micEl = document.getElementById('mic-btn'); if (micEl) micEl.classList.remove('recording');
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
};
this.cancelRecording = function() {
if (this._mediaRecorder && this._mediaRecorder.state !== 'inactive') {
this._mediaRecorder.stop();
}
this._mediaRecorder = null;
this.selectedFile = null;
if (this._recordingInterval) clearInterval(this._recordingInterval);
this._recordingInterval = null;
this._recordingStart = null;
const ind = document.getElementById('recording-indicator'); if (ind) ind.style.display = 'none';
const micEl = document.getElementById('mic-btn'); if (micEl) micEl.classList.remove('recording');
if (this._updateSendMicVisibility) this._updateSendMicVisibility();
};
return true;
}
// --- New messages indicator helpers ---
showNewMessagesIndicator(count) {
try {
let el = document.getElementById('new-messages-indicator');
if (!el) {
el = document.createElement('div');
el.id = 'new-messages-indicator';
el.style.position = 'fixed';
el.style.bottom = '90px';
el.style.left = '50%';
el.style.transform = 'translateX(-50%)';
el.style.background = '#0d6efd';
el.style.color = 'white';
el.style.padding = '8px 12px';
el.style.borderRadius = '20px';
el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
el.style.zIndex = '2000';
el.style.cursor = 'pointer';
el.onclick = () => { this.scrollToBottom(); this.hideNewMessagesIndicator(); };
document.body.appendChild(el);
}
el.textContent = (count > 1) ? (`${count} mensajes nuevos`) : '1 mensaje nuevo';
el.style.display = 'block';
} catch (e) { console.warn('showNewMessagesIndicator failed', e); }
}
hideNewMessagesIndicator() {
try {
const el = document.getElementById('new-messages-indicator');
if (el) el.style.display = 'none';
} catch (e) { console.warn('hideNewMessagesIndicator failed', e); }
}
}
getStatusIcon(status) {