se cargan modulos de chat en tiempo real
This commit is contained in:
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Generated
+152
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Vendored
+8
@@ -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';
|
||||
|
||||
@@ -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'],
|
||||
});
|
||||
@@ -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
|
||||
@@ -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 --}}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user