Se corrigen bugs de chat en tiempo real

This commit is contained in:
2026-03-04 16:26:45 -06:00
parent e47938001a
commit ec6a72ba0c
6 changed files with 216 additions and 60 deletions
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Broadcasting\PrivateChannel;
class UserStoppedTyping implements ShouldBroadcast
{
public $conversationId;
public $user;
public function __construct($conversationId, $user)
{
$this->conversationId = $conversationId;
$this->user = $user;
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->conversationId);
}
public function broadcastAs()
{
return 'UserStoppedTyping';
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class UserTyping implements ShouldBroadcast
{
public $conversationId;
public $user;
public function __construct($conversationId, $user)
{
$this->conversationId = $conversationId;
$this->user = $user;
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->conversationId);
}
public function broadcastAs()
{
return 'UserTyping';
}
}
+12 -3
View File
@@ -13,17 +13,26 @@ class ChatController extends Controller
public function send(Request $request) public function send(Request $request)
{ {
$request->validate([
'conversation_id' => 'required|exists:conversations,id',
'message' => 'required|string'
]);
$message = Message::create([ $message = Message::create([
'conversation_id' => $request->conversation_id, 'conversation_id' => $request->conversation_id,
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'message' => $request->message 'message' => $request->message
]); ]);
// cargar usuario para que venga en el JSON
$message->load('user'); $message->load('user');
// enviar a los demás usuarios por websocket
broadcast(new ChatMessageSent($message))->toOthers(); broadcast(new ChatMessageSent($message))->toOthers();
return response()->json($message); return response()->json([
'message' => $message
]);
} }
public function start(User $user) public function start(User $user)
@@ -53,7 +62,7 @@ class ChatController extends Controller
} }
public function show($id) public function show($id)
{ {
$conversation = Conversation::with([ $conversation = Conversation::with([
'messages' => function ($query) { 'messages' => function ($query) {
$query->orderBy('created_at', 'asc'); $query->orderBy('created_at', 'asc');
@@ -62,5 +71,5 @@ class ChatController extends Controller
])->findOrFail($id); ])->findOrFail($id);
return view('chat.show', compact('conversation')); return view('chat.show', compact('conversation'));
} }
} }
+17
View File
@@ -316,3 +316,20 @@ button:hover {
color: white; color: white;
} }
.typing-indicator::after {
content: "...";
animation: dots 1.5s infinite;
}
@keyframes dots {
0% {content: ".";}
33% {content: "..";}
66% {content: "...";}
}
#typingIndicator{
font-size:12px;
color:#6c757d;
min-height:18px;
padding-left:5px;
}
+112 -41
View File
@@ -19,11 +19,10 @@
<div id="chat-app" <div id="chat-app"
data-conversation-id="{{ $conversation->id }}"> data-conversation-id="{{ $conversation->id }}">
{{-- MENSAJES --}}
<div id="messages" <div id="messages"
style="height:400px;overflow-y:auto;padding:10px;background:#f5f5f5;border-radius:10px;"> style="height:70vh;overflow-y:auto;padding:10px;background:#f5f5f5;border-radius:10px;">
@foreach($conversation->messages as $message) @foreach($conversation->messages->sortBy('created_at') as $message)
<div class="message {{ $message->user_id == auth()->id() ? 'mine' : 'theirs' }}"> <div class="message {{ $message->user_id == auth()->id() ? 'mine' : 'theirs' }}">
<div class="bubble"> <div class="bubble">
@@ -36,6 +35,10 @@
</div> </div>
<div id="typingIndicator"
style="font-size:12px;color:#888;padding:8px 5px;height:20px;">
</div>
{{-- INPUT --}} {{-- INPUT --}}
<div style="margin-top:10px;display:flex;gap:10px;"> <div style="margin-top:10px;display:flex;gap:10px;">
<input id="messageInput" <input id="messageInput"
@@ -50,6 +53,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -64,97 +68,164 @@ window.USER_ID = {{ auth()->id() }};
</script> </script>
<script> <script>
let conversationId;
let typingTimeout;
let typingActive = false;
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const chatApp = document.getElementById('chat-app'); const chatApp = document.getElementById('chat-app');
const messagesDiv = document.getElementById('messages'); const messagesDiv = document.getElementById('messages');
const input = document.getElementById('messageInput');
if (!chatApp || !messagesDiv) { if (!chatApp || !messagesDiv) {
console.error('Chat DOM no encontrado'); console.error('Chat DOM no encontrado');
return; return;
} }
const conversationId = conversationId = chatApp.dataset.conversationId;
chatApp.dataset.conversationId;
console.log('Chat conectado a:', conversationId); console.log('Chat conectado a:', conversationId);
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| ESCUCHAR MENSAJES REALTIME | ESCUCHAR "USUARIO ESCRIBIENDO"
|--------------------------------------------------------------------------
*/
window.Echo.private(`chat.${conversationId}`)
.listen('.UserTyping', (e) => {
if (e.user.id == window.USER_ID) return;
document.getElementById('typingIndicator').innerText =
e.user.name + ' está escribiendo...';
});
/*
|--------------------------------------------------------------------------
| ESCUCHAR "USUARIO DEJÓ DE ESCRIBIR"
|--------------------------------------------------------------------------
*/
window.Echo.private(`chat.${conversationId}`)
.listen('.UserStoppedTyping', (e) => {
if (e.user.id == window.USER_ID) return;
document.getElementById('typingIndicator').innerText = '';
});
/*
|--------------------------------------------------------------------------
| ESCUCHAR MENSAJES
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
window.Echo.private(`chat.${conversationId}`) window.Echo.private(`chat.${conversationId}`)
.listen('.ChatMessageSent', (e) => { .listen('.ChatMessageSent', (e) => {
console.log('Nuevo mensaje:', e); if (e.message.user_id == window.USER_ID) return;
let mine = appendMessage(e.message);
e.message.user_id == window.USER_ID;
let div = document.createElement('div'); document.getElementById('typingIndicator').innerText = '';
});
div.className = /*
'message ' + (mine ? 'mine' : 'theirs'); |--------------------------------------------------------------------------
| DETECTAR ESCRITURA
|--------------------------------------------------------------------------
*/
div.innerHTML = ` input.addEventListener('input', () => {
<div class="bubble">
<strong>${e.message.user.name}</strong><br>
${e.message.message}
</div>
`;
messagesDiv.appendChild(div); if (!typingActive) {
typingActive = true;
axios.post('/chat/typing', {
conversation_id: conversationId
});
}
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
typingActive = false;
axios.post('/chat/stop-typing', {
conversation_id: conversationId
});
}, 700);
scrollChat();
}); });
scrollChat(); scrollChat();
}); });
/*
|--------------------------------------------------------------------------
| ENVIAR MENSAJE
|--------------------------------------------------------------------------
*/
function sendMessage() { function sendMessage() {
let input = document.getElementById('messageInput'); let input = document.getElementById('messageInput');
if (!input.value.trim()) return; if (!input.value.trim()) return;
axios.post('/chat/send', { typingActive = false;
conversation_id:
document
.getElementById('chat-app')
.dataset.conversationId,
axios.post('/chat/stop-typing', {
conversation_id: conversationId
});
axios.post('/chat/send', {
conversation_id: conversationId,
message: input.value message: input.value
})
.then(response => {
appendMessage(response.data.message);
document.getElementById('typingIndicator').innerText = '';
}); });
input.value = ''; input.value = '';
} }
/* function appendMessage(message) {
|--------------------------------------------------------------------------
| AUTO SCROLL const messagesDiv = document.getElementById('messages');
|--------------------------------------------------------------------------
*/ let mine = message.user_id == window.USER_ID;
let div = document.createElement('div');
div.className = 'message ' + (mine ? 'mine' : 'theirs');
div.innerHTML = `
<div class="bubble">
<strong>${message.user.name}</strong><br>
${message.message}
</div>
`;
messagesDiv.appendChild(div);
scrollChat();
}
function scrollChat() { function scrollChat() {
const container = const container = document.getElementById('messages');
document.getElementById('messages');
container.scrollTop = container.scrollHeight;
container.scrollTop =
container.scrollHeight;
} }
</script> </script>
@endpush @endpush
+3
View File
@@ -27,6 +27,7 @@ use App\Http\Controllers\StripeWebhookController;
use App\Http\Controllers\TurnoController; use App\Http\Controllers\TurnoController;
use App\Http\Controllers\UnsubscribeController; use App\Http\Controllers\UnsubscribeController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Laravel\Jetstream\Http\Controllers\Livewire\UserProfileController; use Laravel\Jetstream\Http\Controllers\Livewire\UserProfileController;
@@ -51,6 +52,8 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified',
Route::get('/chat/start/{user}',[ChatController::class, 'start']); Route::get('/chat/start/{user}',[ChatController::class, 'start']);
Route::get('/chat/{conversation}',[ChatController::class, 'show']); Route::get('/chat/{conversation}',[ChatController::class, 'show']);
Route::post('/chat/send',[ChatController::class,'send']); Route::post('/chat/send',[ChatController::class,'send']);
Route::post('/chat/typing', function (Request $request) {broadcast(new \App\Events\UserTyping($request->conversation_id,auth()->user()))->toOthers();return response()->json();});
Route::post('/chat/stop-typing', function (Request $request) {broadcast(new \App\Events\UserStoppedTyping($request->conversation_id,auth()->user()))->toOthers();return response()->json();});
Route::resource('/duration',DurationController::class); Route::resource('/duration',DurationController::class);
Route::get('/horario', [HorarioController::class, 'index'])->name('horario.index'); Route::get('/horario', [HorarioController::class, 'index'])->name('horario.index');
Route::get('/horario/create', [HorarioController::class, 'create'])->name('horario.create'); Route::get('/horario/create', [HorarioController::class, 'create'])->name('horario.create');