feature: user can chat to each other
This commit is contained in:
parent
12726c93b5
commit
964d7a8936
@ -9,13 +9,12 @@
|
|||||||
final readonly class CreateOrGetInboxAction
|
final readonly class CreateOrGetInboxAction
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function execute(User $recipient, User $sender): Inbox
|
public function execute(User $recipient, User $sender): Inbox
|
||||||
{
|
{
|
||||||
$existingInbox = Inbox::whereHas('users', fn($q) => $q->where('id', $sender->id))
|
$existingInbox = Inbox::whereHas('users', fn ($q) => $q->where('users.id', $sender->id))
|
||||||
->whereHas('users', fn($q) => $q->where('id', $recipient->id))
|
->whereHas('users', fn ($q) => $q->where('users.id', $recipient->id))
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($existingInbox) {
|
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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\CreateOrGetInboxAction;
|
||||||
|
use App\Actions\SendMessageAction;
|
||||||
|
use App\Http\Requests\SendMessageRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Container\Attributes\CurrentUser;
|
use Illuminate\Container\Attributes\CurrentUser;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ChatController extends Controller
|
class ChatController extends Controller
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
public function index(#[CurrentUser] User $user)
|
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')
|
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\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,6 +16,7 @@
|
|||||||
* @property-read \App\Models\User|null $lastUser
|
* @property-read \App\Models\User|null $lastUser
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
||||||
* @property-read int|null $users_count
|
* @property-read int|null $users_count
|
||||||
|
*
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newModelQuery()
|
* @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 newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox query()
|
* @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 whereLastMessage($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
|
||||||
|
*
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Inbox extends Model
|
class Inbox extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['last_user_id', 'last_message'];
|
protected $fillable = ['last_user_id', 'last_message'];
|
||||||
|
|
||||||
public function users(): BelongsToMany
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class);
|
return $this->belongsToMany(User::class);
|
||||||
@ -37,4 +41,9 @@ public function lastUser(): HasOne
|
|||||||
{
|
{
|
||||||
return $this->hasOne(User::class, 'id', 'last_user_id');
|
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 {initNavMenu} from "./nav-menu.js";
|
||||||
import {toggleShimmer} from "./shimmer.js";
|
import {toggleShimmer} from "./shimmer.js";
|
||||||
import {follow} from "./interaction.js";
|
import {follow} from "./interaction.js";
|
||||||
|
import {addMessageToChat, sendMessage} from "./message.js";
|
||||||
|
|
||||||
document.deleteSearch = deleteRecentSearch;
|
document.deleteSearch = deleteRecentSearch;
|
||||||
document.like = like;
|
document.like = like;
|
||||||
@ -23,7 +24,8 @@ document.redirect = redirect;
|
|||||||
document.showReportModal = showReportModal;
|
document.showReportModal = showReportModal;
|
||||||
window.toggleShimmer = toggleShimmer;
|
window.toggleShimmer = toggleShimmer;
|
||||||
window.follow = follow;
|
window.follow = follow;
|
||||||
|
window.sendMessage = sendMessage;
|
||||||
|
window.addMessageToChat = addMessageToChat;
|
||||||
window.addEventListener('load', async () => {
|
window.addEventListener('load', async () => {
|
||||||
const preloader = document.getElementById('preloader');
|
const preloader = document.getElementById('preloader');
|
||||||
const content = document.getElementById('content');
|
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' => []])
|
@props(['recipient', 'chats' => []])
|
||||||
<x-dashboard.page-heading class="m-0 mb-0.5" :title="$recipient->name" description=""/>
|
<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="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)
|
@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
|
@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>
|
<p class="text-gray-600">No Messages Found!</p>
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
<x-chat.message-input/>
|
<x-chat.message-input :recipient_id="$recipient->id"/>
|
||||||
</div>
|
</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">
|
<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">
|
<form id="messageBox" data-recipient-id="{{$recipient_id}}" onsubmit="sendMessage(this, event)" action=""
|
||||||
<x-ui.textarea class="flex-1" rows="1" name="message" placeholder="Enter your message...">hi</x-ui.textarea>
|
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-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>
|
</x-ui.button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
</x-slot:sidebarItems>
|
</x-slot:sidebarItems>
|
||||||
|
|
||||||
@if(isset($recipient))
|
@if(isset($recipient))
|
||||||
<x-chat.message-box :recipient="$recipient" />
|
<x-chat.message-box :recipient="$recipient" :chats="$chats" />
|
||||||
@else
|
@else
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<div class="w-full h-full flex items-center justify-center">
|
||||||
<p class="font-bold text-5xl text-gray-400">Start a chat ! </p>
|
<p class="font-bold text-5xl text-gray-400">Start a chat ! </p>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\ChatController;
|
||||||
use App\Http\Controllers\RecentSearchController;
|
use App\Http\Controllers\RecentSearchController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -12,4 +13,6 @@
|
|||||||
include __DIR__.'/push-notification.php';
|
include __DIR__.'/push-notification.php';
|
||||||
|
|
||||||
Route::delete('/recent-search/{recentSearch}', RecentSearchController::class)->name('recent-search.destroy');
|
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
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
|
||||||
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||||
return (int) $user->id === (int) $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