Skip to content

Commit 3dedafc

Browse files
committed
feat(phase-11): real-time WebSockets via Laravel Reverb + Echo
Backend: - Install laravel/reverb; configure broadcasting.php and channels.php - NewChatMessage event — broadcasts on private chat-session.{id} - NewDiscussMessage event — broadcasts on private discuss-channel.{id} - ErpNotification event — broadcasts on private tenant.{tenantId} - LiveChatController::sendMessage() dispatches NewChatMessage - DiscussController::sendMessage() dispatches NewDiscussMessage - Channel auth in routes/channels.php (session, discuss, tenant scopes) Frontend: - Install laravel-echo + pusher-js; wire Echo into bootstrap.ts - useEchoPrivateChannel() hook for declarative channel subscriptions - LiveChat/Sessions/Show.tsx: real-time message streaming + auto-scroll - Discuss/Show.tsx: receive other-user messages via Echo (dedup by id) - global.d.ts: declare Window.Echo and Window.Pusher types Tests (5/5 passing): - Event payload and channel assertions for all 3 broadcast events - Controller-level dispatch assertions using Event::fake() Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f9ac16b commit 3dedafc

15 files changed

Lines changed: 1569 additions & 60 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Events\Discuss;
4+
5+
use App\Modules\Discuss\Models\DiscussMessage;
6+
use Illuminate\Broadcasting\InteractsWithSockets;
7+
use Illuminate\Broadcasting\PrivateChannel;
8+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
9+
use Illuminate\Foundation\Events\Dispatchable;
10+
use Illuminate\Queue\SerializesModels;
11+
12+
class NewDiscussMessage implements ShouldBroadcast
13+
{
14+
use Dispatchable, InteractsWithSockets, SerializesModels;
15+
16+
public function __construct(public readonly DiscussMessage $message) {}
17+
18+
public function broadcastOn(): array
19+
{
20+
return [
21+
new PrivateChannel('discuss-channel.' . $this->message->channel_id),
22+
];
23+
}
24+
25+
public function broadcastWith(): array
26+
{
27+
return [
28+
'id' => $this->message->id,
29+
'channel_id' => $this->message->channel_id,
30+
'user_id' => $this->message->user_id,
31+
'body' => $this->message->body,
32+
'parent_id' => $this->message->parent_id,
33+
'is_edited' => (bool) $this->message->is_edited,
34+
'created_at' => $this->message->created_at?->toIso8601String(),
35+
'user' => $this->message->relationLoaded('user')
36+
? ['id' => $this->message->user->id, 'name' => $this->message->user->name]
37+
: null,
38+
];
39+
}
40+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Events\LiveChat;
4+
5+
use App\Modules\LiveChat\Models\ChatMessage;
6+
use Illuminate\Broadcasting\Channel;
7+
use Illuminate\Broadcasting\InteractsWithSockets;
8+
use Illuminate\Broadcasting\PrivateChannel;
9+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
10+
use Illuminate\Foundation\Events\Dispatchable;
11+
use Illuminate\Queue\SerializesModels;
12+
13+
class NewChatMessage implements ShouldBroadcast
14+
{
15+
use Dispatchable, InteractsWithSockets, SerializesModels;
16+
17+
public function __construct(public readonly ChatMessage $message) {}
18+
19+
public function broadcastOn(): array
20+
{
21+
return [
22+
new PrivateChannel('chat-session.' . $this->message->session_id),
23+
];
24+
}
25+
26+
public function broadcastWith(): array
27+
{
28+
return [
29+
'id' => $this->message->id,
30+
'session_id' => $this->message->session_id,
31+
'sender_type' => $this->message->sender_type,
32+
'agent_id' => $this->message->agent_id,
33+
'message' => $this->message->message,
34+
'created_at' => $this->message->created_at?->toIso8601String(),
35+
];
36+
}
37+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Events\Notifications;
4+
5+
use Illuminate\Broadcasting\InteractsWithSockets;
6+
use Illuminate\Broadcasting\PrivateChannel;
7+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
8+
use Illuminate\Foundation\Events\Dispatchable;
9+
use Illuminate\Queue\SerializesModels;
10+
11+
class ErpNotification implements ShouldBroadcast
12+
{
13+
use Dispatchable, InteractsWithSockets, SerializesModels;
14+
15+
public function __construct(
16+
public readonly int $tenantId,
17+
public readonly string $type,
18+
public readonly string $title,
19+
public readonly string $message,
20+
public readonly array $data = [],
21+
) {}
22+
23+
public function broadcastOn(): array
24+
{
25+
return [
26+
new PrivateChannel('tenant.' . $this->tenantId),
27+
];
28+
}
29+
30+
public function broadcastWith(): array
31+
{
32+
return [
33+
'type' => $this->type,
34+
'title' => $this->title,
35+
'message' => $this->message,
36+
'data' => $this->data,
37+
];
38+
}
39+
}

erp/app/Modules/Discuss/Http/Controllers/DiscussController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Modules\Discuss\Http\Controllers;
44

5+
use App\Events\Discuss\NewDiscussMessage;
56
use App\Http\Controllers\Controller;
67
use App\Models\User;
78
use App\Modules\Discuss\Models\DiscussChannel;
@@ -123,6 +124,8 @@ public function sendMessage(Request $request, DiscussChannel $channel): JsonResp
123124

124125
$message->load('user');
125126

127+
broadcast(new NewDiscussMessage($message))->toOthers();
128+
126129
return response()->json([
127130
'id' => $message->id,
128131
'body' => $message->body,

erp/app/Modules/LiveChat/Http/Controllers/LiveChatController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Modules\LiveChat\Http\Controllers;
44

5+
use App\Events\LiveChat\NewChatMessage;
56
use App\Http\Controllers\Controller;
67
use App\Modules\LiveChat\Models\ChatChannel;
78
use App\Modules\LiveChat\Models\ChatMessage;
@@ -107,7 +108,7 @@ public function sendMessage(Request $request, ChatSession $session): RedirectRes
107108
'message' => 'required|string',
108109
]);
109110

110-
ChatMessage::create([
111+
$msg = ChatMessage::create([
111112
'tenant_id' => $session->tenant_id,
112113
'session_id' => $session->id,
113114
'sender_type' => 'agent',
@@ -117,6 +118,8 @@ public function sendMessage(Request $request, ChatSession $session): RedirectRes
117118

118119
$session->update(['last_message_at' => now()]);
119120

121+
broadcast(new NewChatMessage($msg))->toOthers();
122+
120123
return redirect()->back()->with('success', 'Message sent.');
121124
}
122125

erp/composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
"name": "laravel/laravel",
44
"type": "project",
55
"description": "The skeleton application for the Laravel framework.",
6-
"keywords": ["laravel", "framework"],
6+
"keywords": [
7+
"laravel",
8+
"framework"
9+
],
710
"license": "MIT",
811
"require": {
912
"php": "^8.3",
1013
"barryvdh/laravel-dompdf": "^3.1",
1114
"inertiajs/inertia-laravel": "^2.0",
1215
"laravel/framework": "^13.8",
16+
"laravel/reverb": "^1.10",
1317
"laravel/sanctum": "^4.0",
1418
"laravel/tinker": "^3.0",
1519
"pragmarx/google2fa-laravel": "^3.0",

0 commit comments

Comments
 (0)