From 1baabb76496b5c46e9259c6c0f5378560b230589 Mon Sep 17 00:00:00 2001 From: Ghostie Date: Thu, 9 Jan 2025 21:16:27 -0500 Subject: [PATCH] started working in notifications --- .env.example | 14 +- TODO.md | 10 +- app/Events/AP/ActivityUndoEvent.php | 62 + app/Events/NoteLikedEvent.php | 34 + app/Events/UserFollowedEvent.php | 34 + app/Events/UserUnfollowedEvent.php | 33 + app/Http/Controllers/AP/APInboxController.php | 19 +- .../AP/APInstanceInboxController.php | 13 +- app/Listeners/NoteLikedListener.php | 46 + app/Listeners/UserFollowedListener.php | 49 + app/Listeners/UserUnfollowedListener.php | 48 + app/Notifications/UserNotification.php | 65 ++ app/Types/TypeNote.php | 16 +- bootstrap/app.php | 1 + composer.json | 1 + composer.lock | 1000 ++++++++++++++++- config/broadcasting.php | 82 ++ config/reverb.php | 92 ++ ...1_10_012709_create_notifications_table.php | 31 + package-lock.json | 26 + package.json | 2 + public/resources/sounds/notification.mp3 | Bin 0 -> 46937 bytes resources/js/bootstrap.js | 8 + resources/js/echo.js | 14 + resources/views/partials/layout.blade.php | 23 + routes/channels.php | 7 + test.php | 9 + 27 files changed, 1718 insertions(+), 21 deletions(-) create mode 100644 app/Events/AP/ActivityUndoEvent.php create mode 100644 app/Events/NoteLikedEvent.php create mode 100644 app/Events/UserFollowedEvent.php create mode 100644 app/Events/UserUnfollowedEvent.php create mode 100644 app/Listeners/NoteLikedListener.php create mode 100644 app/Listeners/UserFollowedListener.php create mode 100644 app/Listeners/UserUnfollowedListener.php create mode 100644 app/Notifications/UserNotification.php create mode 100644 config/broadcasting.php create mode 100644 config/reverb.php create mode 100644 database/migrations/2025_01_10_012709_create_notifications_table.php create mode 100644 public/resources/sounds/notification.mp3 create mode 100644 resources/js/echo.js create mode 100644 routes/channels.php create mode 100644 test.php diff --git a/.env.example b/.env.example index 7d50a2d..ea0ef23 100644 --- a/.env.example +++ b/.env.example @@ -36,7 +36,7 @@ SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null -BROADCAST_CONNECTION=log +BROADCAST_CONNECTION=reverb FILESYSTEM_DISK=local QUEUE_CONNECTION=database @@ -66,3 +66,15 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +REVERB_APP_ID= +REVERB_APP_KEY= +REVERB_APP_SECRET= +REVERB_HOST="localhost" +REVERB_PORT=8080 +REVERB_SCHEME=http + +VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" +VITE_REVERB_HOST="${REVERB_HOST}" +VITE_REVERB_PORT="${REVERB_PORT}" +VITE_REVERB_SCHEME="${REVERB_SCHEME}" diff --git a/TODO.md b/TODO.md index 8858320..33ef283 100644 --- a/TODO.md +++ b/TODO.md @@ -16,10 +16,10 @@ - [x] Boosts - [x] Local Boost - [x] Tags - - [ ] Mentions - - [ ] Local mentions + - [x] Mentions + - [x] Local mentions - [ ] Private post - - [ ] Pinned Posts + - [x] Pinned Posts - [x] Nodeinfo - [ ] Notifications @@ -31,7 +31,7 @@ - [x] Update profile picture - [ ] Mark account as private (in federation manual approval is needed) - [x] Allow custom CSS - - [ ] Profile audio + - [x] Profile audio - [ ] Top 8 friends - [x] Friends (they are known as follows in the activitypub protocol) - [x] Add friends @@ -67,4 +67,4 @@ - [x] Use jobs when posting activities - [ ] Sign the get activities for mastodon when secure mode is enable - [x] Set a minimum font size for the tags cloud - - [ ] Pagination, pagination, AND MORE PAGINATION + - [x] Pagination, pagination, AND MORE PAGINATION diff --git a/app/Events/AP/ActivityUndoEvent.php b/app/Events/AP/ActivityUndoEvent.php new file mode 100644 index 0000000..f5e94a2 --- /dev/null +++ b/app/Events/AP/ActivityUndoEvent.php @@ -0,0 +1,62 @@ +activity = $activity; + + $this->actor = TypeActor::actor_exists_or_obtain ($activity ["actor"]); + + $child_activity = $activity ["object"]; + $child_activity_id = ""; + + if (is_array ($child_activity)) + $child_activity_id = $child_activity ["id"]; + else + $child_activity_id = $child_activity; + + if (!TypeActivity::activity_exists ($child_activity_id)) + return ["error" => "Activity not found",]; + + $child_activity = Activity::where ("activity_id", $child_activity_id)->first (); + $this->object = $child_activity; + + switch ($this->object->type) + { + case "Follow": + $unfollowed_actor = Actor::where ("actor_id", $this->object->object)->first (); + UserUnfollowedEvent::dispatch ($this->object, $this->actor, $unfollowed_actor); + break; + } + + $child_activity->delete (); + } +} diff --git a/app/Events/NoteLikedEvent.php b/app/Events/NoteLikedEvent.php new file mode 100644 index 0000000..19aa12a --- /dev/null +++ b/app/Events/NoteLikedEvent.php @@ -0,0 +1,34 @@ +activity = $activity; + $this->actor = $actor; + $this->note = $note; + } +} diff --git a/app/Events/UserFollowedEvent.php b/app/Events/UserFollowedEvent.php new file mode 100644 index 0000000..1014757 --- /dev/null +++ b/app/Events/UserFollowedEvent.php @@ -0,0 +1,34 @@ +activity = $activity; + $this->actor = $actor; + $this->object = $object; + } +} diff --git a/app/Events/UserUnfollowedEvent.php b/app/Events/UserUnfollowedEvent.php new file mode 100644 index 0000000..32514ee --- /dev/null +++ b/app/Events/UserUnfollowedEvent.php @@ -0,0 +1,33 @@ +activity = $activity; + $this->actor = $actor; + $this->object = $object; + } +} diff --git a/app/Http/Controllers/AP/APInboxController.php b/app/Http/Controllers/AP/APInboxController.php index 6f4b1a4..ef43602 100644 --- a/app/Http/Controllers/AP/APInboxController.php +++ b/app/Http/Controllers/AP/APInboxController.php @@ -13,6 +13,11 @@ use App\Models\Like; use App\Types\TypeActor; use App\Types\TypeActivity; +use App\Events\UserFollowedEvent; +use App\Events\NoteLikedEvent; + +use App\Events\AP\ActivityUndoEvent; + use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use App\Http\Controllers\Controller; @@ -66,14 +71,9 @@ class APInboxController extends Controller return response ()->json (["error" => "Follow already exists",], 409); $activity ["activity_id"] = $activity ["id"]; - $act = Activity::create ($activity); - $follow = Follow::create ([ - "activity_id" => $act->id, - "actor" => $actor->id, - "object" => $target->id, - ]); + UserFollowedEvent::dispatch ($act, $actor, $target); // TODO: Users should be able to manually check this $accept_activity = TypeActivity::craft_accept ($act); @@ -88,6 +88,7 @@ class APInboxController extends Controller public function handle_undo (User $user, $activity) { + ActivityUndoEvent::dispatch ($activity, $activity); return response ()->json (ActionsActivity::activity_undo ($activity)); } @@ -114,11 +115,7 @@ class APInboxController extends Controller else $act = Activity::where ("activity_id", $activity ["id"])->first (); - $like = Like::create ([ - "activity_id" => $act->id, - "actor_id" => $actor->id, - "note_id" => $note->id, - ]); + NoteLikedEvent::dispatch ($act, $actor, $note); return response ()->json (["success" => "Like created",], 200); } diff --git a/app/Http/Controllers/AP/APInstanceInboxController.php b/app/Http/Controllers/AP/APInstanceInboxController.php index 8d5ef37..0aa201b 100644 --- a/app/Http/Controllers/AP/APInstanceInboxController.php +++ b/app/Http/Controllers/AP/APInstanceInboxController.php @@ -2,20 +2,23 @@ namespace App\Http\Controllers\AP; -use App\Actions\ActionsActivity; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use App\Models\Actor; use App\Models\Activity; use App\Models\Announcement; +use App\Models\ProfilePin; use App\Types\TypeActor; use App\Types\TypeActivity; use App\Types\TypeNote; +use App\Actions\ActionsActivity; + use App\Http\Controllers\Controller; -use App\Models\ProfilePin; + +use App\Notifications\UserNotification; class APInstanceInboxController extends Controller { @@ -93,6 +96,12 @@ class APInstanceInboxController extends Controller "note_id" => $note_exists->id ]); + $note_actor = $note_exists->get_actor ()->first (); + if ($note_actor->user) + { + $note_actor->user->notify (new UserNotification("Boost", $announcement_actor, $note_exists)); + } + return response ()->json (["status" => "ok"]); } diff --git a/app/Listeners/NoteLikedListener.php b/app/Listeners/NoteLikedListener.php new file mode 100644 index 0000000..a6f799f --- /dev/null +++ b/app/Listeners/NoteLikedListener.php @@ -0,0 +1,46 @@ + $event->activity->id, + "actor_id" => $event->actor->id, + "note_id" => $event->note->id + ]); + + $user = $event->note->get_actor ()->first ()->user; + if (!$user) + return; + + $user->notify (new UserNotification( + "Like", + $event->actor->id, + $event->note->id, + $event->activity->id + )); + } +} diff --git a/app/Listeners/UserFollowedListener.php b/app/Listeners/UserFollowedListener.php new file mode 100644 index 0000000..cae76de --- /dev/null +++ b/app/Listeners/UserFollowedListener.php @@ -0,0 +1,49 @@ + $event->activity->id, + "actor" => $event->actor->id, + "object" => $event->object->id + ]); + + $user = $event->object->user; + if (!$user) + return; + + $user->notify (new UserNotification ( + "Follow", + $event->actor->id, + $event->object->id, + $event->activity->id + )); + } +} diff --git a/app/Listeners/UserUnfollowedListener.php b/app/Listeners/UserUnfollowedListener.php new file mode 100644 index 0000000..0abdb9e --- /dev/null +++ b/app/Listeners/UserUnfollowedListener.php @@ -0,0 +1,48 @@ +actor->id) + ->where("object_id", $event->object->id) + ->first(); + + if ($follow_exists) + $follow_exists->delete (); + + $user = $event->object->user; + if (!$user) + return; + + $user->notify(new UserNotification( + "Unfollow", + $event->actor->id, + $event->object->id, + $event->activity->id + )); + } +} diff --git a/app/Notifications/UserNotification.php b/app/Notifications/UserNotification.php new file mode 100644 index 0000000..6e38602 --- /dev/null +++ b/app/Notifications/UserNotification.php @@ -0,0 +1,65 @@ +type = $type; + $this->actor = $actor; + $this->object = $object; + $this->activity = $activity; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(object $notifiable): array + { + return ['database', 'broadcast']; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'type' => $this->type, + 'actor' => $this->actor, + 'object' => $this->object, + 'activity' => $this->activity, + ]; + } + + public function toBroadcast ($notifiable) + { + // we don't really need to broadcast any information + return [ + "notification_type" => $this->type, + "actor" => $this->actor, + "object" => $this->object, + "activity" => $this->activity, + ]; + } +} diff --git a/app/Types/TypeNote.php b/app/Types/TypeNote.php index b5eeafb..e36fc31 100644 --- a/app/Types/TypeNote.php +++ b/app/Types/TypeNote.php @@ -8,6 +8,7 @@ use App\Models\Actor; use App\Models\Activity; use App\Models\NoteAttachment; use App\Models\NoteMention; +use App\Notifications\UserNotification; use GuzzleHttp\Client; use Illuminate\Support\Facades\Log; @@ -187,19 +188,23 @@ class TypeNote $mention_actor = null; $actor_exists = null; - Log::info (json_encode ($exploded_name)); + $is_local = false; if (count ($exploded_name) == 2) { // let's check if maybe it's local $actor_exists = Actor::where ("preferredUsername", $exploded_name [1])->first (); if (!$actor_exists) continue; + + $is_local = true; } else if (count ($exploded_name) == 3) { // maybe it's remote $actor_exists = TypeActor::actor_exists_or_obtain_from_handle($exploded_name [1], $exploded_name [2]); + if ($actor_exists->user) + $is_local = true; } else continue; @@ -208,6 +213,15 @@ class TypeNote "note_id" => $note->id, "actor_id" => $actor_exists->id ]); + + if ($is_local) + { + $actor_exists->user->notify (new UserNotification( + "Mention", + $actor, + $actor_exists + )); + } break; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 3afe501..e6a7b5b 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -10,6 +10,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { diff --git a/composer.json b/composer.json index 72112bb..98da26e 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "guzzlehttp/guzzle": "^7.9", "intervention/image": "^3.10", "laravel/framework": "^11.31", + "laravel/reverb": "^1.0", "laravel/tinker": "^2.9", "league/html-to-markdown": "^5.1", "predis/predis": "^2.3" diff --git a/composer.lock b/composer.lock index 8aa745e..5448bd5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "86986bb5e6245e2f122938332d651d64", + "content-hash": "85664129adb9aca87569d3e717e34c4f", "packages": [ { "name": "brick/math", @@ -135,6 +135,136 @@ ], "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": "dflydev/dot-access-data", "version": "v3.0.3", @@ -663,6 +793,53 @@ ], "time": "2023-10-06T06:47:41+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", "version": "v1.3.0", @@ -1625,6 +1802,88 @@ }, "time": "2024-11-12T14:59:47+00:00" }, + { + "name": "laravel/reverb", + "version": "v1.4.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "22eab51c56c9a06e26a892b57e9b9d392e4e1da0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/22eab51c56c9a06e26a892b57e9b9d392e4e1da0", + "reference": "22eab51c56c9a06e26a892b57e9b9d392e4e1da0", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0", + "illuminate/contracts": "^10.47|^11.0", + "illuminate/http": "^10.47|^11.0", + "illuminate/support": "^10.47|^11.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.3.1", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0", + "symfony/http-foundation": "^6.3|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Output": "Laravel\\Reverb\\Output" + }, + "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.4.4" + }, + "time": "2024-12-06T18:56:46+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.1", @@ -2894,6 +3153,97 @@ ], "time": "2024-11-21T10:39:51+00:00" }, + { + "name": "paragonie/sodium_compat", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" + }, + "time": "2024-09-04T12:51:01+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -3570,6 +3920,67 @@ }, "time": "2024-12-10T01:58:33+00:00" }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "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.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3795,6 +4206,593 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "ratchet/rfc6455", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "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.3.1" + }, + "time": "2021-12-09T23:20:49+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.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "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.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "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.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 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.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+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.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "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.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+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": "symfony/clock", "version": "v7.2.0", diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..ebc3fb9 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,82 @@ + 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', + ], + + ], + +]; diff --git a/config/reverb.php b/config/reverb.php new file mode 100644 index 0000000..3fead53 --- /dev/null +++ b/config/reverb.php @@ -0,0 +1,92 @@ + 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), + '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'), + ], + ], + '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_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + ], + ], + + ], + +]; diff --git a/database/migrations/2025_01_10_012709_create_notifications_table.php b/database/migrations/2025_01_10_012709_create_notifications_table.php new file mode 100644 index 0000000..d738032 --- /dev/null +++ b/database/migrations/2025_01_10_012709_create_notifications_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/package-lock.json b/package-lock.json index d200175..afe6858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,10 @@ "autoprefixer": "^10.4.20", "axios": "^1.7.4", "concurrently": "^9.0.1", + "laravel-echo": "^1.17.1", "laravel-vite-plugin": "^1.0", "postcss": "^8.4.47", + "pusher-js": "^8.4.0-rc2", "tailwindcss": "^3.4.13", "vite": "^6.0" } @@ -1585,6 +1587,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/laravel-echo": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.17.1.tgz", + "integrity": "sha512-ORWc4vDfnBj/Oe5ThZ5kYyGItRjLDqAQUyhD/7UhehUOqc+s5x9HEBjtMVludNMP6VuXw6t7Uxt8bp63kaTofg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/laravel-vite-plugin": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.1.1.tgz", @@ -1994,6 +2005,15 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, + "node_modules/pusher-js": { + "version": "8.4.0-rc2", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz", + "integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==", + "dev": true, + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2434,6 +2454,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/package.json b/package.json index 0d10472..a6d106b 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "autoprefixer": "^10.4.20", "axios": "^1.7.4", "concurrently": "^9.0.1", + "laravel-echo": "^1.17.1", "laravel-vite-plugin": "^1.0", "postcss": "^8.4.47", + "pusher-js": "^8.4.0-rc2", "tailwindcss": "^3.4.13", "vite": "^6.0" } diff --git a/public/resources/sounds/notification.mp3 b/public/resources/sounds/notification.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a1ad34acd5b5d21a9e557143b636b23668eb67a2 GIT binary patch literal 46937 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 @vite(["resources/js/app.js"]) + + @if (auth ()->check ()) + + @endif diff --git a/routes/channels.php b/routes/channels.php new file mode 100644 index 0000000..df2ad28 --- /dev/null +++ b/routes/channels.php @@ -0,0 +1,7 @@ +id === (int) $id; +}); diff --git a/test.php b/test.php new file mode 100644 index 0000000..4e88e9e --- /dev/null +++ b/test.php @@ -0,0 +1,9 @@ +use App\Models\Actor; +use App\Models\User; + +use App\Events\UserFollowed; + +$actor = Actor::find(3); +$user = User::find(1); + +UserFollowed::dispatch ($actor, $user);