se cargan modulos de chat en tiempo real

This commit is contained in:
2026-02-28 16:10:19 -06:00
parent 68d32dbd88
commit aeb24f2874
28 changed files with 1830 additions and 33 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class ChatMessageSent implements ShouldBroadcastNow
{
public $message;
public function __construct(Message $message)
{
$this->message = $message->load('user');
}
public function broadcastOn()
{
return new PrivateChannel(
'chat.' . $this->message->conversation_id
);
}
public function broadcastAs()
{
return 'ChatMessageSent';
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public function __construct($message)
{
$this->message = $message;
}
public function broadcastOn()
{
return new Channel('chat');
}
public function broadcastAs()
{
return 'MessageSent';
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Events\ChatMessageSent;
use App\Models\Conversation;
use App\Models\Message;
use App\Models\User;
use Illuminate\Http\Request;
class ChatController extends Controller
{
public function send(Request $request)
{
$message = Message::create([
'conversation_id' => $request->conversation_id,
'user_id' => auth()->id(),
'message' => $request->message
]);
$message->load('user');
broadcast(new ChatMessageSent($message))->toOthers();
return response()->json($message);
}
public function start(User $user)
{
$authUser = auth()->user();
// buscar conversación existente
$conversation = Conversation::whereHas('users', function ($q) use ($authUser) {
$q->where('user_id', $authUser->id);
})
->whereHas('users', function ($q) use ($user) {
$q->where('user_id', $user->id);
})
->first();
// si no existe → crear
if (!$conversation) {
$conversation = Conversation::create();
$conversation->users()->attach([
$authUser->id,
$user->id
]);
}
return response()->json($conversation);
}
public function show($id)
{
$conversation = Conversation::with([
'messages' => function ($query) {
$query->orderBy('created_at', 'asc');
},
'messages.user'
])->findOrFail($id);
return view('chat.show', compact('conversation'));
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Conversation extends Model
{
public function users()
{
return $this->belongsToMany(User::class);
}
public function messages()
{
return $this->hasMany(Message::class)
->latest()
->limit(50);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = [
'conversation_id',
'user_id',
'message'
];
public function user()
{
return $this->belongsTo(User::class);
}
}
+1
View File
@@ -9,6 +9,7 @@ return Application::configure(basePath: dirname(__DIR__))
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
+1
View File
@@ -13,6 +13,7 @@
"laravel/cashier-paddle": "^2.6",
"laravel/framework": "^12.0",
"laravel/jetstream": "^5.4",
"laravel/reverb": "^1.8",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1",
"livewire/livewire": "^3.6.4",
Generated
+1003 -1
View File
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];
+96
View File
@@ -0,0 +1,96 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'),
],
],
],
];
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('conversations');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('conversation_user', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('conversation_user');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('message');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('messages');
}
};
+152
View File
@@ -11,8 +11,10 @@
"autoprefixer": "^10.4.16",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.3.0",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.32",
"pusher-js": "^8.4.0",
"tailwindcss": "^3.4.0",
"vite": "^7.0.7"
}
@@ -868,6 +870,13 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/forms": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz",
@@ -1590,6 +1599,24 @@
"node": ">=4"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1653,6 +1680,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
@@ -2132,6 +2183,20 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/laravel-echo": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.0.tgz",
"integrity": "sha512-wgHPnnBvfHmu2I58xJ4asZH37Nu6P0472ku6zuoGRLc3zEWwIbpovDLYTiOshDH1SM7rA6AjZTKuu+jYoM1tpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz",
@@ -2523,6 +2588,13 @@
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -2824,6 +2896,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3012,6 +3095,37 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3256,6 +3370,13 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -3412,6 +3533,37 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+2
View File
@@ -13,8 +13,10 @@
"autoprefixer": "^10.4.16",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.3.0",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.32",
"pusher-js": "^8.4.0",
"tailwindcss": "^3.4.0",
"vite": "^7.0.7"
}
+28
View File
@@ -288,3 +288,31 @@ button:hover {
#content{
flex:1;
}
.message {
display: flex;
margin-bottom: 10px;
}
.message.mine {
justify-content: flex-end;
}
.message.theirs {
justify-content: flex-start;
}
.bubble {
max-width: 60%;
padding: 10px;
border-radius: 12px;
background: #e4e6eb;
}
.message.mine .bubble {
background: #4e73df;
color: white;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-BuUgh5ie.css",
"file": "assets/app-DL2CrTMt.css",
"src": "resources/css/app.css",
"isEntry": true,
"name": "app",
@@ -9,7 +9,7 @@
]
},
"resources/js/app.js": {
"file": "assets/app-CAiCLEjY.js",
"file": "assets/app-CxcWyewu.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
+8
View File
@@ -2,3 +2,11 @@ import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allow your team to quickly build robust real-time web applications.
*/
import './echo';
+14
View File
@@ -0,0 +1,14 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
});
-23
View File
@@ -1,23 +0,0 @@
@extends('layouts.landing')
@section('title',"show")
@section('content')
<div class="container-fluid">
<div class="card shadow mb-4">
<div class="card-header py-3">
<div class="d-sm-flex align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Datos carrera</h6>
<a href="{{route('carrera.index')}}" class="d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i class="fas fa-download fa-sm text-white-50"></i> Regresar</a>
</div>
</div>
<div class="card-body">
<h1>{{$carrera->name}}</h1>
</div>
</div>
</div>
@endsection
+160
View File
@@ -0,0 +1,160 @@
@extends('layouts.landing')
@section('title',"Chat")
@section('content')
<div class="container-fluid">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
Mensajes
</h6>
</div>
<div class="card-body">
{{-- CONTENEDOR CHAT --}}
<div id="chat-app"
data-conversation-id="{{ $conversation->id }}">
{{-- MENSAJES --}}
<div id="messages"
style="height:400px;overflow-y:auto;padding:10px;background:#f5f5f5;border-radius:10px;">
@foreach($conversation->messages as $message)
<div class="message {{ $message->user_id == auth()->id() ? 'mine' : 'theirs' }}">
<div class="bubble">
<strong>{{ $message->user->name }}</strong><br>
{{ $message->message }}
</div>
</div>
@endforeach
</div>
{{-- INPUT --}}
<div style="margin-top:10px;display:flex;gap:10px;">
<input id="messageInput"
class="form-control"
placeholder="Escribe mensaje...">
<button onclick="sendMessage()"
class="btn btn-primary">
Enviar
</button>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
window.USER_ID = {{ auth()->id() }};
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const chatApp = document.getElementById('chat-app');
const messagesDiv = document.getElementById('messages');
if (!chatApp || !messagesDiv) {
console.error('Chat DOM no encontrado');
return;
}
const conversationId =
chatApp.dataset.conversationId;
console.log('Chat conectado a:', conversationId);
/*
|--------------------------------------------------------------------------
| ESCUCHAR MENSAJES REALTIME
|--------------------------------------------------------------------------
*/
window.Echo.private(`chat.${conversationId}`)
.listen('.ChatMessageSent', (e) => {
console.log('Nuevo mensaje:', e);
let mine =
e.message.user_id == window.USER_ID;
let div = document.createElement('div');
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
|--------------------------------------------------------------------------
*/
function sendMessage() {
let input = document.getElementById('messageInput');
if (!input.value.trim()) return;
axios.post('/chat/send', {
conversation_id:
document
.getElementById('chat-app')
.dataset.conversationId,
message: input.value
});
input.value = '';
}
/*
|--------------------------------------------------------------------------
| AUTO SCROLL
|--------------------------------------------------------------------------
*/
function scrollChat() {
const container =
document.getElementById('messages');
container.scrollTop =
container.scrollHeight;
}
</script>
@endpush
@@ -3,6 +3,8 @@
<!DOCTYPE html>
<html lang="es">
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@include('layouts._partials.head')
{{-- Livewire styles --}}
+15
View File
@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('chat.{conversationId}', function ($user, $conversationId) {
return \App\Models\Conversation::where('id', $conversationId)
->whereHas('users', fn ($q) =>
$q->where('user_id', $user->id)
)->exists();
});
+6
View File
@@ -1,9 +1,11 @@
<?php
use App\Events\MessageSent;
use App\Http\Controllers\AreaCodeController;
use App\Http\Controllers\AreaController;
use App\Http\Controllers\CampanController;
use App\Http\Controllers\CarreraController;
use App\Http\Controllers\ChatController;
use App\Http\Controllers\CodeController;
use App\Http\Controllers\ConceptoController;
use App\Http\Controllers\DurationController;
@@ -46,6 +48,9 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified',
Route::resource('/carrera',CarreraController::class);
Route::resource('/code',CodeController::class);
Route::resource('/concepto',ConceptoController::class);
Route::get('/chat/start/{user}',[ChatController::class, 'start']);
Route::get('/chat/{conversation}',[ChatController::class, 'show']);
Route::post('/chat/send',[ChatController::class,'send']);
Route::resource('/duration',DurationController::class);
Route::get('/horario', [HorarioController::class, 'index'])->name('horario.index');
Route::get('/horario/create', [HorarioController::class, 'create'])->name('horario.create');
@@ -69,6 +74,7 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified',
Route::resource('/prospecto',ProspectoController::class);
Route::get('/registros', [CodeController::class,'registros' ])->name('registros');
Route::resource('/role',RoleController::class);
Route::get('/test-chat', function () { broadcast(new MessageSent('Hola realtime 🚀')); return 'Mensaje enviado';});
Route::resource('/turno',TurnoController::class);
Route::resource('/user',UserController::class);
Route::resource('/mail',MailController::class);