feat: Implement real-time chat with Reverb

This commit is contained in:
Daan Meijer 2026-06-15 00:44:55 +02:00
parent f8ee8165ff
commit 9cb87b61ef
13 changed files with 1267 additions and 5 deletions

View File

@ -14,6 +14,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/fortify (FORTIFY) - v1 - laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13 - laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0 - laravel/prompts (PROMPTS) - v0
- laravel/reverb (REVERB) - v1
- laravel/wayfinder (WAYFINDER) - v0 - laravel/wayfinder (WAYFINDER) - v0
- larastan/larastan (LARASTAN) - v3 - larastan/larastan (LARASTAN) - v3
- laravel/boost (BOOST) - v2 - laravel/boost (BOOST) - v2

View File

@ -0,0 +1,44 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
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, Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('chats.'.$this->message->chat_id),
];
}
public function broadcastWith(): array
{
return [
'message' => $this->message->load('user'),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Http\Requests\StoreMessageRequest; use App\Http\Requests\StoreMessageRequest;
use App\Models\Chat; use App\Models\Chat;
use App\Models\Message; use App\Models\Message;
@ -30,11 +31,13 @@ class MessageController extends Controller
*/ */
public function store(StoreMessageRequest $request, Chat $chat) public function store(StoreMessageRequest $request, Chat $chat)
{ {
$chat->messages()->create([ $message = $chat->messages()->create([
...$request->validated(), ...$request->validated(),
'user_id' => $request->user()->id, 'user_id' => $request->user()->id,
]); ]);
broadcast(new MessageSent($message));
return redirect()->back(); return redirect()->back();
} }

View File

@ -12,6 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {

View File

@ -14,6 +14,7 @@
"laravel/chisel": "^0.1.0", "laravel/chisel": "^0.1.0",
"laravel/fortify": "^1.37.2", "laravel/fortify": "^1.37.2",
"laravel/framework": "^13.7", "laravel/framework": "^13.7",
"laravel/reverb": "^1.10",
"laravel/tinker": "^3.0", "laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.14" "laravel/wayfinder": "^0.1.14"
}, },
@ -116,4 +117,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

907
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "02ce7e91b13dfc88fdfdf24fe7f23e66", "content-hash": "eb506fd975e79f12430ecb1c6cd824ac",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -190,6 +190,136 @@
], ],
"time": "2024-02-09T16:56:22+00:00" "time": "2024-02-09T16:56:22+00:00"
}, },
{
"name": "clue/redis-protocol",
"version": "v0.3.2",
"source": {
"type": "git",
"url": "https://github.com/clue/redis-protocol.git",
"reference": "6f565332f5531b7722d1e9c445314b91862f6d6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c",
"reference": "6f565332f5531b7722d1e9c445314b91862f6d6c",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\Redis\\Protocol\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.",
"homepage": "https://github.com/clue/redis-protocol",
"keywords": [
"parser",
"protocol",
"redis",
"resp",
"serializer",
"streaming"
],
"support": {
"issues": "https://github.com/clue/redis-protocol/issues",
"source": "https://github.com/clue/redis-protocol/tree/v0.3.2"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2024-08-07T11:06:28+00:00"
},
{
"name": "clue/redis-react",
"version": "v2.8.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-redis.git",
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca",
"reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca",
"shasum": ""
},
"require": {
"clue/redis-protocol": "^0.3.2",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.0 || ^1.1",
"react/promise-timer": "^1.11",
"react/socket": "^1.16"
},
"require-dev": {
"clue/block-react": "^1.5",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\React\\Redis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "Async Redis client implementation, built on top of ReactPHP.",
"homepage": "https://github.com/clue/reactphp-redis",
"keywords": [
"async",
"client",
"database",
"reactphp",
"redis"
],
"support": {
"issues": "https://github.com/clue/reactphp-redis/issues",
"source": "https://github.com/clue/reactphp-redis/tree/v2.8.0"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2025-01-03T16:18:33+00:00"
},
{ {
"name": "dasprid/enum", "name": "dasprid/enum",
"version": "1.0.7", "version": "1.0.7",
@ -661,6 +791,53 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "evenement/evenement",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/igorw/evenement.git",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^9 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Evenement\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
}
],
"description": "Événement is a very simple event dispatching library for PHP",
"keywords": [
"event-dispatcher",
"event-emitter"
],
"support": {
"issues": "https://github.com/igorw/evenement/issues",
"source": "https://github.com/igorw/evenement/tree/v3.0.2"
},
"time": "2023-08-08T05:53:35+00:00"
},
{ {
"name": "fruitcake/php-cors", "name": "fruitcake/php-cors",
"version": "v1.4.0", "version": "v1.4.0",
@ -1751,6 +1928,85 @@
}, },
"time": "2026-05-19T00:47:18+00:00" "time": "2026-05-19T00:47:18+00:00"
}, },
{
"name": "laravel/reverb",
"version": "v1.10.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/reverb.git",
"reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/reverb/zipball/43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac",
"reference": "43a5c0a99b1aaba33dc32f97fcf51f182dd8c8ac",
"shasum": ""
},
"require": {
"clue/redis-react": "^2.6",
"guzzlehttp/psr7": "^2.6",
"illuminate/console": "^10.47|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.47|^11.0|^12.0|^13.0",
"illuminate/http": "^10.47|^11.0|^12.0|^13.0",
"illuminate/support": "^10.47|^11.0|^12.0|^13.0",
"laravel/prompts": "^0.1.15|^0.2.0|^0.3.0",
"php": "^8.2",
"pusher/pusher-php-server": "^7.2",
"ratchet/rfc6455": "^0.4",
"react/promise-timer": "^1.10",
"react/socket": "^1.14",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/http-foundation": "^6.3|^7.0|^8.0"
},
"require-dev": {
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"pestphp/pest": "^2.0|^3.0|^4.0",
"phpstan/phpstan": "^1.10",
"ratchet/pawl": "^0.4.1",
"react/async": "^4.2",
"react/http": "^1.9"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Reverb\\ApplicationManagerServiceProvider",
"Laravel\\Reverb\\ReverbServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Reverb\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Joe Dixon",
"email": "joe@laravel.com"
}
],
"description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.",
"keywords": [
"WebSockets",
"laravel",
"real-time",
"websocket"
],
"support": {
"issues": "https://github.com/laravel/reverb/issues",
"source": "https://github.com/laravel/reverb/tree/v1.10.2"
},
"time": "2026-05-10T15:47:52+00:00"
},
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
"version": "v2.0.13", "version": "v2.0.13",
@ -3924,6 +4180,66 @@
}, },
"time": "2026-05-23T13:41:31+00:00" "time": "2026-05-23T13:41:31+00:00"
}, },
{
"name": "pusher/pusher-php-server",
"version": "7.2.8",
"source": {
"type": "git",
"url": "https://github.com/pusher/pusher-http-php.git",
"reference": "4aa139ed2a2a805cd265449b691198beee1309d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/4aa139ed2a2a805cd265449b691198beee1309d2",
"reference": "4aa139ed2a2a805cd265449b691198beee1309d2",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.2",
"php": "^7.3|^8.0",
"psr/log": "^1.0|^2.0|^3.0"
},
"require-dev": {
"overtrue/phplint": "^2.3",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Pusher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Library for interacting with the Pusher REST API",
"keywords": [
"events",
"messaging",
"php-pusher-server",
"publish",
"push",
"pusher",
"real time",
"real-time",
"realtime",
"rest",
"trigger"
],
"support": {
"issues": "https://github.com/pusher/pusher-http-php/issues",
"source": "https://github.com/pusher/pusher-http-php/tree/7.2.8"
},
"time": "2026-05-18T13:11:36+00:00"
},
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
"version": "3.0.3", "version": "3.0.3",
@ -4122,6 +4438,595 @@
}, },
"time": "2025-12-14T04:43:48+00:00" "time": "2025-12-14T04:43:48+00:00"
}, },
{
"name": "ratchet/rfc6455",
"version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/ratchetphp/RFC6455.git",
"reference": "9b05f371219cbaf9748b505f139617dd0715592b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/9b05f371219cbaf9748b505f139617dd0715592b",
"reference": "9b05f371219cbaf9748b505f139617dd0715592b",
"shasum": ""
},
"require": {
"php": ">=7.4",
"psr/http-factory-implementation": "^1.0",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"guzzlehttp/psr7": "^2.7",
"phpunit/phpunit": "^9.5",
"react/socket": "^1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Ratchet\\RFC6455\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"role": "Developer"
},
{
"name": "Matt Bonneau",
"role": "Developer"
}
],
"description": "RFC6455 WebSocket protocol handler",
"homepage": "http://socketo.me",
"keywords": [
"WebSockets",
"rfc6455",
"websocket"
],
"support": {
"chat": "https://gitter.im/reactphp/reactphp",
"issues": "https://github.com/ratchetphp/RFC6455/issues",
"source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.1"
},
"time": "2026-06-06T14:34:23+00:00"
},
{
"name": "react/cache",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/cache.git",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": [
"cache",
"caching",
"promise",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/cache/issues",
"source": "https://github.com/reactphp/cache/tree/v1.2.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2022-11-30T15:59:55+00:00"
},
{
"name": "react/dns",
"version": "v1.14.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/dns.git",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async DNS resolver for ReactPHP",
"keywords": [
"async",
"dns",
"dns-resolver",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/dns/issues",
"source": "https://github.com/reactphp/dns/tree/v1.14.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-18T19:34:28+00:00"
},
{
"name": "react/event-loop",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/event-loop.git",
"reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
"reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"suggest": {
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\EventLoop\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
"keywords": [
"asynchronous",
"event-loop"
],
"support": {
"issues": "https://github.com/reactphp/event-loop/issues",
"source": "https://github.com/reactphp/event-loop/tree/v1.6.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-17T20:46:25+00:00"
},
{
"name": "react/promise",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"phpstan/phpstan": "1.12.28 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"keywords": [
"promise",
"promises"
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
"source": "https://github.com/reactphp/promise/tree/v3.3.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-08-19T18:57:03+00:00"
},
{
"name": "react/promise-timer",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise-timer.git",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1",
"reference": "4f70306ed66b8b44768941ca7f142092600fafc1",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\Timer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
"homepage": "https://github.com/reactphp/promise-timer",
"keywords": [
"async",
"event-loop",
"promise",
"reactphp",
"timeout",
"timer"
],
"support": {
"issues": "https://github.com/reactphp/promise-timer/issues",
"source": "https://github.com/reactphp/promise-timer/tree/v1.11.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-04T14:27:45+00:00"
},
{
"name": "react/socket",
"version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/socket.git",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/dns": "^1.13",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3.3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Socket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
"keywords": [
"Connection",
"Socket",
"async",
"reactphp",
"stream"
],
"support": {
"issues": "https://github.com/reactphp/socket/issues",
"source": "https://github.com/reactphp/socket/tree/v1.17.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-19T20:47:34+00:00"
},
{
"name": "react/stream",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/stream.git",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.8",
"react/event-loop": "^1.2"
},
"require-dev": {
"clue/stream-filter": "~1.2",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
"keywords": [
"event-driven",
"io",
"non-blocking",
"pipe",
"reactphp",
"readable",
"stream",
"writable"
],
"support": {
"issues": "https://github.com/reactphp/stream/issues",
"source": "https://github.com/reactphp/stream/tree/v1.4.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-11T12:45:25+00:00"
},
{ {
"name": "spomky-labs/cbor-php", "name": "spomky-labs/cbor-php",
"version": "3.2.3", "version": "3.2.3",

82
config/broadcasting.php Normal file
View File

@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

102
config/reverb.php Normal file
View File

@ -0,0 +1,102 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'),
'rate_limiting' => [
'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false),
'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60),
'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60),
'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false),
],
],
],
],
];

89
package-lock.json generated
View File

@ -19,10 +19,12 @@
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-input-otp": "^0.3.2", "vue-input-otp": "^0.3.2",
"vue-sonner": "^2.0.0" "vue-sonner": "^2.0.0",
"ziggy-js": "^2.6.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@laravel/echo-vue": "^2.3.7",
"@laravel/vite-plugin-wayfinder": "^0.1.3", "@laravel/vite-plugin-wayfinder": "^0.1.3",
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
@ -35,8 +37,10 @@
"eslint-import-resolver-typescript": "^4.4.4", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"laravel-echo": "^2.3.7",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"pusher-js": "^8.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^8.23.0", "typescript-eslint": "^8.23.0",
"vite": "^8.0.0", "vite": "^8.0.0",
@ -573,6 +577,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@laravel/echo-vue": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/@laravel/echo-vue/-/echo-vue-2.3.7.tgz",
"integrity": "sha512-52lOuge5JdGKjGs1bgzrgbubh6mGATTB2kpQsf4TVXXQez0xF3joPcSoyFv9YAqcn8wxwxUdm2dStTsqfL0r2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"pusher-js": {
"optional": true
},
"socket.io-client": {
"optional": true
}
}
},
"node_modules/@laravel/passkeys": { "node_modules/@laravel/passkeys": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@laravel/passkeys/-/passkeys-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@laravel/passkeys/-/passkeys-0.2.0.tgz",
@ -4608,6 +4635,28 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/laravel-echo": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.7.tgz",
"integrity": "sha512-6NoPtOk16PuaykVgcV1MV5665VPtrbyvacBD6AJ8NJdRjTwrOwKrgOYHgq4bz5E1zUbbVi2UJfMN7P7D/yceyQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
},
"peerDependenciesMeta": {
"pusher-js": {
"optional": true
},
"socket.io-client": {
"optional": true
}
}
},
"node_modules/laravel-precognition": { "node_modules/laravel-precognition": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-2.0.0.tgz", "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-2.0.0.tgz",
@ -5550,6 +5599,28 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pusher-js": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz",
"integrity": "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/qs-esm": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
"integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -6322,6 +6393,13 @@
"url": "https://github.com/sponsors/Wombosvideo" "url": "https://github.com/sponsors/Wombosvideo"
} }
}, },
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -6942,6 +7020,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/ziggy-js": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.2.tgz",
"integrity": "sha512-41xc9wRvv5Olh8pZjKSaL5vDAjw4BfTDMFoeLwRpDGc2B+uqcdwIKd81EHs6uwpqahdRrU7uMab0xWj0hBSDjg==",
"license": "MIT",
"dependencies": {
"qs-esm": "^7.0.3"
}
} }
} }
} }

