refactor: chat time, grouping and toast
- show message time - group messages by date - show toast when message sent is failed
This commit is contained in:
parent
44b13a5ebf
commit
b2132f8c02
@ -3,7 +3,9 @@
|
|||||||
namespace App\Actions;
|
namespace App\Actions;
|
||||||
|
|
||||||
use App\Events\MessageSent;
|
use App\Events\MessageSent;
|
||||||
|
use App\Exceptions\MessageNotSendException;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use DB;
|
||||||
|
|
||||||
final readonly class SendMessageAction
|
final readonly class SendMessageAction
|
||||||
{
|
{
|
||||||
@ -14,18 +16,30 @@ public function __construct(private CreateOrGetInboxAction $inboxAction) {}
|
|||||||
*/
|
*/
|
||||||
public function execute(User $sender, User $recipient, array $data): void
|
public function execute(User $sender, User $recipient, array $data): void
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
|
|
||||||
// find the inbox between the two users
|
// find the inbox between the two users
|
||||||
\DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
$inbox = $this->inboxAction->execute($recipient, $sender);
|
$inbox = $this->inboxAction->execute($recipient, $sender);
|
||||||
|
|
||||||
|
// update the inbox with the last message and last user as current user
|
||||||
$inbox->last_message = $data['message'];
|
$inbox->last_message = $data['message'];
|
||||||
$inbox->last_user_id = $sender->id;
|
$inbox->last_user_id = $sender->id;
|
||||||
$inbox->save();
|
$inbox->save();
|
||||||
|
|
||||||
|
// create a new message in the inbox
|
||||||
$message = $inbox->messages()->create([
|
$message = $inbox->messages()->create([
|
||||||
'message' => $data['message'],
|
'message' => $data['message'],
|
||||||
'user_id' => $sender->id,
|
'user_id' => $sender->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Send the message to all other users in the inbox
|
||||||
broadcast(new MessageSent($message))->toOthers();
|
broadcast(new MessageSent($message))->toOthers();
|
||||||
\DB::commit();
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
throw new MessageNotSendException('Message not sent.', previous: $e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
app/Exceptions/MessageNotSendException.php
Normal file
7
app/Exceptions/MessageNotSendException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class MessageNotSendException extends Exception {}
|
||||||
@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
use App\Actions\CreateOrGetInboxAction;
|
use App\Actions\CreateOrGetInboxAction;
|
||||||
use App\Actions\SendMessageAction;
|
use App\Actions\SendMessageAction;
|
||||||
|
use App\Exceptions\MessageNotSendException;
|
||||||
use App\Http\Requests\SendMessageRequest;
|
use App\Http\Requests\SendMessageRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Container\Attributes\CurrentUser;
|
use Illuminate\Container\Attributes\CurrentUser;
|
||||||
|
use Log;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class ChatController extends Controller
|
class ChatController extends Controller
|
||||||
{
|
{
|
||||||
@ -21,8 +24,8 @@ public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetIn
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$inbox = $action->execute($recipient, $sender);
|
$inbox = $action->execute($recipient, $sender);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
\Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
|
Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
|
||||||
abort(500);
|
abort(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function store(
|
public function store(
|
||||||
#[CurrentUser] User $sender,
|
#[CurrentUser] User $sender,
|
||||||
@ -41,8 +44,16 @@ public function store(
|
|||||||
SendMessageRequest $request,
|
SendMessageRequest $request,
|
||||||
SendMessageAction $action
|
SendMessageAction $action
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
$action->execute($sender, $recipient, $request->validated());
|
$action->execute($sender, $recipient, $request->validated());
|
||||||
|
|
||||||
response()->json(['message' => 'Message sent successfully.']);
|
return response()->json(['message' => 'Message sent successfully.']);
|
||||||
|
|
||||||
|
} catch (MessageNotSendException $e) {
|
||||||
|
|
||||||
|
Log::error('Message send failed', [$e->getMessage()]);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Message sent failed.'], 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Eloquent;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@ -14,24 +17,24 @@
|
|||||||
* @property string|null $delivered_at
|
* @property string|null $delivered_at
|
||||||
* @property string|null $deleted_at
|
* @property string|null $deleted_at
|
||||||
* @property string|null $failed_at
|
* @property string|null $failed_at
|
||||||
* @property \Illuminate\Support\Carbon $created_at
|
* @property Carbon $created_at
|
||||||
* @property-read \App\Models\Inbox|null $inbox
|
* @property-read Inbox|null $inbox
|
||||||
* @property-read \App\Models\User|null $user
|
* @property-read User|null $user
|
||||||
*
|
*
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message newModelQuery()
|
* @method static Builder<static>|Message newModelQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message newQuery()
|
* @method static Builder<static>|Message newQuery()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message query()
|
* @method static Builder<static>|Message query()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereCreatedAt($value)
|
* @method static Builder<static>|Message whereCreatedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereDeletedAt($value)
|
* @method static Builder<static>|Message whereDeletedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereDeliveredAt($value)
|
* @method static Builder<static>|Message whereDeliveredAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereFailedAt($value)
|
* @method static Builder<static>|Message whereFailedAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereId($value)
|
* @method static Builder<static>|Message whereId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereInboxId($value)
|
* @method static Builder<static>|Message whereInboxId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereMessage($value)
|
* @method static Builder<static>|Message whereMessage($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereReadAt($value)
|
* @method static Builder<static>|Message whereReadAt($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Message whereUserId($value)
|
* @method static Builder<static>|Message whereUserId($value)
|
||||||
*
|
*
|
||||||
* @mixin \Eloquent
|
* @mixin Eloquent
|
||||||
*/
|
*/
|
||||||
class Message extends Model
|
class Message extends Model
|
||||||
{
|
{
|
||||||
@ -43,6 +46,10 @@ class Message extends Model
|
|||||||
'created_at', 'delivered_at',
|
'created_at', 'delivered_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
public function inbox(): BelongsTo
|
public function inbox(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Inbox::class);
|
return $this->belongsTo(Inbox::class);
|
||||||
|
|||||||
@ -1,34 +1,51 @@
|
|||||||
|
import {showToast} from "./toast.js";
|
||||||
|
|
||||||
export const sendMessage = async (element, event) => {
|
export const sendMessage = async (element, event) => {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
const messageInput = element.querySelector('[name="message"]');
|
const messageInput = element.querySelector('[name="message"]');
|
||||||
if (!messageInput.value) return;
|
const message = messageInput.value;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
const recipentId = element.dataset.recipientId;
|
const recipentId = element.dataset.recipientId;
|
||||||
const message = messageInput.value;
|
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
|
|
||||||
addMessageToChat({message: message}, true);
|
// Capture the ID of the UI element we just added
|
||||||
|
const tempMessageId = addMessageToChat({message: message}, true);
|
||||||
|
|
||||||
const response = await axios.post(`/api/chat/${recipentId}/message`, {message: message});
|
try {
|
||||||
|
await axios.post(`/api/chat/${recipentId}/message`, {message: message});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Message could not be sent.');
|
||||||
|
|
||||||
|
const failedMessage = document.getElementById(tempMessageId);
|
||||||
|
if (failedMessage) {
|
||||||
|
failedMessage.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
messageInput.value = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addMessageToChat = (message, right = false) => {
|
export const addMessageToChat = (message, right = false) => {
|
||||||
const chatContainer = document.getElementById('chat-container');
|
const chatContainer = document.getElementById('chat-container');
|
||||||
if (!chatContainer) return;
|
if (!chatContainer) return;
|
||||||
|
|
||||||
|
const tempId = 'msg-' + Date.now();
|
||||||
|
|
||||||
const placeholder = chatContainer.querySelector('#no-messages-placeholder');
|
const placeholder = chatContainer.querySelector('#no-messages-placeholder');
|
||||||
if (placeholder) {
|
if (placeholder) placeholder.remove();
|
||||||
placeholder.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const messagePlaceholder = `
|
const messagePlaceholder = `
|
||||||
<div class="grid px-4 my-1 w-full ${right ? 'place-items-end' : 'place-items-start'}">
|
<div id="${tempId}" class="grid px-4 my-1 w-full ${right ? 'place-items-end' : 'place-items-start'}">
|
||||||
<div class="max-w-[40vw] py-2 px-4 rounded-2xl bg-gray-200">
|
<div class="max-w-[40vw] py-2 px-4 rounded-full ${right ? 'rounded-br-none' : 'rounded-tl-none'} bg-gray-200">
|
||||||
${message.message}
|
${message.message}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
chatContainer.insertAdjacentHTML('afterbegin', messagePlaceholder);
|
chatContainer.insertAdjacentHTML('afterbegin', messagePlaceholder);
|
||||||
|
|
||||||
chatContainer.scrollTop = 0;
|
chatContainer.scrollTop = 0;
|
||||||
|
|
||||||
|
// Return the ID so the caller can find this specific message later
|
||||||
|
return tempId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,32 @@
|
|||||||
@props(['recipient', 'messages' => []])
|
@props(['recipient', 'messages' => []])
|
||||||
|
@php
|
||||||
|
$groupedMessages = $messages->groupBy(function($msg) {
|
||||||
|
return $msg->created_at->format('M j, Y');
|
||||||
|
});
|
||||||
|
@endphp
|
||||||
|
|
||||||
<x-dashboard.page-heading class="m-0 mb-0.5" :title="$recipient->name" description="offline"/>
|
<x-dashboard.page-heading class="m-0 mb-0.5" :title="$recipient->name" description="offline"/>
|
||||||
<div class="bg-gray-50 h-full overflow-hidden flex-shrink-0">
|
<div class="bg-gray-50 h-full overflow-hidden flex-shrink-0">
|
||||||
<div id="chat-container" data-auth-id="{{ auth()->id() }}" data-partner-id="{{ $recipient->id }}"
|
<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">
|
class="text-sm flex flex-col-reverse overflow-y-scroll h-full max-h-screen pb-50 scroll-snap-y-container">
|
||||||
@forelse($messages as $message)
|
@forelse($groupedMessages as $date => $dayMessages)
|
||||||
<x-chat.message :right="$message->user_id === auth()->user()->id">{{$message->message}}</x-chat.message>
|
@foreach($dayMessages as $message)
|
||||||
@empty
|
<x-chat.message :right="$message->user_id === auth()->id()">
|
||||||
<div id="no-messages-placeholder" class="grid px-4 my-1 w-full h-full place-items-center ">
|
<p>{{ $message->message }}</p>
|
||||||
<p class="text-gray-600">No Messages Found!</p>
|
<span class="text-[10px] opacity-70 block text-right mt-1">
|
||||||
|
{{ $message->created_at->format('g:i A') }}
|
||||||
|
</span>
|
||||||
|
</x-chat.message>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
{{-- The Date Divider --}}
|
||||||
|
<div class="flex justify-center my-4 top-0">
|
||||||
|
<span class="bg-gray-200 text-gray-600 text-xs px-3 py-1 rounded-full shadow-sm">
|
||||||
|
{{ $date }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@empty
|
||||||
|
<div id="no-messages-placeholder">No Messages Found !</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
<x-chat.message-input :recipient_id="$recipient->id"/>
|
<x-chat.message-input :recipient_id="$recipient->id"/>
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
@props(['right' => false])
|
@props(['right' => false])
|
||||||
<div class="grid px-4 my-1 w-full @if($right) place-items-end @else place-items-start @endif">
|
<div class="grid px-4 my-1 w-full @if($right) place-items-end @else place-items-start @endif">
|
||||||
<div class="max-w-[40vw] py-2 px-4 @if($right) rounded-l-xl rounded-br-xl @else rounded-r-xl rounded-bl-xl @endif bg-gray-200">
|
<div class="max-w-[70vw] md:max-w-[40vw] py-2 px-4 bg-gray-200 rounded-3xl
|
||||||
|
@if($right)
|
||||||
|
rounded-br-none
|
||||||
|
@else
|
||||||
|
rounded-tl-none
|
||||||
|
@endif">
|
||||||
{{$slot}}
|
{{$slot}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
@php use App\Enums\UserTypes; @endphp
|
||||||
@props(['profileLink' => ''])
|
@props(['profileLink' => ''])
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@auth
|
@auth
|
||||||
@ -13,7 +14,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@if(auth()->check() && auth()->user()->role === \App\Enums\UserTypes::Broker->value)
|
@if(auth()->check() && auth()->user()->role === UserTypes::Broker->value)
|
||||||
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
|
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
|
||||||
<a href="{{route('broker.dashboard')}}" class="flex space-x-4">
|
<a href="{{route('broker.dashboard')}}" class="flex space-x-4">
|
||||||
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
|
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
|
||||||
@ -22,6 +23,8 @@
|
|||||||
<p>Control Panel</p>
|
<p>Control Panel</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@endif
|
||||||
|
|
||||||
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
|
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
|
||||||
<a href="{{route('chat')}}" class="flex space-x-4">
|
<a href="{{route('chat')}}" class="flex space-x-4">
|
||||||
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
|
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
|
||||||
@ -30,8 +33,6 @@
|
|||||||
<p>Chat</p>
|
<p>Chat</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@endauth
|
@endauth
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
@php use App\Services\ProfileInitialsService; @endphp
|
||||||
<x-chat.layout>
|
<x-chat.layout>
|
||||||
<x-slot:sidebarItems>
|
<x-slot:sidebarItems>
|
||||||
@forelse($inboxes as $inbox)
|
@forelse($inboxes as $inbox)
|
||||||
<x-chat.sidebar-item
|
<x-chat.sidebar-item
|
||||||
:name="$inbox->recipient->name"
|
:name="$inbox->recipient->name"
|
||||||
:link="route('chat.show', $inbox->recipient->id)"
|
:link="route('chat.show', $inbox->recipient->id)"
|
||||||
:avatar="(new \App\Services\ProfileInitialsService)->create($inbox->recipient->name)"
|
:avatar="(new ProfileInitialsService)->create($inbox->recipient->name)"
|
||||||
:message="$inbox->last_message"/>
|
:message="$inbox->last_message"/>
|
||||||
@empty
|
@empty
|
||||||
No chats found !
|
No chats found !
|
||||||
@ -20,4 +21,5 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<x-ui.toast/>
|
||||||
</x-chat.layout>
|
</x-chat.layout>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user