feat: dropdown de mensajes muestra conversaciones sin responder

- HeaderComposer ahora muestra conversaciones donde el último mensaje
  es del otro usuario (pendiente de respuesta), no solo mensajes no leídos
- El badge refleja el conteo de conversaciones pendientes
- Al responder desde el chat, se dispara chat:replied para quitar
  la conversación del dropdown en tiempo real
- El badge en tiempo real solo incrementa si es una conversación nueva

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 17:00:17 -06:00
parent e2ebbf38c5
commit 0f7de9b4d8
3 changed files with 46 additions and 14 deletions
+14 -7
View File
@@ -15,18 +15,25 @@ class HeaderComposer
$userId = auth()->id(); $userId = auth()->id();
// Últimas conversaciones con mensajes no leídos // Conversaciones donde el último mensaje es del otro usuario (pendiente de respuesta)
$unreadConversations = Message::where('receiver_id', $userId) $pendingConversations = Message::where(function ($q) use ($userId) {
->whereNull('read_at') $q->where('receiver_id', $userId)
->orWhere('user_id', $userId);
})
->with('sender') ->with('sender')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->get() ->get()
->unique('user_id') ->groupBy(function ($msg) use ($userId) {
return $msg->user_id === $userId ? $msg->receiver_id : $msg->user_id;
})
->filter(function ($msgs) use ($userId) {
return $msgs->first()->user_id !== $userId;
})
->map(fn($msgs) => $msgs->first())
->take(5); ->take(5);
$unreadMessagesCount = Message::where('receiver_id', $userId) $unreadConversations = $pendingConversations;
->whereNull('read_at') $unreadMessagesCount = $pendingConversations->count();
->count();
// Notificaciones no leídas // Notificaciones no leídas
$unreadNotifications = auth()->user() $unreadNotifications = auth()->user()
+2
View File
@@ -114,6 +114,8 @@ document.getElementById('chat-form').addEventListener('submit', function(e) {
scrollBottom(); scrollBottom();
form.reset(); form.reset();
document.getElementById('attachment-preview').textContent = ''; document.getElementById('attachment-preview').textContent = '';
// Notificar al header que esta conversación ya fue respondida
window.dispatchEvent(new CustomEvent('chat:replied', { detail: { receiverId } }));
}); });
}); });
@@ -147,10 +147,10 @@
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ Auth::user()->name }}</span> <span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ Auth::user()->name }}</span>
{{-- <img class="img-profile rounded-circle" src="{{ Auth::user()->profile_photo_url }}"> --}} <img class="img-profile rounded-circle" src="{{ Auth::user()->profile_photo_url }}">
<img class="img-profile rounded-circle" {{-- <img class="img-profile rounded-circle"
src="{{ Auth::user()->profile_photo_url }}" src="{{ Auth::user()->profile_photo_url }}"
alt="{{ Auth::user()->name }}"> alt="{{ Auth::user()->name }}"> --}}
</a> </a>
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
aria-labelledby="userDropdown"> aria-labelledby="userDropdown">
@@ -199,8 +199,6 @@ waitForEcho(() => {
// Badge + dropdown de mensajes // Badge + dropdown de mensajes
window.Echo.channel(`chat.${authId}`) window.Echo.channel(`chat.${authId}`)
.listen('MessageSent', (e) => { .listen('MessageSent', (e) => {
updateBadge('msg-badge', 1);
const list = document.querySelector('#messagesDropdown + .dropdown-list'); const list = document.querySelector('#messagesDropdown + .dropdown-list');
if (!list) return; if (!list) return;
@@ -208,9 +206,14 @@ waitForEcho(() => {
const empty = list.querySelector('.text-muted.py-3'); const empty = list.querySelector('.text-muted.py-3');
if (empty) empty.remove(); if (empty) empty.remove();
// Quitar entrada previa del mismo remitente si ya estaba // Si ya había una entrada de este remitente, quitarla (actualizar con el nuevo)
const existing = list.querySelector(`[data-sender-id="${e.sender.id}"]`); const existing = list.querySelector(`[data-sender-id="${e.sender.id}"]`);
if (existing) existing.remove(); if (!existing) {
// Solo incrementar badge si es una conversación nueva en el dropdown
updateBadge('msg-badge', 1);
} else {
existing.remove();
}
// Insertar nueva entrada al tope (después del header) // Insertar nueva entrada al tope (después del header)
const preview = e.body ? e.body : '📎 Archivo adjunto'; const preview = e.body ? e.body : '📎 Archivo adjunto';
@@ -232,6 +235,26 @@ waitForEcho(() => {
list.querySelector('.dropdown-header').insertAdjacentElement('afterend', item); list.querySelector('.dropdown-header').insertAdjacentElement('afterend', item);
}); });
// Cuando el usuario responde desde el chat, quitar esa conversación del dropdown
window.addEventListener('chat:replied', (ev) => {
const receiverId = ev.detail.receiverId;
const list = document.querySelector('#messagesDropdown + .dropdown-list');
if (!list) return;
const entry = list.querySelector(`[data-sender-id="${receiverId}"]`);
if (entry) {
entry.remove();
updateBadge('msg-badge', -1);
// Si ya no quedan conversaciones, mostrar el mensaje vacío
const remaining = list.querySelectorAll('.dropdown-item[data-sender-id]');
if (remaining.length === 0) {
const empty = document.createElement('div');
empty.className = 'dropdown-item text-center text-muted py-3';
empty.innerHTML = '<i class="fas fa-check-circle text-success mr-1"></i> Sin mensajes nuevos';
list.querySelector('.dropdown-header').insertAdjacentElement('afterend', empty);
}
}
});
// Badge de notificaciones // Badge de notificaciones
window.Echo.channel(`notifications.${authId}`) window.Echo.channel(`notifications.${authId}`)
.listen('NewNotification', (e) => { .listen('NewNotification', (e) => {