feature: user can chat to each other

This commit is contained in:
kusowl 2026-02-16 12:30:57 +05:30
parent 12726c93b5
commit 964d7a8936
13 changed files with 208 additions and 17 deletions

View File

@ -9,13 +9,12 @@
final readonly class CreateOrGetInboxAction
{
/**
*
* @throws \Throwable
*/
public function execute(User $recipient, User $sender): Inbox
{
$existingInbox = Inbox::whereHas('users', fn($q) => $q->where('id', $sender->id))
->whereHas('users', fn($q) => $q->where('id', $recipient->id))
$existingInbox = Inbox::whereHas('users', fn ($q) => $q->where('users.id', $sender->id))
->whereHas('users', fn ($q) => $q->where('users.id', $recipient->id))
->first();
if ($existingInbox) {

View File

@ -0,0 +1,27 @@
<?php
namespace App\Actions;
use App\Events\MessageSent;
use App\Models\User;
final readonly class SendMessageAction
{
public function __construct(private CreateOrGetInboxAction $inboxAction) {}
/**
* @throws \Throwable
*/
public function execute(User $sender, User $recipient, array $data): void
{
// find the inbox between the two users
$inbox = $this->inboxAction->execute($recipient, $sender);
$message = $inbox->messages()->create([
'message' => $data['message'],
'user_id' => $sender->id,
]);
broadcast(new MessageSent($message))->toOthers();
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Message $message)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
$users = $this->message->inbox->users->pluck('id')->toArray();
sort($users);
return [
new PrivateChannel("chat.{$users[0]}.{$users[1]}"),
];
}
}

View File

@ -2,9 +2,11 @@
namespace App\Http\Controllers;
use App\Actions\CreateOrGetInboxAction;
use App\Actions\SendMessageAction;
use App\Http\Requests\SendMessageRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\Request;
class ChatController extends Controller
{
@ -14,9 +16,31 @@ public function index(#[CurrentUser] User $user)
return view('dashboards.user.chat');
}
public function show(User $recipient)
public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetInboxAction $action)
{
try {
$inbox = $action->execute($recipient, $sender);
} catch (\Throwable $e) {
\Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
abort(500);
}
return view('dashboards.user.chat')
->with('recipient', $recipient);
->with('recipient', $recipient)
->with('chats', $inbox->messages()->latest()->get());
}
/**
* @throws \Throwable
*/
public function store(
#[CurrentUser] User $sender,
User $recipient,
SendMessageRequest $request,
SendMessageAction $action
) {
$action->execute($sender, $recipient, $request->validated());
response()->json(['message' => 'Message sent successfully.']);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendMessageRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => 'required|string',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
@ -15,6 +16,7 @@
* @property-read \App\Models\User|null $lastUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read int|null $users_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox query()
@ -23,11 +25,13 @@
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastMessage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Inbox extends Model
{
protected $fillable = ['last_user_id', 'last_message'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
@ -37,4 +41,9 @@ public function lastUser(): HasOne
{
return $this->hasOne(User::class, 'id', 'last_user_id');
}
public function messages(): HasMany
{
return $this->hasMany(Message::class, 'inbox_id');
}
}

View File

@ -15,6 +15,7 @@ import {deleteRecentSearch} from "./deleteRecentSearch.js";
import {initNavMenu} from "./nav-menu.js";
import {toggleShimmer} from "./shimmer.js";
import {follow} from "./interaction.js";
import {addMessageToChat, sendMessage} from "./message.js";
document.deleteSearch = deleteRecentSearch;
document.like = like;
@ -23,7 +24,8 @@ document.redirect = redirect;
document.showReportModal = showReportModal;
window.toggleShimmer = toggleShimmer;
window.follow = follow;
window.sendMessage = sendMessage;
window.addMessageToChat = addMessageToChat;
window.addEventListener('load', async () => {
const preloader = document.getElementById('preloader');
const content = document.getElementById('content');

34
resources/js/message.js Normal file
View File

@ -0,0 +1,34 @@
export const sendMessage = async (element, event) => {
event.preventDefault()
const messageInput = element.querySelector('[name="message"]');
if (!messageInput.value) return;
const recipentId = element.dataset.recipientId;
const message = messageInput.value;
messageInput.value = '';
addMessageToChat({message: message}, true);
const response = await axios.post(`/api/chat/${recipentId}/message`, {message: message});
}
export const addMessageToChat = (message, right = false) => {
const chatContainer = document.getElementById('chat-container');
if (!chatContainer) return;
const placeholder = chatContainer.querySelector('#no-messages-placeholder');
if (placeholder) {
placeholder.remove();
}
const messagePlaceholder = `
<div class="grid px-4 my-1 w-full ${right ? 'place-items-end' : 'place-items-start'}">
<div class="w-fit px-6 py-2 rounded-2xl bg-gray-200">
${message.message}
</div>
</div>`;
chatContainer.insertAdjacentHTML('afterbegin', messagePlaceholder);
chatContainer.scrollTop = 0;
}

View File

@ -1,14 +1,41 @@
@props(['recipient', 'chats' => []])
<x-dashboard.page-heading class="m-0 mb-0.5" :title="$recipient->name" description=""/>
<div class="bg-gray-100 h-full overflow-hidden">
<div class="text-sm flex flex-col-reverse overflow-y-scroll h-full max-h-screen pb-50 scroll-snap-y-container">
<div id="chat-container" data-auth-id="{{ auth()->id() }}" data-partner-id="{{ $recipient->id }}"
class="text-sm flex flex-col-reverse overflow-y-scroll h-full max-h-screen pb-50 scroll-snap-y-container">
@forelse($chats as $chat)
<x-chat.message :right="$chat->user_id === auth()->user()->id">{{$chat->text}}</x-chat.message>
@ds($chat)
<x-chat.message :right="$chat->user_id === auth()->user()->id">{{$chat->message}}</x-chat.message>
@empty
<div class="grid px-4 my-1 w-full h-full place-items-center ">
<div id="no-messages-placeholder" class="grid px-4 my-1 w-full h-full place-items-center ">
<p class="text-gray-600">No Messages Found!</p>
</div>
@endforelse
</div>
<x-chat.message-input/>
<x-chat.message-input :recipient_id="$recipient->id"/>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('chat-container');
if (container) {
const authId = parseInt(container.dataset.authId);
const partnerId = parseInt(container.dataset.partnerId);
// Sort IDs for consistent channel naming
const user1 = Math.min(authId, partnerId);
const user2 = Math.max(authId, partnerId);
window.Echo.private(`chat.${user1}.${user2}`)
.listen('MessageSent', (e) => {
const message = e.message;
if (!message) return;
// Check if user is the recipient of the message
if (message.user_id === partnerId) {
addMessageToChat({message: e.message.message}, false);
}
});
}
});
</script>

View File

@ -1,6 +1,8 @@
@props(['recipient_id'])
<div class="absolute bottom-5 w-8/12 rounded-xl left-50 p-2 bg-white border border-gray-300 shadow-xl">
<form action="" class="flex space-x-4 items-center">
<x-ui.textarea class="flex-1" rows="1" name="message" placeholder="Enter your message...">hi</x-ui.textarea>
<form id="messageBox" data-recipient-id="{{$recipient_id}}" onsubmit="sendMessage(this, event)" action=""
class="flex space-x-4 items-center">
<x-ui.textarea class="flex-1" rows="1" name="message" placeholder="Enter your message..."></x-ui.textarea>
<x-ui.button variant="neutral" icon="">
<x-heroicon-o-paper-airplane class="w-5 h-5"/>
</x-ui.button>

View File

@ -6,7 +6,7 @@
</x-slot:sidebarItems>
@if(isset($recipient))
<x-chat.message-box :recipient="$recipient" />
<x-chat.message-box :recipient="$recipient" :chats="$chats" />
@else
<div class="w-full h-full flex items-center justify-center">
<p class="font-bold text-5xl text-gray-400">Start a chat ! </p>

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\ChatController;
use App\Http\Controllers\RecentSearchController;
use Illuminate\Support\Facades\Route;
@ -12,4 +13,6 @@
include __DIR__.'/push-notification.php';
Route::delete('/recent-search/{recentSearch}', RecentSearchController::class)->name('recent-search.destroy');
Route::post('/chat/{recipient}/message', [ChatController::class, 'store'])->name('chat.message');
});

View File

@ -1,7 +1,13 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('chat.{user1}.{user2}', function (User $user, int $user1, int $user2) {
// Only allow the user if their ID matches one of the two in the channel name
return $user->id === $user1 || $user->id === $user2;
});