Update conversations.php
This commit is contained in:
+198
-82
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user