feature: user can chat to each other
This commit is contained in:
parent
12726c93b5
commit
964d7a8936
@ -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) {
|
||||
|
||||
27
app/Actions/SendMessageAction.php
Normal file
27
app/Actions/SendMessageAction.php
Normal 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();
|
||||
|
||||
}
|
||||
}
|
||||
38
app/Events/MessageSent.php
Normal file
38
app/Events/MessageSent.php
Normal 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]}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,21 +2,45 @@
|
||||
|
||||
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
|
||||
{
|
||||
//
|
||||
public function index(#[CurrentUser] User $user)
|
||||
{
|
||||
return view('dashboards.user.chat') ;
|
||||
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.']);
|
||||
}
|
||||
}
|
||||
|
||||
20
app/Http/Requests/SendMessageRequest.php
Normal file
20
app/Http/Requests/SendMessageRequest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
34
resources/js/message.js
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
@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-heroicon-o-paper-airplane class="w-5 h-5"/>
|
||||
</x-ui.button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user