Se corrigen bugs de chat en tiempo real
This commit is contained in:
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
|
|
||||||
div.innerHTML = `
|
|
||||||
<div class="bubble">
|
|
||||||
<strong>${e.message.user.name}</strong><br>
|
|
||||||
${e.message.message}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
messagesDiv.appendChild(div);
|
|
||||||
|
|
||||||
scrollChat();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
scrollChat();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| ENVIAR MENSAJE
|
| DETECTAR ESCRITURA
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user