diff --git a/app/Events/ChatMessageSent.php b/app/Events/ChatMessageSent.php deleted file mode 100644 index bc13e983..00000000 --- a/app/Events/ChatMessageSent.php +++ /dev/null @@ -1,29 +0,0 @@ -message = $message->load('user'); - } - - public function broadcastOn() - { - return new PrivateChannel( - 'chat.' . $this->message->conversation_id - ); - } - - public function broadcastAs() - { - return 'ChatMessageSent'; - } -} diff --git a/app/Events/MessageSent.php b/app/Events/MessageSent.php index 6c2163de..4308a1ae 100644 --- a/app/Events/MessageSent.php +++ b/app/Events/MessageSent.php @@ -2,30 +2,39 @@ namespace App\Events; +use App\Models\Message; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Broadcasting\ShouldBroadcast; -class MessageSent implements ShouldBroadcast +class MessageSent implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; - public $message; + public function __construct(public Message $message){} - public function __construct($message) + public function broadcastOn(): array { - $this->message = $message; + return [ + new Channel('chat.' . $this->message->receiver_id), + ]; } - public function broadcastOn() + public function broadcastWith(): array { - return new Channel('chat'); - } - - public function broadcastAs() - { - return 'MessageSent'; + return [ + 'id' => $this->message->id, + 'body' => $this->message->body, + 'attachment' => $this->message->attachment, + 'attachment_type' => $this->message->attachment_type, + 'created_at' => $this->message->created_at->format('H:i'), + 'sender' => [ + 'id' => $this->message->sender->id, + 'name' => $this->message->sender->name, + 'avatar' => $this->message->sender->profile_photo_url ?? null, + ], + ]; } } diff --git a/app/Events/UserStoppedTyping.php b/app/Events/UserStoppedTyping.php deleted file mode 100644 index b1728f90..00000000 --- a/app/Events/UserStoppedTyping.php +++ /dev/null @@ -1,28 +0,0 @@ -conversationId = $conversationId; - $this->user = $user; - } - - public function broadcastOn() - { - return new PrivateChannel('chat.' . $this->conversationId); - } - - public function broadcastAs() - { - return 'UserStoppedTyping'; - } -} diff --git a/app/Events/UserTyping.php b/app/Events/UserTyping.php deleted file mode 100644 index 8cde18f6..00000000 --- a/app/Events/UserTyping.php +++ /dev/null @@ -1,28 +0,0 @@ -conversationId = $conversationId; - $this->user = $user; - } - - public function broadcastOn() - { - return new PrivateChannel('chat.' . $this->conversationId); - } - - public function broadcastAs() - { - return 'UserTyping'; - } -} diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 5025a63f..ae9ca4c1 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -2,74 +2,82 @@ namespace App\Http\Controllers; -use App\Events\ChatMessageSent; -use App\Models\Conversation; +use App\Events\MessageSent; use App\Models\Message; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; class ChatController extends Controller { - - public function send(Request $request) -{ - $request->validate([ - 'conversation_id' => 'required|exists:conversations,id', - 'message' => 'required|string' - ]); - - $message = Message::create([ - 'conversation_id' => $request->conversation_id, - 'user_id' => auth()->id(), - 'message' => $request->message - ]); - - // cargar usuario para que venga en el JSON - $message->load('user'); - - // enviar a los demás usuarios por websocket - broadcast(new ChatMessageSent($message))->toOthers(); - - return response()->json([ - 'message' => $message - ]); -} - - public function start(User $user) + // Lista de usuarios con quienes puedo chatear + public function index() { - $authUser = auth()->user(); + $users = User::where('id', '!=', auth()->id())->get(); + return view('chat.index', compact('users')); + } - // 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(); + // Mensajes entre yo y otro usuario + public function show(User $user) + { + $messages = Message::where(function ($q) use ($user) { + $q->where('user_id', auth()->id()) + ->where('receiver_id', $user->id); + }) + ->orWhere(function ($q) use ($user) { + $q->where('user_id', $user->id) + ->where('receiver_id', auth()->id()); + }) + ->with('sender') + ->orderBy('created_at', 'asc') + ->get(); - // si no existe → crear - if (!$conversation) { - $conversation = Conversation::create(); + // marcar como leídos + Message::where('user_id', $user->id) + ->where('receiver_id', auth()->id()) + ->whereNull('read_at') + ->update(['read_at' => now()]); - $conversation->users()->attach([ - $authUser->id, - $user->id - ]); + return view('chat.show', compact('user', 'messages')); + } + + // Enviar mensaje + public function send(Request $request) + { + $request->validate([ + 'receiver_id' => 'required|exists:users,id', + 'body' => 'nullable|string', + 'attachment' => 'nullable|file|max:20480', + ]); + + $attachment = null; + $attachmentType = null; + + if ($request->hasFile('attachment')) { + $file = $request->file('attachment'); + $attachment = $file->store('chat', 'public'); + $mime = $file->getMimeType(); + + if (str_starts_with($mime, 'image')) $attachmentType = 'image'; + elseif (str_starts_with($mime, 'video')) $attachmentType = 'video'; + elseif (str_starts_with($mime, 'audio')) $attachmentType = 'audio'; + else $attachmentType = 'file'; } - return response()->json($conversation); - } + $message = Message::create([ + 'user_id' => auth()->id(), + 'receiver_id' => $request->receiver_id, + 'body' => $request->body, + 'attachment' => $attachment, + 'attachment_type' => $attachmentType, + ]); - public function show($id) - { - $conversation = Conversation::with([ - 'messages' => function ($query) { - $query->orderBy('created_at', 'asc'); - }, - 'messages.user' - ])->findOrFail($id); + $message->load('sender'); - return view('chat.show', compact('conversation')); + broadcast(new MessageSent($message))->toOthers(); + + $message->created_at = $message->created_at->format('H:i'); + + return response()->json($message); } } diff --git a/app/Models/Friendship.php b/app/Models/Friendship.php new file mode 100644 index 00000000..96373c4b --- /dev/null +++ b/app/Models/Friendship.php @@ -0,0 +1,24 @@ +belongsTo(User::class, 'sender_id'); + } + + public function receiver() + { + return $this->belongsTo(User::class, 'receiver_id'); + } +} diff --git a/app/Models/Message.php b/app/Models/Message.php index 3a755c31..1107b74b 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -7,13 +7,25 @@ use Illuminate\Database\Eloquent\Model; class Message extends Model { protected $fillable = [ - 'conversation_id', 'user_id', - 'message' + 'receiver_id', + 'body', + 'attachment', + 'attachment_type', + 'read_at', ]; - public function user() + protected $casts = [ + 'read_at' => 'datetime', + ]; + + public function sender() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); + } + + public function receiver() + { + return $this->belongsTo(User::class, 'receiver_id'); } } diff --git a/database/migrations/2026_02_28_114213_create_messages_table.php b/database/migrations/2026_04_25_235119_create_messages_table.php similarity index 52% rename from database/migrations/2026_02_28_114213_create_messages_table.php rename to database/migrations/2026_04_25_235119_create_messages_table.php index 9eb3b410..e86c8de2 100644 --- a/database/migrations/2026_02_28_114213_create_messages_table.php +++ b/database/migrations/2026_04_25_235119_create_messages_table.php @@ -13,9 +13,12 @@ return new class extends Migration { Schema::create('messages', function (Blueprint $table) { $table->id(); - $table->foreignId('conversation_id')->constrained()->cascadeOnDelete(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->text('message'); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('receiver_id')->constrained('users')->onDelete('cascade'); + $table->text('body')->nullable(); // nullable porque puede ser solo archivo + $table->string('attachment')->nullable(); // ruta del archivo + $table->string('attachment_type')->nullable(); // 'image', 'audio', 'video' + $table->timestamp('read_at')->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2026_04_26_000250_create_friendships_table.php b/database/migrations/2026_04_26_000250_create_friendships_table.php new file mode 100644 index 00000000..45fbc9b3 --- /dev/null +++ b/database/migrations/2026_04_26_000250_create_friendships_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('sender_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('receiver_id')->constrained('users')->onDelete('cascade'); + $table->enum('status', ['pending', 'accepted', 'rejected', 'blocked'])->default('pending'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('friendships'); + } +}; diff --git a/resources/views/chat/index.blade.php b/resources/views/chat/index.blade.php new file mode 100644 index 00000000..eb405eb7 --- /dev/null +++ b/resources/views/chat/index.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.app') + +@section('content') +