View File

@ -14,6 +14,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@laravel/echo-vue": "^2.3.7",
"@laravel/vite-plugin-wayfinder": "^0.1.3", "@laravel/vite-plugin-wayfinder": "^0.1.3",
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
@ -26,8 +27,10 @@
"eslint-import-resolver-typescript": "^4.4.4", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"laravel-echo": "^2.3.7",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"pusher-js": "^8.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^8.23.0", "typescript-eslint": "^8.23.0",
"vite": "^8.0.0", "vite": "^8.0.0",
@ -48,7 +51,8 @@
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-input-otp": "^0.3.2", "vue-input-otp": "^0.3.2",
"vue-sonner": "^2.0.0" "vue-sonner": "^2.0.0",
"ziggy-js": "^2.6.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5", "@rollup/rollup-linux-x64-gnu": "4.9.5",

View File

@ -4,6 +4,15 @@ import AppLayout from '@/layouts/AppLayout.vue';
import AuthLayout from '@/layouts/AuthLayout.vue'; import AuthLayout from '@/layouts/AuthLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue'; import SettingsLayout from '@/layouts/settings/Layout.vue';
import { initializeFlashToast } from '@/lib/flashToast'; import { initializeFlashToast } from '@/lib/flashToast';
import { configureEcho } from '@laravel/echo-vue';
configureEcho({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
host: import.meta.env.VITE_REVERB_HOST,
port: import.meta.env.VITE_REVERB_PORT,
scheme: import.meta.env.VITE_REVERB_SCHEME,
});
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

View File

@ -1,6 +1,8 @@
<script setup> <script setup>
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import { onMounted } from 'vue';
import { useEcho } from '@laravel/echo-vue';
const props = defineProps({ const props = defineProps({
chat: Object, chat: Object,
@ -10,6 +12,15 @@ const form = useForm({
content: '', content: '',
}); });
const echo = useEcho();
onMounted(() => {
echo.private(`chats.${props.chat.id}`)
.listen('MessageSent', (e) => {
props.chat.messages.push(e.message);
});
});
function submit() { function submit() {
form.post(route('chats.messages.store', props.chat.id), { form.post(route('chats.messages.store', props.chat.id), {
onSuccess: () => form.reset(), onSuccess: () => form.reset(),

12
routes/channels.php Normal file
View File

@ -0,0 +1,12 @@
<?php
use App\Models\Chat;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('chats.{chat}', function ($user, Chat $chat) {
return $user->can('view', $chat->chatable);
});