├── .editorconfig ├── .env.example ├── .env.testing ├── .env.testing.example ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Events │ ├── ChatCreated.php │ └── ChatUpdated.php ├── Http │ └── Controllers │ │ └── Controller.php ├── Livewire │ ├── Actions │ │ └── Logout.php │ ├── Chats │ │ ├── Index.php │ │ ├── ListChats.php │ │ ├── Save.php │ │ └── Show.php │ ├── Forms │ │ └── LoginForm.php │ ├── Layout │ │ └── Navigation.php │ ├── Pages │ │ ├── Auth │ │ │ ├── ConfirmPassword.php │ │ │ ├── ForgotPassword.php │ │ │ ├── Login.php │ │ │ ├── Register.php │ │ │ └── ResetPassword.php │ │ ├── Chats.php │ │ ├── Dashboard.php │ │ └── Profile.php │ ├── Profile │ │ ├── DeleteUserForm.php │ │ ├── UpdatePasswordForm.php │ │ └── UpdateProfileInformationForm.php │ ├── Rooms │ │ ├── Create.php │ │ └── Index.php │ └── Welcome │ │ └── Navigation.php ├── Models │ ├── Chat.php │ ├── Member.php │ ├── Room.php │ └── User.php └── Providers │ └── AppServiceProvider.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── database.php ├── filesystems.php ├── logging.php ├── mail.php ├── queue.php ├── reverb.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ ├── ChatFactory.php │ ├── MemberFactory.php │ ├── RoomFactory.php │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2024_09_01_114347_create_rooms_table.php │ ├── 2024_09_01_122615_create_members_table.php │ ├── 2024_09_01_132337_create_chats_table.php │ └── 2025_04_29_155057_add_deleted_at_column_to_chats_table.php └── seeders │ └── DatabaseSeeder.php ├── package-lock.json ├── package.json ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── rector.php ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ ├── bootstrap.js │ ├── echo.js │ ├── notification.js │ └── save-chat.js └── views │ ├── components │ ├── action-message.blade.php │ ├── application-logo.blade.php │ ├── auth-session-status.blade.php │ ├── danger-button.blade.php │ ├── dropdown-link.blade.php │ ├── dropdown.blade.php │ ├── icons │ │ ├── add.blade.php │ │ ├── check.blade.php │ │ ├── close.blade.php │ │ ├── edit.blade.php │ │ ├── info.blade.php │ │ ├── reply.blade.php │ │ ├── trash.blade.php │ │ ├── warning.blade.php │ │ └── x.blade.php │ ├── input-error.blade.php │ ├── input-label.blade.php │ ├── layouts │ │ ├── app.blade.php │ │ └── guest.blade.php │ ├── modal.blade.php │ ├── nav-link.blade.php │ ├── notifications.blade.php │ ├── primary-button.blade.php │ ├── responsive-nav-link.blade.php │ ├── secondary-button.blade.php │ ├── select-input.blade.php │ └── text-input.blade.php │ ├── livewire │ ├── .gitkeep │ ├── chats │ │ ├── index.blade.php │ │ ├── list-chats.blade.php │ │ ├── save.blade.php │ │ └── show.blade.php │ ├── layout │ │ └── navigation.blade.php │ ├── pages │ │ ├── auth │ │ │ ├── confirm-password.blade.php │ │ │ ├── forgot-password.blade.php │ │ │ ├── login.blade.php │ │ │ ├── register.blade.php │ │ │ └── reset-password.blade.php │ │ ├── chats.blade.php │ │ ├── dashboard.blade.php │ │ └── profile.blade.php │ ├── profile │ │ ├── delete-user-form.blade.php │ │ ├── update-password-form.blade.php │ │ └── update-profile-information-form.blade.php │ ├── rooms │ │ ├── create.blade.php │ │ └── index.blade.php │ └── welcome │ │ └── navigation.blade.php │ └── welcome.blade.php ├── routes ├── auth.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ └── RegistrationTest.php │ ├── ChatsTest.php │ ├── ProfileTest.php │ └── WelcomeTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── Events │ ├── ChatCreatedTest.php │ └── ChatUpdatedTest.php │ ├── Livewire │ ├── Chats │ │ ├── IndexTest.php │ │ ├── ListChatTest.php │ │ ├── SaveTest.php │ │ └── ShowTest.php │ ├── Layout │ │ └── NavigationTest.php │ ├── Pages │ │ └── Auth │ │ │ ├── ConfirmPasswordTest.php │ │ │ ├── ForgotPasswordTest.php │ │ │ ├── LoginTest.php │ │ │ ├── RegisterTest.php │ │ │ └── ResetPasswordTest.php │ ├── Profile │ │ ├── DeleteUserFormTest.php │ │ ├── UpdatePasswordFormTest.php │ │ └── UpdateProfileInformationFormTest.php │ └── Rooms │ │ ├── CreateTest.php │ │ └── IndexTest.php │ └── Models │ ├── ChatTest.php │ ├── MemberTest.php │ ├── RoomTest.php │ └── UserTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=sqlite 23 | # DB_HOST=127.0.0.1 24 | # DB_PORT=3306 25 | # DB_DATABASE=laravel 26 | # DB_USERNAME=root 27 | # DB_PASSWORD= 28 | 29 | SESSION_DRIVER=database 30 | SESSION_LIFETIME=120 31 | SESSION_ENCRYPT=false 32 | SESSION_PATH=/ 33 | SESSION_DOMAIN=null 34 | 35 | BROADCAST_CONNECTION=log 36 | FILESYSTEM_DISK=local 37 | QUEUE_CONNECTION=database 38 | 39 | CACHE_STORE=database 40 | CACHE_PREFIX= 41 | 42 | MEMCACHED_HOST=127.0.0.1 43 | 44 | REDIS_CLIENT=phpredis 45 | REDIS_HOST=127.0.0.1 46 | REDIS_PASSWORD=null 47 | REDIS_PORT=6379 48 | 49 | MAIL_MAILER=log 50 | MAIL_HOST=127.0.0.1 51 | MAIL_PORT=2525 52 | MAIL_USERNAME=null 53 | MAIL_PASSWORD=null 54 | MAIL_ENCRYPTION=null 55 | MAIL_FROM_ADDRESS="hello@example.com" 56 | MAIL_FROM_NAME="${APP_NAME}" 57 | 58 | AWS_ACCESS_KEY_ID= 59 | AWS_SECRET_ACCESS_KEY= 60 | AWS_DEFAULT_REGION=us-east-1 61 | AWS_BUCKET= 62 | AWS_USE_PATH_STYLE_ENDPOINT=false 63 | 64 | VITE_APP_NAME="${APP_NAME}" 65 | 66 | REVERB_APP_ID= 67 | REVERB_APP_KEY= 68 | REVERB_APP_SECRET= 69 | REVERB_HOST="localhost" 70 | REVERB_PORT=8080 71 | REVERB_SCHEME=http 72 | 73 | VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" 74 | VITE_REVERB_HOST="${REVERB_HOST}" 75 | VITE_REVERB_PORT="${REVERB_PORT}" 76 | VITE_REVERB_SCHEME="${REVERB_SCHEME}" 77 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_ENV=testing 2 | APP_KEY=base64:8wjSBjHE9DUzgVvzEYMYEZmK4aHSdZGINs+kQFFYrf0= 3 | APP_DEBUG=true 4 | 5 | 6 | DB_CONNECTION=sqlite 7 | DB_DATABASE=:memory: 8 | 9 | CACHE_DRIVER=array 10 | QUEUE_CONNECTION=sync 11 | SESSION_DRIVER=array 12 | 13 | 14 | MAIL_MAILER=array 15 | 16 | 17 | # 18 | 19 | 20 | BCRYPT_ROUNDS=4 21 | 22 | 23 | TELESCOPE_ENABLED=false 24 | -------------------------------------------------------------------------------- /.env.testing.example: -------------------------------------------------------------------------------- 1 | APP_ENV=testing 2 | APP_KEY= 3 | APP_DEBUG=true 4 | 5 | 6 | DB_CONNECTION=sqlite 7 | DB_DATABASE=:memory: 8 | 9 | 10 | CACHE_DRIVER=array 11 | QUEUE_CONNECTION=sync 12 | SESSION_DRIVER=array 13 | 14 | 15 | MAIL_MAILER=array 16 | 17 | 18 | # 19 | 20 | 21 | BCRYPT_ROUNDS=4 22 | 23 | 24 | TELESCOPE_ENABLED=false 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpactor.json 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | auth.json 16 | npm-debug.log 17 | yarn-error.log 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire Chat App 2 | 3 | ### It's WIP but already usable 4 | 5 | ## Getting Started 🚀 6 | 7 | These instructions will guide you through setting up the project on your local machine for development and testing. 8 | 9 | ### Prerequisites 10 | 11 | You need to have installed the following software: 12 | 13 | - PHP 8.3 14 | - Composer 2.0.8 15 | - Node 20.10.0 16 | 17 | ### Installing 18 | 19 | Follow these steps to set up a development environment: 20 | 21 | 1. **Clone the repository** 22 | 23 | ```bash 24 | git clone https://github.com/mrpunyapal/livewire-chat-app.git 25 | ``` 26 | 27 | 2. **Install dependencies** 28 | 29 | ```bash 30 | composer install 31 | ``` 32 | 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | 3. **Duplicate the .env.example file and rename it to .env** 38 | 39 | ```bash 40 | cp .env.example .env 41 | ``` 42 | 43 | 4. **Generate the application key** 44 | 45 | ```bash 46 | php artisan key:generate 47 | ``` 48 | 49 | 5. **Run migration and seed** 50 | 51 | ```bash 52 | php artisan migrate --seed 53 | ``` 54 | 55 | 6. **Run the application** 56 | 57 | ```bash 58 | npm run dev 59 | ``` 60 | 61 | ```bash 62 | php artisan serve 63 | ``` 64 | 65 | ## How to Test the Application 🧪 66 | 67 | - Copy .env.testing.example to .env.testing 68 | - Run the following commands 69 | 70 | ```bash 71 | php artisan key:generate --env=testing 72 | ``` 73 | 74 | ```bash 75 | npm install && npm run build 76 | ``` 77 | 78 | ```bash 79 | # Lint the code using Pint 80 | composer lint 81 | composer test:lint 82 | 83 | # Refactor the code using Rector 84 | composer refactor 85 | composer test:refactor 86 | 87 | # Run PHPStan 88 | composer test:types 89 | 90 | # Run type coverage 91 | composer test:type-coverage 92 | 93 | # Run the test suite 94 | composer test:unit 95 | 96 | # Run all the tests 97 | composer test 98 | ``` 99 | Check [composer.json](/composer.json#L57-L71) for more details on scripts. 100 | 101 | ### Give Feedback 💬 102 | 103 | Give your feedback on [@MrPunyapal](https://x.com/MrPunyapal) 104 | 105 | ### Contribute 🤝 106 | 107 | Contribute if you have any ideas to improve this project. 108 | -------------------------------------------------------------------------------- /app/Events/ChatCreated.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function broadcastOn(): array 34 | { 35 | return [ 36 | new PrivateChannel('chats.'.$this->roomId), 37 | ]; 38 | } 39 | 40 | public function broadcastAs(): string 41 | { 42 | return 'chat-created'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Events/ChatUpdated.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function broadcastOn(): array 34 | { 35 | return [ 36 | new PrivateChannel('chats.'.$this->roomId), 37 | ]; 38 | } 39 | 40 | public function broadcastAs(): string 41 | { 42 | return 'chat-updated'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | logout(); 18 | 19 | Session::invalidate(); 20 | Session::regenerateToken(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Livewire/Chats/Index.php: -------------------------------------------------------------------------------- 1 | roomId === null ? null : Room::query() 28 | ->whereRelation('users', 'users.id', auth()->id()) 29 | ->find($this->roomId); 30 | } 31 | 32 | #[On('room-selected')] 33 | public function selectRoom(int $id): void 34 | { 35 | $this->dispatch('room-closed', roomId: $this->roomId); 36 | 37 | $this->roomId = $id; 38 | } 39 | 40 | public function render(): View 41 | { 42 | return view('livewire.chats.index', [ 43 | 'room' => $this->room, 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Livewire/Chats/ListChats.php: -------------------------------------------------------------------------------- 1 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | HTML; 39 | } 40 | 41 | public function render(): View 42 | { 43 | if ($this->offset > 0) { 44 | /** 45 | * @var Event $event 46 | */ 47 | $event = $this->dispatch('chats:loaded'); 48 | $event->self(); 49 | } 50 | 51 | return view('livewire.chats.list-chats', [ 52 | 'chats' => Chat::query() 53 | ->where('room_id', $this->roomId) 54 | ->whereHas('room.users', function (Builder $query): void { 55 | $query->where('users.id', auth()->id()); 56 | }) 57 | ->orderBy('created_at', 'desc') 58 | ->with('user') 59 | ->limit($this->limit) 60 | ->offset($this->offset) 61 | ->get(), 62 | ]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Livewire/Chats/Save.php: -------------------------------------------------------------------------------- 1 | chatId = $chatId; 42 | $this->message = $message; 43 | $this->parentId = null; 44 | $this->replyMessage = ''; 45 | } 46 | 47 | #[On('chat-replying')] 48 | public function startReplying(int $chatId, string $message): void 49 | { 50 | $this->parentId = $chatId; 51 | $this->replyMessage = $message; 52 | $this->message = ''; 53 | $this->chatId = null; 54 | } 55 | 56 | public function save(): void 57 | { 58 | $this->validate(); 59 | 60 | $room = Room::query()->findOrFail($this->roomId); 61 | 62 | $memberExists = $room->users() 63 | ->where('user_id', auth()->id()) 64 | ->exists(); 65 | 66 | Gate::denyIf(! $memberExists, 'You are not a member of this room.'); 67 | 68 | if ($this->parentId !== null && $this->parentId !== 0) { 69 | $chat = $room->chats()->create([ 70 | 'user_id' => auth()->id(), 71 | 'message' => $this->pull('message'), 72 | 'parent_id' => $this->parentId, 73 | ]); 74 | 75 | broadcast(new ChatCreated( 76 | chatId: $chat->id, 77 | roomId: $this->roomId, 78 | ))->toOthers(); 79 | 80 | $this->createdChat = $chat; 81 | $this->dispatch('chat:created'); 82 | 83 | $this->parentId = null; 84 | $this->replyMessage = ''; 85 | 86 | return; 87 | } 88 | 89 | if ($this->chatId !== null && $this->chatId !== 0) { 90 | $chat = $room->chats()->findOrFail($this->chatId); 91 | Gate::denyIf($chat->user_id !== auth()->id(), 'You are not the owner of this chat.'); 92 | 93 | $chat->update([ 94 | 'message' => $this->pull('message'), 95 | ]); 96 | 97 | broadcast(new ChatUpdated( 98 | chatId: $chat->id, 99 | roomId: $this->roomId, 100 | ))->toOthers(); 101 | 102 | $this->dispatch('chat:updated.'.$this->chatId); 103 | 104 | $this->chatId = null; 105 | 106 | return; 107 | } 108 | 109 | $chat = $room->chats()->create([ 110 | 'user_id' => auth()->id(), 111 | 'message' => $this->pull('message'), 112 | ]); 113 | 114 | broadcast(new ChatCreated( 115 | chatId: $chat->id, 116 | roomId: $this->roomId, 117 | ))->toOthers(); 118 | 119 | $this->createdChat = $chat; 120 | 121 | $this->dispatch('chat:created'); 122 | } 123 | 124 | /** 125 | * @param array{roomId: int, chatId: int} $event 126 | */ 127 | #[On('echo-private:chats.{roomId},.chat-created')] 128 | public function chatCreated(array $event): void 129 | { 130 | $this->createdChat = Chat::query() 131 | ->whereKey($event['chatId']) 132 | ->first(); 133 | 134 | $this->dispatch('chat:created'); 135 | } 136 | 137 | /** 138 | * @param array{roomId: int, chatId: int} $event 139 | */ 140 | #[On('echo-private:chats.{roomId},.chat-updated')] 141 | public function chatUpdated(array $event): void 142 | { 143 | $this->dispatch('chat:updated.'.$event['chatId']); 144 | } 145 | 146 | public function cancel(): void 147 | { 148 | $this->message = ''; 149 | $this->chatId = null; 150 | $this->parentId = null; 151 | $this->replyMessage = ''; 152 | } 153 | 154 | public function render(): View 155 | { 156 | return view('livewire.chats.save', [ 157 | 'createdChat' => $this->createdChat, 158 | ]); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/Livewire/Chats/Show.php: -------------------------------------------------------------------------------- 1 | dispatch('chat-editing', chatId: $this->chat->id, message: $this->chat->message); 21 | } 22 | 23 | public function delete(): void 24 | { 25 | abort_unless($this->isCurrentUser(), 403, 'You are not authorized to delete this chat.'); 26 | 27 | $this->chat->touch('deleted_at'); 28 | 29 | broadcast(new ChatUpdated( 30 | chatId: $this->chat->id, 31 | roomId: $this->chat->room_id, 32 | ))->toOthers(); 33 | 34 | $this->dispatch('chat:updated.'.$this->chat->id); 35 | } 36 | 37 | public function reply(): void 38 | { 39 | $this->dispatch('chat-replying', chatId: $this->chat->id, message: $this->chat->message); 40 | } 41 | 42 | public function render(): View 43 | { 44 | if ($this->chat->parent_id !== null) { 45 | $this->chat->load('parent'); 46 | } 47 | 48 | return view('livewire.chats.show', [ 49 | 'chat' => $this->chat, 50 | 'isCurrentUser' => $this->isCurrentUser(), 51 | ]); 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | protected function getListeners(): array 58 | { 59 | if ($this->chat->parent_id !== null) { 60 | return [ 61 | 'chat:updated.'.$this->chat->parent_id => '$refresh', 62 | ]; 63 | } 64 | 65 | return []; 66 | } 67 | 68 | private function isCurrentUser(): bool 69 | { 70 | return $this->chat->user->id === auth()->id(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/Livewire/Forms/LoginForm.php: -------------------------------------------------------------------------------- 1 | ensureIsNotRateLimited(); 34 | 35 | if (! Auth::attempt([ 36 | 'email' => $this->email, 37 | 'password' => $this->password, 38 | ], $this->remember)) { 39 | RateLimiter::hit($this->throttleKey()); 40 | 41 | throw ValidationException::withMessages([ 42 | 'form.email' => trans('auth.failed'), 43 | ]); 44 | } 45 | 46 | RateLimiter::clear($this->throttleKey()); 47 | } 48 | 49 | /** 50 | * Ensure the authentication request is not rate limited. 51 | */ 52 | protected function ensureIsNotRateLimited(): void 53 | { 54 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 55 | return; 56 | } 57 | 58 | event(new Lockout(request())); 59 | 60 | $seconds = RateLimiter::availableIn($this->throttleKey()); 61 | 62 | throw ValidationException::withMessages([ 63 | 'form.email' => trans('auth.throttle', [ 64 | 'seconds' => $seconds, 65 | 'minutes' => ceil($seconds / 60), 66 | ]), 67 | ]); 68 | } 69 | 70 | /** 71 | * Get the authentication rate limiting throttle key. 72 | */ 73 | protected function throttleKey(): string 74 | { 75 | return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Livewire/Layout/Navigation.php: -------------------------------------------------------------------------------- 1 | redirect('/', navigate: true); 21 | } 22 | 23 | /** 24 | * Render the component. 25 | */ 26 | public function render(): View 27 | { 28 | return view('livewire.layout.navigation'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Auth/ConfirmPassword.php: -------------------------------------------------------------------------------- 1 | redirectRoute('login', navigate: true); 26 | 27 | return; 28 | } 29 | 30 | $this->validate([ 31 | 'password' => ['required', 'string'], 32 | ]); 33 | 34 | throw_unless(Auth::guard('web')->validate([ 35 | 'email' => Auth::user()->email, 36 | 'password' => $this->password, 37 | ]), ValidationException::withMessages([ 38 | 'password' => __('auth.password'), 39 | ])); 40 | 41 | session(['auth.password_confirmed_at' => Carbon::now()->timestamp]); 42 | 43 | $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); 44 | } 45 | 46 | /** 47 | * Render the component. 48 | */ 49 | public function render(): View 50 | { 51 | return view('livewire.pages.auth.confirm-password'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Auth/ForgotPassword.php: -------------------------------------------------------------------------------- 1 | validate([ 23 | 'email' => ['required', 'string', 'email'], 24 | ]); 25 | 26 | // We will send the password reset link to this user. Once we have attempted 27 | // to send the link, we will examine the response then see the message we 28 | // need to show to the user. Finally, we'll send out a proper response. 29 | $status = Password::sendResetLink([ 30 | 'email' => $this->email, 31 | ]); 32 | 33 | if ($status !== Password::RESET_LINK_SENT) { 34 | $this->addError('email', __($status)); 35 | 36 | return; 37 | } 38 | 39 | $this->reset('email'); 40 | 41 | session()->flash('status', __($status)); 42 | } 43 | 44 | /** 45 | * Render the component. 46 | */ 47 | public function render(): View 48 | { 49 | return view('livewire.pages.auth.forgot-password'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Auth/Login.php: -------------------------------------------------------------------------------- 1 | validate(); 24 | 25 | $this->form->authenticate(); 26 | 27 | Session::regenerate(); 28 | 29 | $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); 30 | } 31 | 32 | /** 33 | * Render the component. 34 | */ 35 | public function render(): View 36 | { 37 | return view('livewire.pages.auth.login'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Auth/Register.php: -------------------------------------------------------------------------------- 1 | $validated */ 33 | $validated = $this->validate([ 34 | 'name' => ['required', 'string', 'max:255'], 35 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 36 | 'password' => ['required', 'string', 'confirmed', Password::defaults()], 37 | ]); 38 | 39 | $validated['password'] = Hash::make($validated['password']); 40 | 41 | event(new Registered($user = User::query()->create($validated))); 42 | 43 | Auth::login($user); 44 | 45 | $this->redirect(route('dashboard', absolute: false), navigate: true); 46 | } 47 | 48 | /** 49 | * Render the component. 50 | */ 51 | public function render(): View 52 | { 53 | return view('livewire.pages.auth.register'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Auth/ResetPassword.php: -------------------------------------------------------------------------------- 1 | token = $token; 37 | 38 | $this->email = request()->string('email')->toString(); 39 | } 40 | 41 | /** 42 | * Reset the password for the given user. 43 | */ 44 | public function resetPassword(): void 45 | { 46 | $this->validate([ 47 | 'token' => ['required'], 48 | 'email' => ['required', 'string', 'email'], 49 | 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], 50 | ]); 51 | 52 | /** @var array $data */ 53 | $data = $this->only('email', 'password', 'password_confirmation', 'token'); 54 | 55 | // Here we will attempt to reset the user's password. If it is successful we 56 | // will update the password on an actual user model and persist it to the 57 | // database. Otherwise we will parse the error and return the response. 58 | /** 59 | * @var string 60 | */ 61 | $status = Password::reset( 62 | $data, 63 | function (User $user): void { 64 | $user->forceFill([ 65 | 'password' => Hash::make($this->password), 66 | 'remember_token' => Str::random(60), 67 | ])->save(); 68 | 69 | event(new PasswordReset($user)); 70 | } 71 | ); 72 | 73 | // If the password was successfully reset, we will redirect the user back to 74 | // the application's home authenticated view. If there is an error we can 75 | // redirect them back to where they came from with their error message. 76 | if ($status !== Password::PASSWORD_RESET) { 77 | $this->addError('email', __($status)); 78 | 79 | return; 80 | } 81 | 82 | Session::flash('status', __($status)); 83 | 84 | $this->redirectRoute('login', navigate: true); 85 | } 86 | 87 | /** 88 | * Render the component. 89 | */ 90 | public function render(): View 91 | { 92 | return view('livewire.pages.auth.reset-password'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Livewire/Pages/Chats.php: -------------------------------------------------------------------------------- 1 | redirectRoute('login', navigate: true); 23 | 24 | return; 25 | } 26 | 27 | $this->validate([ 28 | 'password' => ['required', 'string', 'current_password'], 29 | ]); 30 | 31 | tap(Auth::user(), $logout(...))->delete(); 32 | 33 | $this->redirect('/', navigate: true); 34 | } 35 | 36 | /** 37 | * Render the component. 38 | */ 39 | public function render(): View 40 | { 41 | return view('livewire.profile.delete-user-form'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Livewire/Profile/UpdatePasswordForm.php: -------------------------------------------------------------------------------- 1 | redirectRoute('login', navigate: true); 29 | 30 | return; 31 | } 32 | 33 | try { 34 | /** @var array $validated */ 35 | $validated = $this->validate([ 36 | 'current_password' => ['required', 'string', 'current_password'], 37 | 'password' => ['required', 'string', Password::defaults(), 'confirmed'], 38 | ]); 39 | } catch (ValidationException $validationException) { 40 | $this->reset('current_password', 'password', 'password_confirmation'); 41 | 42 | throw $validationException; 43 | } 44 | 45 | Auth::user()->update([ 46 | 'password' => Hash::make($validated['password']), 47 | ]); 48 | 49 | $this->reset('current_password', 'password', 'password_confirmation'); 50 | 51 | $this->dispatch('password-updated'); 52 | } 53 | 54 | /** 55 | * Render the component. 56 | */ 57 | public function render(): View 58 | { 59 | return view('livewire.profile.update-password-form'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Livewire/Profile/UpdateProfileInformationForm.php: -------------------------------------------------------------------------------- 1 | redirectRoute('login', navigate: true); 26 | 27 | return; 28 | } 29 | 30 | $this->name = Auth::user()->name; 31 | $this->email = Auth::user()->email; 32 | } 33 | 34 | /** 35 | * Update the profile information for the currently authenticated user. 36 | */ 37 | public function updateProfileInformation(): void 38 | { 39 | $user = Auth::user(); 40 | 41 | if ($user === null) { 42 | $this->redirectRoute('login', navigate: true); 43 | 44 | return; 45 | } 46 | 47 | /** @var array $validated */ 48 | $validated = $this->validate([ 49 | 'name' => ['required', 'string', 'max:255'], 50 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)], 51 | ]); 52 | 53 | $user->fill($validated); 54 | 55 | $user->save(); 56 | 57 | $this->dispatch('profile-updated', name: $user->name); 58 | } 59 | 60 | /** 61 | * Render the component. 62 | */ 63 | public function render(): View 64 | { 65 | return view('livewire.profile.update-profile-information-form'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Livewire/Rooms/Create.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | #[Validate([ 24 | 'members' => ['array', 'min:1'], 25 | 'members.*' => [ 26 | 'required', 27 | 'exists:users,id', 28 | ], 29 | ])] 30 | public ?array $members = []; 31 | 32 | public function store(): void 33 | { 34 | if (auth()->user() === null) { 35 | $this->redirectRoute('login', navigate: true); 36 | 37 | return; 38 | } 39 | 40 | /** @var array{members: array, name: string} $validated */ 41 | $validated = $this->validate(); 42 | 43 | $room = Room::create([ 44 | 'name' => $this->pull('name'), 45 | 'user_id' => auth()->id(), 46 | ]); 47 | 48 | $validated['members'][] = auth()->id(); 49 | 50 | $room->users()->attach($validated['members']); 51 | 52 | $this->dispatch('room-created'); 53 | $this->dispatch('room-selected', id: $room->id); 54 | 55 | $this->reset(); 56 | } 57 | 58 | public function render(): View 59 | { 60 | return view('livewire.rooms.create', [ 61 | 'users' => User::query() 62 | ->where('id', '!=', auth()->id()) 63 | ->pluck('name', 'id'), 64 | ]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Livewire/Rooms/Index.php: -------------------------------------------------------------------------------- 1 | activeRoomId = $id; 21 | } 22 | 23 | public function render(): View 24 | { 25 | return view('livewire.rooms.index', [ 26 | 'rooms' => Room::query() 27 | ->whereRelation('users', 'users.id', auth()->id()) 28 | ->latest() 29 | ->get(), 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Livewire/Welcome/Navigation.php: -------------------------------------------------------------------------------- 1 | */ 30 | use HasFactory; 31 | 32 | /** 33 | * Get the user who sent the chat. 34 | * 35 | * @return BelongsTo 36 | */ 37 | public function user(): BelongsTo 38 | { 39 | return $this->belongsTo(User::class); 40 | } 41 | 42 | /** 43 | * Get the parent chat if this is a reply. 44 | * 45 | * @return BelongsTo 46 | */ 47 | public function parent(): BelongsTo 48 | { 49 | return $this->belongsTo(Chat::class, 'parent_id'); 50 | } 51 | 52 | /** 53 | * Get the room in which the chat was sent. 54 | * 55 | * @return BelongsTo 56 | */ 57 | public function room(): BelongsTo 58 | { 59 | return $this->belongsTo(Room::class); 60 | } 61 | 62 | /** 63 | * Get the attributes that should be cast. 64 | * 65 | * @return array 66 | */ 67 | #[Override] 68 | protected function casts(): array 69 | { 70 | return [ 71 | 'deleted_at' => 'datetime', 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Models/Member.php: -------------------------------------------------------------------------------- 1 | */ 25 | use HasFactory; 26 | 27 | /** 28 | * Get the room that the member belongs to. 29 | * 30 | * @return BelongsTo 31 | */ 32 | public function room(): BelongsTo 33 | { 34 | return $this->belongsTo(Room::class); 35 | } 36 | 37 | /** 38 | * Get the user that the member belongs to. 39 | * 40 | * @return BelongsTo 41 | */ 42 | public function user(): BelongsTo 43 | { 44 | return $this->belongsTo(User::class); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Models/Room.php: -------------------------------------------------------------------------------- 1 | $users 25 | * @property-read Collection $chats 26 | */ 27 | class Room extends Model 28 | { 29 | /** @use HasFactory */ 30 | use HasFactory; 31 | 32 | /** 33 | * Get the user that owns the room. 34 | * 35 | * @return BelongsTo 36 | */ 37 | public function user(): BelongsTo 38 | { 39 | return $this->belongsTo(User::class); 40 | } 41 | 42 | /** 43 | * Get the members for the room. 44 | * 45 | * @return BelongsToMany 46 | */ 47 | public function users(): BelongsToMany 48 | { 49 | return $this->belongsToMany(User::class, 'members'); 50 | } 51 | 52 | /** 53 | * Get the chats for the room. 54 | * 55 | * @return HasMany 56 | */ 57 | public function chats(): HasMany 58 | { 59 | return $this->hasMany(Chat::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 29 | use HasFactory; 30 | 31 | use Notifiable; 32 | 33 | /** 34 | * The attributes that are mass assignable. 35 | * 36 | * @var list 37 | */ 38 | protected $fillable = [ 39 | 'name', 40 | 'email', 41 | 'password', 42 | ]; 43 | 44 | /** 45 | * The attributes that should be hidden for serialization. 46 | * 47 | * @var list 48 | */ 49 | protected $hidden = [ 50 | 'password', 51 | 'remember_token', 52 | ]; 53 | 54 | /** 55 | * Get the rooms for the user. 56 | * 57 | * @return BelongsToMany 58 | */ 59 | public function rooms(): BelongsToMany 60 | { 61 | return $this->belongsToMany(Room::class, 'members') 62 | ->withTimestamps(); 63 | } 64 | 65 | /** 66 | * Get the user's profile image. 67 | */ 68 | public function getProfileAttribute(): string 69 | { 70 | return 'https://ui-avatars.com/api/?name='.urlencode($this->name).'&color=7F9CF5&background=EBF4FF'; 71 | } 72 | 73 | /** 74 | * Get the attributes that should be cast. 75 | * 76 | * @return array 77 | */ 78 | #[Override] 79 | protected function casts(): array 80 | { 81 | return [ 82 | 'email_verified_at' => 'datetime', 83 | 'password' => 'hashed', 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | web: __DIR__.'/../routes/web.php', 12 | commands: __DIR__.'/../routes/console.php', 13 | channels: __DIR__.'/../routes/channels.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware): void {}) 17 | ->withExceptions(function (Exceptions $exceptions): void {})->create(); 18 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Application Environment 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This value determines the "environment" your application is currently 26 | | running in. This may determine how you prefer to configure various 27 | | services the application utilizes. Set this in your ".env" file. 28 | | 29 | */ 30 | 31 | 'env' => env('APP_ENV', 'production'), 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Application Debug Mode 36 | |-------------------------------------------------------------------------- 37 | | 38 | | When your application is in debug mode, detailed error messages with 39 | | stack traces will be shown on every error that occurs within your 40 | | application. If disabled, a simple generic error page is shown. 41 | | 42 | */ 43 | 44 | 'debug' => (bool) env('APP_DEBUG', false), 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Application URL 49 | |-------------------------------------------------------------------------- 50 | | 51 | | This URL is used by the console to properly generate URLs when using 52 | | the Artisan command line tool. You should set this to the root of 53 | | the application so that it's available within Artisan commands. 54 | | 55 | */ 56 | 57 | 'url' => env('APP_URL', 'http://localhost'), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Application Timezone 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may specify the default timezone for your application, which 65 | | will be used by the PHP date and date-time functions. The timezone 66 | | is set to "UTC" by default as it is suitable for most use cases. 67 | | 68 | */ 69 | 70 | 'timezone' => env('APP_TIMEZONE', 'UTC'), 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Application Locale Configuration 75 | |-------------------------------------------------------------------------- 76 | | 77 | | The application locale determines the default locale that will be used 78 | | by Laravel's translation / localization methods. This option can be 79 | | set to any locale for which you plan to have translation strings. 80 | | 81 | */ 82 | 83 | 'locale' => env('APP_LOCALE', 'en'), 84 | 85 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 86 | 87 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Encryption Key 92 | |-------------------------------------------------------------------------- 93 | | 94 | | This key is utilized by Laravel's encryption services and should be set 95 | | to a random, 32 character string to ensure that all encrypted values 96 | | are secure. You should do this prior to deploying the application. 97 | | 98 | */ 99 | 100 | 'cipher' => 'AES-256-CBC', 101 | 102 | 'key' => env('APP_KEY'), 103 | 104 | 'previous_keys' => [ 105 | ...array_filter( 106 | explode(',', (string) env('APP_PREVIOUS_KEYS', '')) 107 | ), 108 | ], 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | Maintenance Mode Driver 113 | |-------------------------------------------------------------------------- 114 | | 115 | | These configuration options determine the driver used to determine and 116 | | manage Laravel's "maintenance mode" status. The "cache" driver will 117 | | allow maintenance mode to be controlled across multiple machines. 118 | | 119 | | Supported drivers: "file", "cache" 120 | | 121 | */ 122 | 123 | 'maintenance' => [ 124 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 125 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 126 | ], 127 | 128 | ]; 129 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'guard' => env('AUTH_GUARD', 'web'), 22 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 23 | ], 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Authentication Guards 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Next, you may define every authentication guard for your application. 31 | | Of course, a great default configuration has been defined for you 32 | | which utilizes session storage plus the Eloquent user provider. 33 | | 34 | | All authentication guards have a user provider, which defines how the 35 | | users are actually retrieved out of your database or other storage 36 | | system used by the application. Typically, Eloquent is utilized. 37 | | 38 | | Supported: "session" 39 | | 40 | */ 41 | 42 | 'guards' => [ 43 | 'web' => [ 44 | 'driver' => 'session', 45 | 'provider' => 'users', 46 | ], 47 | ], 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | User Providers 52 | |-------------------------------------------------------------------------- 53 | | 54 | | All authentication guards have a user provider, which defines how the 55 | | users are actually retrieved out of your database or other storage 56 | | system used by the application. Typically, Eloquent is utilized. 57 | | 58 | | If you have multiple user tables or models you may configure multiple 59 | | providers to represent the model / table. These providers may then 60 | | be assigned to any extra authentication guards you have defined. 61 | | 62 | | Supported: "database", "eloquent" 63 | | 64 | */ 65 | 66 | 'providers' => [ 67 | 'users' => [ 68 | 'driver' => 'eloquent', 69 | 'model' => env('AUTH_MODEL', User::class), 70 | ], 71 | 72 | // 'users' => [ 73 | // 'driver' => 'database', 74 | // 'table' => 'users', 75 | // ], 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Resetting Passwords 81 | |-------------------------------------------------------------------------- 82 | | 83 | | These configuration options specify the behavior of Laravel's password 84 | | reset functionality, including the table utilized for token storage 85 | | and the user provider that is invoked to actually retrieve users. 86 | | 87 | | The expiry time is the number of minutes that each reset token will be 88 | | considered valid. This security feature keeps tokens short-lived so 89 | | they have less time to be guessed. You may change this as needed. 90 | | 91 | | The throttle setting is the number of seconds a user must wait before 92 | | generating more password reset tokens. This prevents the user from 93 | | quickly generating a very large amount of password reset tokens. 94 | | 95 | */ 96 | 97 | 'passwords' => [ 98 | 'users' => [ 99 | 'provider' => 'users', 100 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 101 | 'expire' => 60, 102 | 'throttle' => 60, 103 | ], 104 | ], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Password Confirmation Timeout 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Here you may define the amount of seconds before a password confirmation 112 | | window expires and users are asked to re-enter their password via the 113 | | confirmation screen. By default, the timeout lasts for three hours. 114 | | 115 | */ 116 | 117 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 118 | 119 | ]; 120 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_CONNECTION', 'null'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Broadcast Connections 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may define all of the broadcast connections that will be used 28 | | to broadcast events to other systems or over WebSockets. Samples of 29 | | each available type of connection are provided inside this array. 30 | | 31 | */ 32 | 33 | 'connections' => [ 34 | 35 | 'reverb' => [ 36 | 'driver' => 'reverb', 37 | 'key' => env('REVERB_APP_KEY'), 38 | 'secret' => env('REVERB_APP_SECRET'), 39 | 'app_id' => env('REVERB_APP_ID'), 40 | 'options' => [ 41 | 'host' => env('REVERB_HOST'), 42 | 'port' => env('REVERB_PORT', 443), 43 | 'scheme' => env('REVERB_SCHEME', 'https'), 44 | 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', 45 | ], 46 | 'client_options' => [ 47 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 48 | ], 49 | ], 50 | 51 | 'pusher' => [ 52 | 'driver' => 'pusher', 53 | 'key' => env('PUSHER_APP_KEY'), 54 | 'secret' => env('PUSHER_APP_SECRET'), 55 | 'app_id' => env('PUSHER_APP_ID'), 56 | 'options' => [ 57 | 'cluster' => env('PUSHER_APP_CLUSTER'), 58 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 59 | 'port' => env('PUSHER_PORT', 443), 60 | 'scheme' => env('PUSHER_SCHEME', 'https'), 61 | 'encrypted' => true, 62 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 63 | ], 64 | 'client_options' => [ 65 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 66 | ], 67 | ], 68 | 69 | 'ably' => [ 70 | 'driver' => 'ably', 71 | 'key' => env('ABLY_KEY'), 72 | ], 73 | 74 | 'log' => [ 75 | 'driver' => 'log', 76 | ], 77 | 78 | 'null' => [ 79 | 'driver' => 'null', 80 | ], 81 | 82 | ], 83 | 84 | ]; 85 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Cache Stores 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may define all of the cache "stores" for your application as 28 | | well as their drivers. You may even define multiple stores for the 29 | | same cache driver to group types of items stored in your caches. 30 | | 31 | | Supported drivers: "array", "database", "file", "memcached", 32 | | "redis", "dynamodb", "octane", "null" 33 | | 34 | */ 35 | 36 | 'stores' => [ 37 | 38 | 'array' => [ 39 | 'driver' => 'array', 40 | 'serialize' => false, 41 | ], 42 | 43 | 'database' => [ 44 | 'driver' => 'database', 45 | 'connection' => env('DB_CACHE_CONNECTION'), 46 | 'table' => env('DB_CACHE_TABLE', 'cache'), 47 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 48 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 49 | ], 50 | 51 | 'file' => [ 52 | 'driver' => 'file', 53 | 'path' => storage_path('framework/cache/data'), 54 | 'lock_path' => storage_path('framework/cache/data'), 55 | ], 56 | 57 | 'memcached' => [ 58 | 'driver' => 'memcached', 59 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 60 | 'sasl' => [ 61 | env('MEMCACHED_USERNAME'), 62 | env('MEMCACHED_PASSWORD'), 63 | ], 64 | 'options' => [ 65 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 66 | ], 67 | 'servers' => [ 68 | [ 69 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 70 | 'port' => env('MEMCACHED_PORT', 11211), 71 | 'weight' => 100, 72 | ], 73 | ], 74 | ], 75 | 76 | 'redis' => [ 77 | 'driver' => 'redis', 78 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 79 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 80 | ], 81 | 82 | 'dynamodb' => [ 83 | 'driver' => 'dynamodb', 84 | 'key' => env('AWS_ACCESS_KEY_ID'), 85 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 86 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 87 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 88 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 89 | ], 90 | 91 | 'octane' => [ 92 | 'driver' => 'octane', 93 | ], 94 | 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Cache Key Prefix 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 103 | | stores, there might be other applications using the same cache. For 104 | | that reason, you may prefix every cache key to avoid collisions. 105 | | 106 | */ 107 | 108 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Filesystem Disks 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Below you may configure as many filesystem disks as necessary, and you 26 | | may even configure multiple disks for the same driver. Examples for 27 | | most supported storage drivers are configured here for reference. 28 | | 29 | | Supported drivers: "local", "ftp", "sftp", "s3" 30 | | 31 | */ 32 | 33 | 'disks' => [ 34 | 35 | 'local' => [ 36 | 'driver' => 'local', 37 | 'root' => storage_path('app'), 38 | 'throw' => false, 39 | ], 40 | 41 | 'public' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path('app/public'), 44 | 'url' => env('APP_URL').'/storage', 45 | 'visibility' => 'public', 46 | 'throw' => false, 47 | ], 48 | 49 | 's3' => [ 50 | 'driver' => 's3', 51 | 'key' => env('AWS_ACCESS_KEY_ID'), 52 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 53 | 'region' => env('AWS_DEFAULT_REGION'), 54 | 'bucket' => env('AWS_BUCKET'), 55 | 'url' => env('AWS_URL'), 56 | 'endpoint' => env('AWS_ENDPOINT'), 57 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 58 | 'throw' => false, 59 | ], 60 | 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Symbolic Links 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may configure the symbolic links that will be created when the 69 | | `storage:link` Artisan command is executed. The array keys should be 70 | | the locations of the links and the values should be their targets. 71 | | 72 | */ 73 | 74 | 'links' => [ 75 | public_path('storage') => storage_path('app/public'), 76 | ], 77 | 78 | ]; 79 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Deprecations Log Channel 28 | |-------------------------------------------------------------------------- 29 | | 30 | | This option controls the log channel that should be used to log warnings 31 | | regarding deprecated PHP and library features. This allows you to get 32 | | your application ready for upcoming major versions of dependencies. 33 | | 34 | */ 35 | 36 | 'deprecations' => [ 37 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 38 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 39 | ], 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Log Channels 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here you may configure the log channels for your application. Laravel 47 | | utilizes the Monolog PHP logging library, which includes a variety 48 | | of powerful log handlers and formatters that you're free to use. 49 | | 50 | | Available drivers: "single", "daily", "slack", "syslog", 51 | | "errorlog", "monolog", "custom", "stack" 52 | | 53 | */ 54 | 55 | 'channels' => [ 56 | 57 | 'stack' => [ 58 | 'driver' => 'stack', 59 | 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 60 | 'ignore_exceptions' => false, 61 | ], 62 | 63 | 'single' => [ 64 | 'driver' => 'single', 65 | 'path' => storage_path('logs/laravel.log'), 66 | 'level' => env('LOG_LEVEL', 'debug'), 67 | 'replace_placeholders' => true, 68 | ], 69 | 70 | 'daily' => [ 71 | 'driver' => 'daily', 72 | 'path' => storage_path('logs/laravel.log'), 73 | 'level' => env('LOG_LEVEL', 'debug'), 74 | 'days' => env('LOG_DAILY_DAYS', 14), 75 | 'replace_placeholders' => true, 76 | ], 77 | 78 | 'slack' => [ 79 | 'driver' => 'slack', 80 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 81 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 82 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 83 | 'level' => env('LOG_LEVEL', 'critical'), 84 | 'replace_placeholders' => true, 85 | ], 86 | 87 | 'papertrail' => [ 88 | 'driver' => 'monolog', 89 | 'level' => env('LOG_LEVEL', 'debug'), 90 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 91 | 'handler_with' => [ 92 | 'host' => env('PAPERTRAIL_URL'), 93 | 'port' => env('PAPERTRAIL_PORT'), 94 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 95 | ], 96 | 'processors' => [PsrLogMessageProcessor::class], 97 | ], 98 | 99 | 'stderr' => [ 100 | 'driver' => 'monolog', 101 | 'level' => env('LOG_LEVEL', 'debug'), 102 | 'handler' => StreamHandler::class, 103 | 'formatter' => env('LOG_STDERR_FORMATTER'), 104 | 'with' => [ 105 | 'stream' => 'php://stderr', 106 | ], 107 | 'processors' => [PsrLogMessageProcessor::class], 108 | ], 109 | 110 | 'syslog' => [ 111 | 'driver' => 'syslog', 112 | 'level' => env('LOG_LEVEL', 'debug'), 113 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 114 | 'replace_placeholders' => true, 115 | ], 116 | 117 | 'errorlog' => [ 118 | 'driver' => 'errorlog', 119 | 'level' => env('LOG_LEVEL', 'debug'), 120 | 'replace_placeholders' => true, 121 | ], 122 | 123 | 'null' => [ 124 | 'driver' => 'monolog', 125 | 'handler' => NullHandler::class, 126 | ], 127 | 128 | 'emergency' => [ 129 | 'path' => storage_path('logs/laravel.log'), 130 | ], 131 | 132 | ], 133 | 134 | ]; 135 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Mailer Configurations 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may configure all of the mailers used by your application plus 27 | | their respective settings. Several examples have been configured for 28 | | you and you are free to add your own as your application requires. 29 | | 30 | | Laravel supports a variety of mail "transport" drivers that can be used 31 | | when delivering an email. You may specify which one you're using for 32 | | your mailers below. You may also add additional mailers if needed. 33 | | 34 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 35 | | "postmark", "resend", "log", "array", 36 | | "failover", "roundrobin" 37 | | 38 | */ 39 | 40 | 'mailers' => [ 41 | 42 | 'smtp' => [ 43 | 'transport' => 'smtp', 44 | 'url' => env('MAIL_URL'), 45 | 'host' => env('MAIL_HOST', '127.0.0.1'), 46 | 'port' => env('MAIL_PORT', 2525), 47 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 48 | 'username' => env('MAIL_USERNAME'), 49 | 'password' => env('MAIL_PASSWORD'), 50 | 'timeout' => null, 51 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 52 | ], 53 | 54 | 'ses' => [ 55 | 'transport' => 'ses', 56 | ], 57 | 58 | 'postmark' => [ 59 | 'transport' => 'postmark', 60 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 61 | // 'client' => [ 62 | // 'timeout' => 5, 63 | // ], 64 | ], 65 | 66 | 'resend' => [ 67 | 'transport' => 'resend', 68 | ], 69 | 70 | 'sendmail' => [ 71 | 'transport' => 'sendmail', 72 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 73 | ], 74 | 75 | 'log' => [ 76 | 'transport' => 'log', 77 | 'channel' => env('MAIL_LOG_CHANNEL'), 78 | ], 79 | 80 | 'array' => [ 81 | 'transport' => 'array', 82 | ], 83 | 84 | 'failover' => [ 85 | 'transport' => 'failover', 86 | 'mailers' => [ 87 | 'smtp', 88 | 'log', 89 | ], 90 | ], 91 | 92 | 'roundrobin' => [ 93 | 'transport' => 'roundrobin', 94 | 'mailers' => [ 95 | 'ses', 96 | 'postmark', 97 | ], 98 | ], 99 | 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Global "From" Address 105 | |-------------------------------------------------------------------------- 106 | | 107 | | You may wish for all emails sent by your application to be sent from 108 | | the same address. Here you may specify a name and address that is 109 | | used globally for all emails that are sent by your application. 110 | | 111 | */ 112 | 113 | 'from' => [ 114 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 115 | 'name' => env('MAIL_FROM_NAME', 'Example'), 116 | ], 117 | 118 | ]; 119 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Queue Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may configure the connection options for every queue backend 26 | | used by your application. An example configuration is provided for 27 | | each backend supported by Laravel. You're also free to add more. 28 | | 29 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 30 | | 31 | */ 32 | 33 | 'connections' => [ 34 | 35 | 'sync' => [ 36 | 'driver' => 'sync', 37 | ], 38 | 39 | 'database' => [ 40 | 'driver' => 'database', 41 | 'connection' => env('DB_QUEUE_CONNECTION'), 42 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 43 | 'queue' => env('DB_QUEUE', 'default'), 44 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 45 | 'after_commit' => false, 46 | ], 47 | 48 | 'beanstalkd' => [ 49 | 'driver' => 'beanstalkd', 50 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 51 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 52 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 53 | 'block_for' => 0, 54 | 'after_commit' => false, 55 | ], 56 | 57 | 'sqs' => [ 58 | 'driver' => 'sqs', 59 | 'key' => env('AWS_ACCESS_KEY_ID'), 60 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 61 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 62 | 'queue' => env('SQS_QUEUE', 'default'), 63 | 'suffix' => env('SQS_SUFFIX'), 64 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 65 | 'after_commit' => false, 66 | ], 67 | 68 | 'redis' => [ 69 | 'driver' => 'redis', 70 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 71 | 'queue' => env('REDIS_QUEUE', 'default'), 72 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 73 | 'block_for' => null, 74 | 'after_commit' => false, 75 | ], 76 | 77 | ], 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Job Batching 82 | |-------------------------------------------------------------------------- 83 | | 84 | | The following options configure the database and table that store job 85 | | batching information. These options can be updated to any database 86 | | connection and table which has been defined by your application. 87 | | 88 | */ 89 | 90 | 'batching' => [ 91 | 'database' => env('DB_CONNECTION', 'sqlite'), 92 | 'table' => 'job_batches', 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Failed Queue Jobs 98 | |-------------------------------------------------------------------------- 99 | | 100 | | These options configure the behavior of failed queue job logging so you 101 | | can control how and where failed jobs are stored. Laravel ships with 102 | | support for storing failed jobs in a simple file or in a database. 103 | | 104 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 105 | | 106 | */ 107 | 108 | 'failed' => [ 109 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 110 | 'database' => env('DB_CONNECTION', 'sqlite'), 111 | 'table' => 'failed_jobs', 112 | ], 113 | 114 | ]; 115 | -------------------------------------------------------------------------------- /config/reverb.php: -------------------------------------------------------------------------------- 1 | env('REVERB_SERVER', 'reverb'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Reverb Servers 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define details for each of the supported Reverb servers. 26 | | Each server has its own configuration options that are defined in 27 | | the array below. You should ensure all the options are present. 28 | | 29 | */ 30 | 31 | 'servers' => [ 32 | 33 | 'reverb' => [ 34 | 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), 35 | 'port' => env('REVERB_SERVER_PORT', 8080), 36 | 'hostname' => env('REVERB_HOST'), 37 | 'options' => [ 38 | 'tls' => [], 39 | ], 40 | 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), 41 | 'scaling' => [ 42 | 'enabled' => env('REVERB_SCALING_ENABLED', false), 43 | 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), 44 | 'server' => [ 45 | 'url' => env('REDIS_URL'), 46 | 'host' => env('REDIS_HOST', '127.0.0.1'), 47 | 'port' => env('REDIS_PORT', '6379'), 48 | 'username' => env('REDIS_USERNAME'), 49 | 'password' => env('REDIS_PASSWORD'), 50 | 'database' => env('REDIS_DB', '0'), 51 | ], 52 | ], 53 | 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), 54 | 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), 55 | ], 56 | 57 | ], 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Reverb Applications 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may define how Reverb applications are managed. If you choose 65 | | to use the "config" provider, you may define an array of apps which 66 | | your server will support, including their connection credentials. 67 | | 68 | */ 69 | 70 | 'apps' => [ 71 | 72 | 'provider' => 'config', 73 | 74 | 'apps' => [ 75 | [ 76 | 'key' => env('REVERB_APP_KEY'), 77 | 'secret' => env('REVERB_APP_SECRET'), 78 | 'app_id' => env('REVERB_APP_ID'), 79 | 'options' => [ 80 | 'host' => env('REVERB_HOST'), 81 | 'port' => env('REVERB_PORT', 443), 82 | 'scheme' => env('REVERB_SCHEME', 'https'), 83 | 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', 84 | ], 85 | 'allowed_origins' => ['*'], 86 | 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), 87 | 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), 88 | ], 89 | ], 90 | 91 | ], 92 | 93 | ]; 94 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'token' => env('POSTMARK_TOKEN'), 21 | ], 22 | 23 | 'ses' => [ 24 | 'key' => env('AWS_ACCESS_KEY_ID'), 25 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 26 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 27 | ], 28 | 29 | 'resend' => [ 30 | 'key' => env('RESEND_KEY'), 31 | ], 32 | 33 | 'slack' => [ 34 | 'notifications' => [ 35 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 36 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 37 | ], 38 | ], 39 | 40 | ]; 41 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/ChatFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ChatFactory extends Factory 17 | { 18 | /** 19 | * Define the model's default state. 20 | * 21 | * @return array 22 | */ 23 | #[Override] 24 | public function definition(): array 25 | { 26 | return [ 27 | 'user_id' => User::factory(), 28 | 'room_id' => Room::factory(), 29 | 'message' => $this->faker->sentence, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/MemberFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class MemberFactory extends Factory 17 | { 18 | /** 19 | * Define the model's default state. 20 | * 21 | * @return array 22 | */ 23 | #[Override] 24 | public function definition(): array 25 | { 26 | return [ 27 | 'room_id' => Room::factory(), 28 | 'user_id' => User::factory(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/factories/RoomFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class RoomFactory extends Factory 16 | { 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | #[Override] 23 | public function definition(): array 24 | { 25 | return [ 26 | 'name' => $this->faker->name, 27 | 'description' => $this->faker->text, 28 | 'user_id' => User::factory(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class UserFactory extends Factory 17 | { 18 | /** 19 | * The current password being used by the factory. 20 | */ 21 | protected static ?string $password = null; 22 | 23 | /** 24 | * Define the model's default state. 25 | * 26 | * @return array 27 | */ 28 | #[Override] 29 | public function definition(): array 30 | { 31 | return [ 32 | 'name' => fake()->name(), 33 | 'email' => fake()->unique()->safeEmail(), 34 | 'email_verified_at' => now(), 35 | 'password' => static::$password ??= Hash::make('password'), 36 | 'remember_token' => Str::random(10), 37 | ]; 38 | } 39 | 40 | /** 41 | * Indicate that the model's email address should be unverified. 42 | */ 43 | public function unverified(): static 44 | { 45 | return $this->state(fn (array $attributes): array => [ 46 | 'email_verified_at' => null, 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | 26 | Schema::create('password_reset_tokens', function (Blueprint $table): void { 27 | $table->string('email')->primary(); 28 | $table->string('token'); 29 | $table->timestamp('created_at')->nullable(); 30 | }); 31 | 32 | Schema::create('sessions', function (Blueprint $table): void { 33 | $table->string('id')->primary(); 34 | $table->foreignId('user_id')->nullable()->index(); 35 | $table->string('ip_address', 45)->nullable(); 36 | $table->text('user_agent')->nullable(); 37 | $table->longText('payload'); 38 | $table->integer('last_activity')->index(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | */ 45 | public function down(): void 46 | { 47 | Schema::dropIfExists('users'); 48 | Schema::dropIfExists('password_reset_tokens'); 49 | Schema::dropIfExists('sessions'); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 18 | $table->mediumText('value'); 19 | $table->integer('expiration'); 20 | }); 21 | 22 | Schema::create('cache_locks', function (Blueprint $table): void { 23 | $table->string('key')->primary(); 24 | $table->string('owner'); 25 | $table->integer('expiration'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('cache'); 35 | Schema::dropIfExists('cache_locks'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('queue')->index(); 19 | $table->longText('payload'); 20 | $table->unsignedTinyInteger('attempts'); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | }); 25 | 26 | Schema::create('job_batches', function (Blueprint $table): void { 27 | $table->string('id')->primary(); 28 | $table->string('name'); 29 | $table->integer('total_jobs'); 30 | $table->integer('pending_jobs'); 31 | $table->integer('failed_jobs'); 32 | $table->longText('failed_job_ids'); 33 | $table->mediumText('options')->nullable(); 34 | $table->integer('cancelled_at')->nullable(); 35 | $table->integer('created_at'); 36 | $table->integer('finished_at')->nullable(); 37 | }); 38 | 39 | Schema::create('failed_jobs', function (Blueprint $table): void { 40 | $table->id(); 41 | $table->string('uuid')->unique(); 42 | $table->text('connection'); 43 | $table->text('queue'); 44 | $table->longText('payload'); 45 | $table->longText('exception'); 46 | $table->timestamp('failed_at')->useCurrent(); 47 | }); 48 | } 49 | 50 | /** 51 | * Reverse the migrations. 52 | */ 53 | public function down(): void 54 | { 55 | Schema::dropIfExists('jobs'); 56 | Schema::dropIfExists('job_batches'); 57 | Schema::dropIfExists('failed_jobs'); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /database/migrations/2024_09_01_114347_create_rooms_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('name'); 20 | $table->text('description')->nullable(); 21 | $table->foreignIdFor(User::class); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('rooms'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_09_01_122615_create_members_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->foreignIdFor(Room::class); 21 | $table->foreignIdFor(User::class); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('members'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_09_01_132337_create_chats_table.php: -------------------------------------------------------------------------------- 1 | id(); 21 | $table->foreignIdFor(Chat::class, 'parent_id')->nullable(); 22 | $table->foreignIdFor(User::class); 23 | $table->foreignIdFor(Room::class); 24 | $table->text('message'); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('chats'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2025_04_29_155057_add_deleted_at_column_to_chats_table.php: -------------------------------------------------------------------------------- 1 | timestamp('deleted_at')->nullable()->after('updated_at'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::table('chats', function (Blueprint $table): void { 27 | $table->dropColumn('deleted_at'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 19 | 20 | User::factory()->create([ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.2", 10 | "@tailwindcss/vite": "^4.1.7", 11 | "axios": "^1.6.4", 12 | "laravel-echo": "^1.16.1", 13 | "laravel-vite-plugin": "^1.0", 14 | "pusher-js": "^8.4.0-rc2", 15 | "tailwindcss": "^4.1.7", 16 | "vite": "^6.0.11" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 4 | 5 | parameters: 6 | 7 | paths: 8 | - app/ 9 | 10 | # Level 10 is the highest level 11 | level: max 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "declare_strict_types": true, 5 | "strict_param": true, 6 | "strict_comparison": true, 7 | "fully_qualified_strict_types": { 8 | "import_symbols": true, 9 | "leading_backslash_in_global_namespace": false, 10 | "phpdoc_tags": [ 11 | "phpstan-param", 12 | "phpstan-property", 13 | "phpstan-property-read", 14 | "phpstan-property-write", 15 | "phpstan-return", 16 | "phpstan-var", 17 | "param", 18 | "property", 19 | "property-read", 20 | "property-write", 21 | "return", 22 | "see", 23 | "throws", 24 | "var" 25 | ] 26 | }, 27 | "ordered_class_elements": { 28 | "order": [ 29 | "use_trait", 30 | "case", 31 | "constant", 32 | "constant_public", 33 | "constant_protected", 34 | "constant_private", 35 | "property_public", 36 | "property_protected", 37 | "property_private", 38 | "construct", 39 | "destruct", 40 | "magic", 41 | "phpunit", 42 | "method_abstract", 43 | "method_public_static", 44 | "method_public", 45 | "method_protected_static", 46 | "method_protected", 47 | "method_private_static", 48 | "method_private" 49 | ], 50 | "sort_algorithm": "none" 51 | }, 52 | "modernize_types_casting": true, 53 | "no_superfluous_elseif": true, 54 | "no_useless_else": true, 55 | "no_multiple_statements_per_line": true, 56 | "no_empty_comment": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrPunyapal/livewire-chat-app/798cbdb6966a27b0aba2ba5e73104bef752d5d13/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 20 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 12 | __DIR__.'/app', 13 | __DIR__.'/bootstrap/app.php', 14 | __DIR__.'/config', 15 | __DIR__.'/database', 16 | __DIR__.'/public', 17 | ]) 18 | ->withPhpSets(php83: true) 19 | ->withPhpVersion(PhpVersion::PHP_83) 20 | ->withPreparedSets( 21 | deadCode: true, 22 | codeQuality: true, 23 | codingStyle: true, 24 | typeDeclarations: true, 25 | privatization: true, 26 | instanceOf: true, 27 | earlyReturn: true, 28 | strictBooleans: true, 29 | carbon: true, 30 | )->withSets([ 31 | LaravelSetList::LARAVEL_110, 32 | LaravelSetList::LARAVEL_CODE_QUALITY, 33 | LaravelSetList::LARAVEL_IF_HELPERS, 34 | LaravelSetList::LARAVEL_ARRAY_STR_FUNCTION_TO_STATIC_CALL, 35 | LaravelSetList::LARAVEL_FACADE_ALIASES_TO_FULL_NAMES, 36 | LaravelSetList::LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER, 37 | LaravelSetList::LARAVEL_CONTAINER_STRING_TO_FULLY_QUALIFIED_NAME, 38 | LaravelSetList::LARAVEL_ARRAYACCESS_TO_METHOD_CALL, 39 | ])->withConfiguredRule(EloquentMagicMethodToQueryBuilderRector::class, [ 40 | 'exclude_methods' => [ 41 | 'create', 42 | ], 43 | ])->withImportNames(); 44 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin '@tailwindcss/forms'; 4 | 5 | @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; 6 | 7 | @theme { 8 | --font-sans: 9 | Figtree, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 10 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 11 | } 12 | 13 | /* 14 | The default border color has changed to `currentColor` in Tailwind CSS v4, 15 | so we've added these compatibility styles to make sure everything still 16 | looks the same as it did with Tailwind CSS v3. 17 | 18 | If we ever want to remove these styles, we need to add an explicit border 19 | color utility to any element that depends on these defaults. 20 | */ 21 | @layer base { 22 | *, 23 | ::after, 24 | ::before, 25 | ::backdrop, 26 | ::file-selector-button { 27 | border-color: var(--color-gray-200, currentColor); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm' 3 | import { saveChat } from './save-chat.js' 4 | import { notifications, notify } from './notification.js' 5 | 6 | window.Alpine = Alpine 7 | 8 | Alpine.data('saveChat', saveChat) 9 | Alpine.data('notifications', notifications) 10 | 11 | window.Alpine.plugin(notify) 12 | 13 | Livewire.start() 14 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | window.axios = axios; 3 | 4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | 6 | /** 7 | * Echo exposes an expressive API for subscribing to channels and listening 8 | * for events that are broadcast by Laravel. Echo and event broadcasting 9 | * allow your team to quickly build robust real-time web applications. 10 | */ 11 | 12 | import './echo'; 13 | -------------------------------------------------------------------------------- /resources/js/echo.js: -------------------------------------------------------------------------------- 1 | import Echo from 'laravel-echo'; 2 | 3 | import Pusher from 'pusher-js'; 4 | window.Pusher = Pusher; 5 | 6 | window.Echo = new Echo({ 7 | broadcaster: 'reverb', 8 | key: import.meta.env.VITE_REVERB_APP_KEY, 9 | wsHost: import.meta.env.VITE_REVERB_HOST, 10 | wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, 11 | wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, 12 | forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', 13 | enabledTransports: ['ws', 'wss'], 14 | }); 15 | -------------------------------------------------------------------------------- /resources/js/notification.js: -------------------------------------------------------------------------------- 1 | export const notifications = () => ({ 2 | notifications: [], 3 | init() { 4 | window.addEventListener('notify', ({ detail }) => this.add(detail.message, detail.type)) 5 | }, 6 | add(message, type = 'success') { 7 | const id = Math.random().toString(36).substr(2, 9) 8 | this.notifications.push({ 9 | id, 10 | message, 11 | type, 12 | visible: true 13 | }) 14 | setTimeout(() => this.remove(id), 3000) 15 | }, 16 | remove(id) { 17 | this.notifications = this.notifications.filter(n => n.id !== id) 18 | } 19 | }) 20 | 21 | export const notify = function (Alpine) { 22 | Alpine.magic('notify', () => (message, type) => 23 | window.dispatchEvent(new CustomEvent('notify', { 24 | detail: { message, type } 25 | })) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /resources/js/save-chat.js: -------------------------------------------------------------------------------- 1 | export const saveChat = () => ({ 2 | async save() { 3 | if (this.$wire.message === '') return; 4 | await this.$wire.save(); 5 | this.$wire.message = ''; 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /resources/views/components/action-message.blade.php: -------------------------------------------------------------------------------- 1 | @props(['on']) 2 | 3 |
merge(['class' => 'text-sm text-gray-600 dark:text-gray-400']) }}> 9 | {{ $slot->isEmpty() ? 'Saved.' : $slot }} 10 |
11 | -------------------------------------------------------------------------------- /resources/views/components/application-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/components/auth-session-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |
merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}> 5 | {{ $status }} 6 |
7 | @endif 8 | -------------------------------------------------------------------------------- /resources/views/components/danger-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-hidden focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700']) 2 | 3 | @php 4 | $alignmentClasses = match ($align) { 5 | 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', 6 | 'top' => 'origin-top', 7 | default => 'ltr:origin-top-right rtl:origin-top-left end-0', 8 | }; 9 | 10 | $width = match ($width) { 11 | '48' => 'w-48', 12 | default => $width, 13 | }; 14 | @endphp 15 | 16 |
17 |
18 | {{ $trigger }} 19 |
20 | 21 | 35 |
36 | -------------------------------------------------------------------------------- /resources/views/components/icons/add.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/components/icons/check.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 3 | fill="none" 4 | stroke="currentColor" 5 | viewBox="0 0 24 24" 6 | > 7 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/components/icons/close.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 3 | fill="none" 4 | stroke="currentColor" 5 | viewBox="0 0 24 24" 6 | > 7 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/components/icons/edit.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 8 | > 9 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/components/icons/info.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 3 | fill="none" 4 | stroke="currentColor" 5 | viewBox="0 0 24 24" 6 | > 7 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/components/icons/reply.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 8 | > 9 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/components/icons/trash.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 8 | > 9 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/components/icons/warning.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-5 h-5']) }} 3 | fill="none" 4 | stroke="currentColor" 5 | viewBox="0 0 24 24" 6 | > 7 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/components/icons/x.blade.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props(['messages']) 2 | 3 | @if ($messages) 4 |
    merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}> 5 | @foreach ((array) $messages as $message) 6 |
  • {{ $message }}
  • 7 | @endforeach 8 |
9 | @endif 10 | -------------------------------------------------------------------------------- /resources/views/components/input-label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/views/components/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $title ?? config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 |
19 | 20 | 21 | 22 | @if (isset($header)) 23 |
24 |
25 | {{ $header }} 26 |
27 |
28 | @endif 29 | 30 | 31 |
32 | {{ $slot }} 33 |
34 |
35 | @livewireScriptConfig 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/views/components/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | {{ $slot }} 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name', 3 | 'show' => false, 4 | 'maxWidth' => '2xl' 5 | ]) 6 | 7 | @php 8 | $maxWidth = [ 9 | 'sm' => 'sm:max-w-sm', 10 | 'md' => 'sm:max-w-md', 11 | 'lg' => 'sm:max-w-lg', 12 | 'xl' => 'sm:max-w-xl', 13 | '2xl' => 'sm:max-w-2xl', 14 | ][$maxWidth]; 15 | @endphp 16 | 17 |
52 |
63 |
64 |
65 | 66 |
76 | {{ $slot }} 77 |
78 |
79 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-hidden focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-hidden focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/notifications.blade.php: -------------------------------------------------------------------------------- 1 |
5 | 73 |
74 | -------------------------------------------------------------------------------- /resources/views/components/primary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-hidden focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out' 6 | : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-hidden focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/select-input.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'disabled' => false, 3 | 'options' => [], 4 | 'selected' => null, 5 | 'name' => '', 6 | 'id' => '', 7 | 'placeholder' => '', 8 | 'multiple' => false, 9 | ]) 10 | 11 | @php 12 | $id = $id ?: $name; 13 | @endphp 14 | 15 | 30 | -------------------------------------------------------------------------------- /resources/views/components/text-input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-xs']) !!}> 4 | -------------------------------------------------------------------------------- /resources/views/livewire/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrPunyapal/livewire-chat-app/798cbdb6966a27b0aba2ba5e73104bef752d5d13/resources/views/livewire/.gitkeep -------------------------------------------------------------------------------- /resources/views/livewire/chats/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | @if ($room !== null) 6 |
9 | {{ $room->user->name }} 14 |
15 |

{{ $room->name }}

16 | @else 17 |

18 | Please select room. 19 |

20 | @endif 21 |
22 |
23 |
25 | @if($room !== null) 26 | 30 | @endif 31 |
32 | @if ($room !== null) 33 | 34 | @endif 35 |
36 | @script 37 | 42 | @endscript 43 |
44 | -------------------------------------------------------------------------------- /resources/views/livewire/chats/list-chats.blade.php: -------------------------------------------------------------------------------- 1 |
5 | @foreach ($chats as $chat) 6 | 10 | @endforeach 11 | 12 | @if ($offset === 0 && $chats->isEmpty()) 13 |
14 |
15 |

No chats found

16 |
17 |
18 | @endif 19 | 20 | @if ($chats->count() === $limit) 21 | 27 | @else 28 |
29 |

End of the chat

30 |
31 | @endif 32 | 33 | @script 34 | 48 | @endscript 49 |
50 | -------------------------------------------------------------------------------- /resources/views/livewire/chats/show.blade.php: -------------------------------------------------------------------------------- 1 |
$isCurrentUser, 4 | ])> 5 |
6 | {{ $chat->user->name }} 11 |
12 | 13 |
14 |
$isCurrentUser])> 15 | 16 | {{ $chat->user->name }} 17 | 18 | @if ($chat->updated_at > $chat->created_at) 19 | (edited) 20 | @endif 21 |
22 | 23 |
$isCurrentUser, 26 | ])> 27 | 28 |
!$isCurrentUser, 31 | '-right-5 translate-x-full' => $isCurrentUser, 32 | ])> 33 | @if($isCurrentUser) 34 | 41 | 42 | @if($chat->deleted_at === null) 43 | 50 | @endif 51 | @endif 52 | 53 | 60 |
61 | 62 |
$isCurrentUser, 65 | 'bg-gray-100 dark:bg-gray-700 dark:text-gray-200 rounded-tl-none' => !$isCurrentUser, 66 | ])> 67 | @if($chat->deleted_at === null) 68 | @if ($chat->parent) 69 |
71 |

73 | 76 | @if ($chat->parent->deleted_at === null) 77 | {{ Str::limit($chat->parent->message, 100) }} 78 | @else 79 | 80 | This message has been deleted. 81 | 82 | @endif 83 |

84 |
85 | @endif 86 |

87 | {{ $chat->message }} 88 |

89 | @else 90 |

91 | This message has been deleted. 92 |

93 | @endif 94 |
95 |
96 | 97 |
$isCurrentUser])> 98 | {{ $chat->updated_at->diffForHumans() }} 99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 4 |
5 | 6 |
7 | 8 |
9 | 10 | 11 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | {{ __('Confirm') }} 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 4 |
5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | {{ __('Email Password Reset Link') }} 20 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/auth/login.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 31 |
32 | 33 |
34 | @if (Route::has('password.request')) 35 | 36 | {{ __('Forgot your password?') }} 37 | 38 | @endif 39 | 40 | 41 | {{ __('Log in') }} 42 | 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/auth/register.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | 25 | 26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | {{ __('Already registered?') }} 43 | 44 | 45 | 46 | {{ __('Register') }} 47 | 48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | {{ __('Reset Password') }} 31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/chats.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/dashboard.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | {{ __('Dashboard') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | {{ __("You're logged in!") }} 13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /resources/views/livewire/pages/profile.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | {{ __('Profile') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /resources/views/livewire/profile/delete-user-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Delete Account') }} 5 |

6 | 7 |

8 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 9 |

10 |
11 | 12 | {{ __('Delete Account') }} 16 | 17 | 18 |
19 | 20 |

21 | {{ __('Are you sure you want to delete your account?') }} 22 |

23 | 24 |

25 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 26 |

27 | 28 |
29 | 30 | 31 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | {{ __('Cancel') }} 46 | 47 | 48 | 49 | {{ __('Delete Account') }} 50 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /resources/views/livewire/profile/update-password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Update Password') }} 5 |

6 | 7 |

8 | {{ __('Ensure your account is using a long, random password to stay secure.') }} 9 |

10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 | {{ __('Save') }} 33 | 34 | 35 | {{ __('Saved.') }} 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /resources/views/livewire/profile/update-profile-information-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Profile Information') }} 5 |

6 | 7 |

8 | {{ __("Update your account's profile information and email address.") }} 9 |

10 |
11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! auth()->user()->hasVerifiedEmail()) 25 |
26 |

27 | {{ __('Your email address is unverified.') }} 28 | 29 | 32 |

33 | 34 | @if (session('status') === 'verification-link-sent') 35 |

36 | {{ __('A new verification link has been sent to your email address.') }} 37 |

38 | @endif 39 |
40 | @endif 41 |
42 | 43 |
44 | {{ __('Save') }} 45 | 46 | 47 | {{ __('Saved.') }} 48 | 49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /resources/views/livewire/rooms/create.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | {{ __('Create') }} 18 | 19 | 20 | {{ __('Room created.') }} 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /resources/views/livewire/rooms/index.blade.php: -------------------------------------------------------------------------------- 1 | 84 | -------------------------------------------------------------------------------- /resources/views/livewire/welcome/navigation.blade.php: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 13 | Route::get('register', Register::class) 14 | ->name('register'); 15 | 16 | Route::get('login', Login::class) 17 | ->name('login'); 18 | 19 | Route::get('forgot-password', ForgotPassword::class) 20 | ->name('password.request'); 21 | 22 | Route::get('reset-password/{token}', ResetPassword::class) 23 | ->name('password.reset'); 24 | }); 25 | 26 | Route::middleware('auth')->group(function () { 27 | Route::get('confirm-password', ConfirmPassword::class) 28 | ->name('password.confirm'); 29 | }); 30 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 10 | }); 11 | 12 | Broadcast::channel('chats.{roomId}', function (User $user, int $roomId) { 13 | return $user->rooms() 14 | ->where('rooms.id', $roomId) 15 | ->exists(); 16 | }); 17 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 10 | })->purpose('Display an inspiring quote')->hourly(); 11 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function (): void { 14 | Route::get('dashboard', Dashboard::class) 15 | ->name('dashboard'); 16 | 17 | Route::get('profile', Profile::class) 18 | ->name('profile'); 19 | 20 | Route::get('chats', Chats::class) 21 | ->name('chats'); 22 | }); 23 | 24 | require __DIR__.'/auth.php'; 25 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 11 | 12 | $response 13 | ->assertOk() 14 | ->assertSeeLivewire(Login::class); 15 | }); 16 | 17 | test('navigation menu can be rendered', function () { 18 | $user = User::factory()->create(); 19 | 20 | $this->actingAs($user); 21 | 22 | $response = $this->get('/dashboard'); 23 | 24 | $response 25 | ->assertOk() 26 | ->assertSeeLivewire(Navigation::class); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | $response = $this->actingAs($user)->get('/confirm-password'); 12 | 13 | $response 14 | ->assertSeeLivewire(ConfirmPassword::class) 15 | ->assertStatus(200); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 14 | 15 | $response 16 | ->assertSeeLivewire(ForgotPassword::class) 17 | ->assertStatus(200); 18 | }); 19 | 20 | test('reset password screen can be rendered', function () { 21 | Notification::fake(); 22 | 23 | $user = User::factory()->create(); 24 | 25 | Password::sendResetLink([ 26 | 'email' => $user->email, 27 | ]); 28 | 29 | Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) { 30 | $response = $this->get('/reset-password/'.$notification->token); 31 | 32 | $response 33 | ->assertSeeLivewire(ResetPassword::class) 34 | ->assertStatus(200); 35 | 36 | return true; 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 9 | 10 | $response 11 | ->assertOk() 12 | ->assertSeeLivewire(Register::class); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/Feature/ChatsTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $this->actingAs($user) 18 | ->get('/chats') 19 | ->assertSeeLivewire(Chats::class) 20 | ->assertSeeLivewire(ChatsIndex::class) 21 | ->assertSeeLivewire(RoomsIndex::class) 22 | ->assertSeeLivewire(CreateRoom::class) 23 | ->assertDontSeeLivewire(CreateChat::class) 24 | ->assertOk(); 25 | }); 26 | 27 | test('create chat component should be there if room is selected', function (): void { 28 | $user = User::factory()->create(); 29 | $room = Room::factory() 30 | ->hasAttached($user, relationship: 'users') 31 | ->create(); 32 | 33 | $this->actingAs($user) 34 | ->get('/chats?roomId='.$room->id) 35 | ->assertSeeLivewire(Chats::class) 36 | ->assertSeeLivewire(ChatsIndex::class) 37 | ->assertSeeLivewire(RoomsIndex::class) 38 | ->assertSeeLivewire(CreateRoom::class) 39 | ->assertSeeLivewire(CreateChat::class) 40 | ->assertSeeLivewire(ListChats::class) 41 | ->assertOk(); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | 14 | $this->actingAs($user); 15 | 16 | $response = $this->get('/profile'); 17 | 18 | $response 19 | ->assertOk() 20 | ->assertSeeLivewire(Profile::class) 21 | ->assertSeeLivewire(UpdateProfileInformationForm::class) 22 | ->assertSeeLivewire(UpdatePasswordForm::class) 23 | ->assertSeeLivewire(DeleteUserForm::class); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/Feature/WelcomeTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 7 | 8 | $response->assertStatus(200); 9 | 10 | $response->assertSeeLivewire('welcome.navigation'); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature', 'Unit'); 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Expectations 24 | |-------------------------------------------------------------------------- 25 | | 26 | | When you're writing tests, you often need to check that values meet certain conditions. The 27 | | "expect()" function gives you access to a set of "expectations" methods that you can use 28 | | to assert different things. Of course, you may extend the Expectation API at any time. 29 | | 30 | */ 31 | 32 | expect()->extend('toBeOne', function () { 33 | return $this->toBe(1); 34 | }); 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Functions 39 | |-------------------------------------------------------------------------- 40 | | 41 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 42 | | project that you don't want to repeat in every file. Here you can also expose helpers as 43 | | global functions to help you to reduce the number of lines of code in your test files. 44 | | 45 | */ 46 | 47 | function something() 48 | { 49 | // .. 50 | } 51 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | create(); 11 | $event = new ChatCreated($chat->id, $chat->room_id); 12 | 13 | expect($event->chatId)->toEqual($chat->id); 14 | expect($event->roomId)->toEqual($chat->room_id); 15 | expect($event->broadcastOn())->toEqual([ 16 | new PrivateChannel('chats.'.$chat->room_id), 17 | ]); 18 | expect($event->broadcastAs())->toEqual('chat-created'); 19 | }); 20 | 21 | it('can be dispatched', function () { 22 | Event::fake(); 23 | $chat = Chat::factory()->create(); 24 | ChatCreated::dispatch($chat->id, $chat->room_id); 25 | Event::assertDispatched(ChatCreated::class, function (ChatCreated $event) use ($chat) { 26 | return $event->chatId === $chat->id && $event->roomId === $chat->room_id; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Unit/Events/ChatUpdatedTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | $event = new ChatUpdated($chat->id, $chat->room_id); 12 | 13 | expect($event->chatId)->toEqual($chat->id); 14 | expect($event->roomId)->toEqual($chat->room_id); 15 | expect($event->broadcastOn())->toEqual([ 16 | new PrivateChannel('chats.'.$chat->room_id), 17 | ]); 18 | expect($event->broadcastAs())->toEqual('chat-updated'); 19 | }); 20 | 21 | it('can be dispatched', function () { 22 | Event::fake(); 23 | $chat = Chat::factory()->create(); 24 | ChatUpdated::dispatch($chat->id, $chat->room_id); 25 | Event::assertDispatched(ChatUpdated::class, function (ChatUpdated $event) use ($chat) { 26 | return $event->chatId === $chat->id && $event->roomId === $chat->room_id; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Chats/IndexTest.php: -------------------------------------------------------------------------------- 1 | create(); 12 | $room = Room::factory() 13 | ->hasAttached($user, relationship: 'users') 14 | ->create(); 15 | 16 | Livewire::actingAs($user) 17 | ->test(Index::class, ['roomId' => $room->id]) 18 | ->assertViewHas('room', $room) 19 | ->assertDontSee('Please select room.'); 20 | }); 21 | 22 | it('renders without room', function () { 23 | $user = User::factory()->create(); 24 | 25 | Livewire::actingAs($user) 26 | ->test(Index::class) 27 | ->assertViewHas('room', null) 28 | ->assertSee('Please select room.'); 29 | }); 30 | 31 | it('selects room', function () { 32 | $user = User::factory()->create(); 33 | $room = Room::factory()->create(); 34 | 35 | Livewire::actingAs($user) 36 | ->test(Index::class) 37 | ->dispatch('room-selected', id: $room->id) 38 | ->assertSet('roomId', $room->id); 39 | }); 40 | 41 | it('renders room only if user is a member', function () { 42 | $user = User::factory()->create(); 43 | $room = Room::factory()->create(); 44 | $room->users()->attach($user); 45 | 46 | Livewire::actingAs($user) 47 | ->test(Index::class, ['roomId' => $room->id]) 48 | ->assertViewHas('room', $room) 49 | ->assertDontSee('Please select room.'); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Chats/ListChatTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | $room = Room::factory() 14 | ->hasAttached($user, relationship: 'users') 15 | ->create(); 16 | 17 | Livewire::actingAs($user) 18 | ->test(ListChats::class, ['roomId' => $room->id]) 19 | ->assertViewHas('chats', new Collection) 20 | ->assertSee('No chats found'); 21 | }); 22 | 23 | it('renders with room with chats', function () { 24 | $user = User::factory()->create(); 25 | $room = Room::factory() 26 | ->hasAttached($user, relationship: 'users') 27 | ->create(); 28 | 29 | $chats = Chat::factory(3) 30 | ->for($room) 31 | ->for($user, 'user') 32 | ->create(); 33 | 34 | Livewire::actingAs($user) 35 | ->test(ListChats::class, ['roomId' => $room->id]) 36 | ->assertViewHas('chats', $chats) 37 | ->assertDontSee('No chats found'); 38 | }); 39 | 40 | it('dispatch the chats:loaded event if offset is greater than zero', function () { 41 | $user = User::factory()->create(); 42 | $room = Room::factory() 43 | ->hasAttached($user, relationship: 'users') 44 | ->create(); 45 | 46 | Chat::factory(1) 47 | ->for($room) 48 | ->for($user, 'user') 49 | ->create(); 50 | 51 | $chats = Chat::factory(3) 52 | ->for($room) 53 | ->for($user, 'user') 54 | ->create(); 55 | 56 | Livewire::actingAs($user) 57 | ->test(ListChats::class, ['roomId' => $room->id, 'offset' => 1]) 58 | ->assertViewHas('chats', $chats) 59 | ->assertDispatched('chats:loaded'); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Chats/ShowTest.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | Livewire::test(Show::class, ['chat' => $chat]) 20 | ->assertSee($chat->message) 21 | ->assertSee($chat->user->name); 22 | }); 23 | 24 | it('can edit', function (): void { 25 | $chat = Chat::factory()->create(); 26 | 27 | Livewire::actingAs($chat->user) 28 | ->test(Show::class, ['chat' => $chat]) 29 | ->call('edit') 30 | ->assertDispatched('chat-editing', 31 | chatId: $chat->id, 32 | message: $chat->message, 33 | ); 34 | }); 35 | 36 | it('can reply', function (): void { 37 | $chat = Chat::factory()->create(); 38 | 39 | Livewire::actingAs($chat->user) 40 | ->test(Show::class, ['chat' => $chat]) 41 | ->call('reply') 42 | ->assertDispatched('chat-replying', 43 | chatId: $chat->id, 44 | message: $chat->message, 45 | ); 46 | }); 47 | 48 | it('can refresh when parent chat is updated', function (): void { 49 | $parentChat = Chat::factory()->create(); 50 | $chat = Chat::factory()->create([ 51 | 'parent_id' => $parentChat->id, 52 | 'message' => 'Original message', 53 | ]); 54 | 55 | $component = Livewire::actingAs($chat->user) 56 | ->test(Show::class, ['chat' => $chat]); 57 | 58 | $parentChat->update(['message' => 'Updated message']); 59 | 60 | $component->assertSee('Original message') 61 | ->dispatch('chat:updated.'.$parentChat->id) 62 | ->assertSee('Updated message'); 63 | }); 64 | 65 | it('only shows the edit button if the user is the owner of the chat', function (): void { 66 | $chat = Chat::factory()->create(); 67 | 68 | Livewire::actingAs($chat->user) 69 | ->test(Show::class, ['chat' => $chat]) 70 | ->assertSeeHtml('wire:click="edit"'); 71 | 72 | Livewire::actingAs(User::factory()->create()) 73 | ->test(Show::class, ['chat' => $chat]) 74 | ->assertDontSeeHtml('wire:click="edit"'); 75 | }); 76 | 77 | it('can delete', function (): void { 78 | $chat = Chat::factory()->create(); 79 | 80 | Livewire::actingAs($chat->user) 81 | ->test(Show::class, ['chat' => $chat]) 82 | ->call('delete') 83 | ->assertDispatched('chat:updated.'.$chat->id); 84 | 85 | expect($chat->fresh()->deleted_at)->not()->toBeNull(); 86 | 87 | Event::assertDispatched(ChatUpdated::class, function (ChatUpdated $event) use ($chat): bool { 88 | return $event->chatId === $chat->id && $event->roomId === $chat->room_id; 89 | }); 90 | }); 91 | 92 | it('can not delete if the user is not the owner of the chat', function (): void { 93 | $chat = Chat::factory()->create(); 94 | 95 | Livewire::actingAs(User::factory()->create()) 96 | ->test(Show::class, ['chat' => $chat]) 97 | ->call('delete') 98 | ->assertForbidden(); 99 | }); 100 | 101 | it('only shows the delete button if the user is the owner of the chat', function (): void { 102 | $chat = Chat::factory()->create(); 103 | 104 | Livewire::actingAs($chat->user) 105 | ->test(Show::class, ['chat' => $chat]) 106 | ->assertSeeHtml('wire:click="delete"'); 107 | 108 | Livewire::actingAs(User::factory()->create()) 109 | ->test(Show::class, ['chat' => $chat]) 110 | ->assertDontSeeHtml('wire:click="delete"'); 111 | }); 112 | 113 | it('only shows the delete button if the chat is not deleted', function (): void { 114 | $chat = Chat::factory()->create(); 115 | 116 | Livewire::actingAs($chat->user) 117 | ->test(Show::class, ['chat' => $chat]) 118 | ->assertSeeHtml('wire:click="delete"'); 119 | 120 | $chat->touch('deleted_at'); 121 | 122 | Livewire::actingAs($chat->user) 123 | ->test(Show::class, ['chat' => $chat]) 124 | ->assertDontSeeHtml('wire:click="delete"'); 125 | }); 126 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Layout/NavigationTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | $this->actingAs($user); 13 | 14 | $component = Livewire::test(Navigation::class); 15 | 16 | $component->call('logout'); 17 | 18 | $component 19 | ->assertHasNoErrors() 20 | ->assertRedirect('/'); 21 | 22 | $this->assertGuest(); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Pages/Auth/ConfirmPasswordTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | $this->actingAs($user); 13 | 14 | $component = Livewire::test(ConfirmPassword::class) 15 | ->set('password', 'password'); 16 | 17 | $component->call('confirmPassword'); 18 | 19 | $component 20 | ->assertRedirect('/dashboard') 21 | ->assertHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $this->actingAs($user); 28 | 29 | $component = Livewire::test(ConfirmPassword::class) 30 | ->set('password', 'wrong-password'); 31 | 32 | $component->call('confirmPassword'); 33 | 34 | $component 35 | ->assertNoRedirect() 36 | ->assertHasErrors('password'); 37 | }); 38 | 39 | test('unauthenticated users are redirected to login', function () { 40 | $component = Livewire::test(ConfirmPassword::class) 41 | ->set('password', 'any-password'); 42 | 43 | $component->call('confirmPassword'); 44 | 45 | $component->assertRedirect(route('login')); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Pages/Auth/ForgotPasswordTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | 14 | Livewire::test(ForgotPassword::class) 15 | ->set('email', $user->email) 16 | ->call('sendPasswordResetLink'); 17 | 18 | Notification::assertSentTo($user, ResetPasswordNotification::class); 19 | }); 20 | 21 | test('reset password link is not sent to invalid email', function () { 22 | Notification::fake(); 23 | 24 | Livewire::test(ForgotPassword::class) 25 | ->set('email', 'invalid-email@example.com') 26 | ->call('sendPasswordResetLink'); 27 | 28 | Notification::assertNothingSent(); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Pages/Auth/LoginTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | $component = Livewire::test(Login::class) 12 | ->set('form.email', $user->email) 13 | ->set('form.password', 'password'); 14 | 15 | $component->call('login'); 16 | 17 | $component 18 | ->assertHasNoErrors() 19 | ->assertRedirect(route('dashboard', absolute: false)); 20 | 21 | $this->assertAuthenticated(); 22 | }); 23 | 24 | test('users can not authenticate with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $component = Livewire::test(Login::class) 28 | ->set('form.email', $user->email) 29 | ->set('form.password', 'wrong-password'); 30 | 31 | $component->call('login'); 32 | 33 | $component 34 | ->assertHasErrors(['form.email']) 35 | ->assertNoRedirect(); 36 | 37 | $this->assertGuest(); 38 | }); 39 | 40 | test('login form is rate limited', function () { 41 | $user = User::factory()->create(); 42 | 43 | $component = Livewire::test(Login::class) 44 | ->set('form.email', $user->email) 45 | ->set('form.password', 'wrong-password'); 46 | 47 | for ($i = 0; $i < 6; $i++) { 48 | $component->call('login'); 49 | } 50 | 51 | $component 52 | ->assertHasErrors(['form.email']) 53 | ->assertNoRedirect(); 54 | 55 | $this->assertGuest(); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Pages/Auth/RegisterTest.php: -------------------------------------------------------------------------------- 1 | set('name', 'Test User') 11 | ->set('email', 'test@example.com') 12 | ->set('password', 'password') 13 | ->set('password_confirmation', 'password'); 14 | 15 | $component->call('register'); 16 | 17 | $component->assertRedirect(route('dashboard', absolute: false)); 18 | 19 | $this->assertAuthenticated(); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Pages/Auth/ResetPasswordTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | Livewire::test(ForgotPassword::class) 18 | ->set('email', $user->email) 19 | ->call('sendPasswordResetLink'); 20 | 21 | Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) use ($user) { 22 | $component = Livewire::test(ResetPassword::class, ['token' => $notification->token]) 23 | ->set('email', $user->email) 24 | ->set('password', 'password') 25 | ->set('password_confirmation', 'password'); 26 | 27 | $component->call('resetPassword'); 28 | 29 | $component 30 | ->assertRedirect('/login') 31 | ->assertHasNoErrors(); 32 | 33 | return true; 34 | }); 35 | }); 36 | 37 | test('password cannot be reset with invalid token', function () { 38 | Notification::fake(); 39 | 40 | $user = User::factory()->create(); 41 | 42 | Livewire::test(ForgotPassword::class) 43 | ->set('email', $user->email) 44 | ->call('sendPasswordResetLink'); 45 | 46 | Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) use ($user) { 47 | $component = Livewire::test(ResetPassword::class, ['token' => 'invalid-token']) 48 | ->set('email', $user->email) 49 | ->set('password', 'password') 50 | ->set('password_confirmation', 'password'); 51 | 52 | $component->call('resetPassword'); 53 | 54 | $component 55 | ->assertHasErrors(); 56 | 57 | return true; 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Profile/DeleteUserFormTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | $this->actingAs($user); 13 | 14 | $component = Livewire::test(DeleteUserForm::class) 15 | ->set('password', 'password') 16 | ->call('deleteUser'); 17 | 18 | $component 19 | ->assertHasNoErrors() 20 | ->assertRedirect('/'); 21 | 22 | $this->assertGuest(); 23 | $this->assertNull($user->fresh()); 24 | }); 25 | 26 | test('correct password must be provided to delete account', function () { 27 | $user = User::factory()->create(); 28 | 29 | $this->actingAs($user); 30 | 31 | $component = Livewire::test(DeleteUserForm::class) 32 | ->set('password', 'wrong-password') 33 | ->call('deleteUser'); 34 | 35 | $component 36 | ->assertHasErrors('password') 37 | ->assertNoRedirect(); 38 | 39 | $this->assertNotNull($user->fresh()); 40 | }); 41 | 42 | test('unauthenticated users are redirected to login', function () { 43 | $component = Livewire::test(DeleteUserForm::class) 44 | ->set('password', 'any-password'); 45 | 46 | $component->call('deleteUser'); 47 | 48 | $component->assertRedirect(route('login')); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Profile/UpdatePasswordFormTest.php: -------------------------------------------------------------------------------- 1 | create(); 12 | 13 | $this->actingAs($user); 14 | 15 | $component = Livewire::test(UpdatePasswordForm::class) 16 | ->set('current_password', 'password') 17 | ->set('password', 'new-password') 18 | ->set('password_confirmation', 'new-password') 19 | ->call('updatePassword'); 20 | 21 | $component 22 | ->assertHasNoErrors() 23 | ->assertNoRedirect(); 24 | 25 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 26 | }); 27 | 28 | test('correct password must be provided to update password', function () { 29 | $user = User::factory()->create(); 30 | 31 | $this->actingAs($user); 32 | 33 | $component = Livewire::test(UpdatePasswordForm::class) 34 | ->set('current_password', 'wrong-password') 35 | ->set('password', 'new-password') 36 | ->set('password_confirmation', 'new-password') 37 | ->call('updatePassword'); 38 | 39 | $component 40 | ->assertHasErrors(['current_password']) 41 | ->assertNoRedirect(); 42 | }); 43 | 44 | test('unauthenticated users are redirected to login', function () { 45 | $component = Livewire::test(UpdatePasswordForm::class) 46 | ->set('current_password', 'password') 47 | ->set('password', 'new-password') 48 | ->set('password_confirmation', 'new-password') 49 | ->call('updatePassword'); 50 | 51 | $component->assertRedirect(route('login')); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Profile/UpdateProfileInformationFormTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | $this->actingAs($user); 13 | 14 | $component = Livewire::test(UpdateProfileInformationForm::class) 15 | ->set('name', 'Test User') 16 | ->set('email', 'test@example.com') 17 | ->call('updateProfileInformation'); 18 | 19 | $component 20 | ->assertHasNoErrors() 21 | ->assertNoRedirect(); 22 | 23 | $user->refresh(); 24 | 25 | $this->assertSame('Test User', $user->name); 26 | $this->assertSame('test@example.com', $user->email); 27 | }); 28 | 29 | test('email verification status is unchanged when the email address is unchanged', function () { 30 | $user = User::factory()->create(); 31 | 32 | $this->actingAs($user); 33 | 34 | $component = Livewire::test(UpdateProfileInformationForm::class) 35 | ->set('name', 'Test User') 36 | ->set('email', $user->email) 37 | ->call('updateProfileInformation'); 38 | 39 | $component 40 | ->assertHasNoErrors() 41 | ->assertNoRedirect(); 42 | 43 | $this->assertNotNull($user->refresh()->email_verified_at); 44 | }); 45 | 46 | test('unauthenticated users are redirected to login', function () { 47 | $component = Livewire::test(UpdateProfileInformationForm::class); 48 | 49 | $component->assertRedirect(route('login')); 50 | 51 | $component = Livewire::actingAs(User::factory()->create()) 52 | ->test(UpdateProfileInformationForm::class) 53 | ->set('name', 'Test User') 54 | ->set('email', 'test@example.com'); 55 | 56 | auth()->logout(); 57 | 58 | $component->call('updateProfileInformation'); 59 | 60 | $component->assertRedirect(route('login')); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Rooms/CreateTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create()); 11 | 12 | Livewire::test(Create::class) 13 | ->assertStatus(200) 14 | ->assertViewIs('livewire.rooms.create'); 15 | }); 16 | 17 | it('validates the name field', function () { 18 | $this->actingAs(User::factory()->create()); 19 | 20 | Livewire::test(Create::class) 21 | ->set('name', '') 22 | ->call('store') 23 | ->assertHasErrors(['name', 'members']); 24 | 25 | Livewire::test(Create::class) 26 | ->set('name', 'A') 27 | ->call('store') 28 | ->assertHasErrors(['name']); 29 | 30 | Livewire::test(Create::class) 31 | ->set('name', str_repeat('A', 81)) 32 | ->call('store') 33 | ->assertHasErrors(['name']); 34 | 35 | Livewire::test(Create::class) 36 | ->set('members', []) 37 | ->call('store') 38 | ->assertHasErrors(['members']); 39 | 40 | Livewire::test(Create::class) 41 | ->set('members', [999]) 42 | ->call('store') 43 | ->assertHasErrors(['members.*']); 44 | }); 45 | 46 | it('can create a chat room', function () { 47 | $user = User::factory()->create(); 48 | 49 | $this->actingAs($user); 50 | 51 | $member = User::factory()->create(); 52 | 53 | Livewire::test(Create::class) 54 | ->set('name', 'Test Room') 55 | ->set('members', [$member->id]) 56 | ->call('store') 57 | ->assertDispatched('room-created') 58 | ->assertDispatched('room-selected'); 59 | 60 | $this->assertDatabaseHas('rooms', ['name' => 'Test Room']); 61 | $this->assertDatabaseHas('members', ['user_id' => $member->id, 'room_id' => 1]); 62 | $this->assertDatabaseHas('members', ['user_id' => $user->id, 'room_id' => 1]); 63 | }); 64 | 65 | it('redirects to login page if user is not authenticated', function () { 66 | Livewire::test(Create::class) 67 | ->call('store') 68 | ->assertRedirect(route('login')); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Rooms/IndexTest.php: -------------------------------------------------------------------------------- 1 | create(); 13 | 14 | $rooms = Room::factory(5) 15 | ->hasAttached($user, relationship: 'users') 16 | ->create(); 17 | 18 | $room = Room::factory()->create(); 19 | 20 | Livewire::actingAs($user) 21 | ->test(Index::class) 22 | ->assertViewHas('rooms', $rooms) 23 | ->assertDontSee($room->name) 24 | ->assertDontSee('No rooms found'); 25 | }); 26 | 27 | test('sidebar component without rooms', function () { 28 | $user = User::factory()->create(); 29 | 30 | Livewire::actingAs($user) 31 | ->test(Index::class) 32 | ->assertSee('No rooms found'); 33 | }); 34 | 35 | test('sidebar component can show active room', function () { 36 | $user = User::factory()->create(); 37 | $room = Room::factory()->create(); 38 | 39 | Livewire::actingAs($user) 40 | ->test(Index::class) 41 | ->dispatch('room-selected', id: $room->id) 42 | ->assertSet('activeRoomId', $room->id); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/Unit/Models/ChatTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 11 | expect(array_keys($chat->toArray()))->toEqual([ 12 | 'id', 13 | 'parent_id', 14 | 'user_id', 15 | 'room_id', 16 | 'message', 17 | 'created_at', 18 | 'updated_at', 19 | 'deleted_at', 20 | ]); 21 | }); 22 | 23 | test('relationships', function () { 24 | $chat = Chat::factory() 25 | ->for(Chat::factory(), 'parent') 26 | ->create(); 27 | 28 | expect($chat->user)->toBeInstanceOf(User::class); 29 | expect($chat->room)->toBeInstanceOf(Room::class); 30 | expect($chat->parent)->toBeInstanceOf(Chat::class); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/Unit/Models/MemberTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 11 | expect(array_keys($member->toArray()))->toEqual([ 12 | 'id', 13 | 'room_id', 14 | 'user_id', 15 | 'created_at', 16 | 'updated_at', 17 | ]); 18 | }); 19 | 20 | test('relationships', function () { 21 | $member = Member::factory() 22 | ->create(); 23 | 24 | expect($member->room)->toBeInstanceOf(Room::class); 25 | expect($member->user)->toBeInstanceOf(User::class); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Unit/Models/RoomTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 12 | expect(array_keys($room->toArray()))->toEqual([ 13 | 'id', 14 | 'name', 15 | 'description', 16 | 'user_id', 17 | 'created_at', 18 | 'updated_at', 19 | ]); 20 | }); 21 | 22 | test('relationships', function () { 23 | $room = Room::factory() 24 | ->has(User::factory()->count(3), 'users') 25 | ->create(); 26 | 27 | Chat::factory() 28 | ->count(3) 29 | ->for($room) 30 | ->for($room->users->first(), 'user') 31 | ->create(); 32 | 33 | expect($room->user)->toBeInstanceOf(User::class) 34 | ->and($room->users)->toBeInstanceOf(Collection::class) 35 | ->and($room->users)->each->toBeInstanceOf(User::class) 36 | ->and($room->chats)->toBeInstanceOf(Collection::class) 37 | ->and($room->chats)->each->toBeInstanceOf(Chat::class); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/Unit/Models/UserTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 11 | expect(array_keys($user->toArray()))->toEqual([ 12 | 'id', 13 | 'name', 14 | 'email', 15 | 'email_verified_at', 16 | 'created_at', 17 | 'updated_at', 18 | ]); 19 | }); 20 | 21 | test('relationships', function () { 22 | $user = User::factory() 23 | ->has(Room::factory()->count(3)) 24 | ->create(); 25 | 26 | expect($user->rooms)->toBeInstanceOf(Collection::class); 27 | expect($user->rooms)->each->toBeInstanceOf(Room::class); 28 | }); 29 | 30 | test('attributes', function () { 31 | $user = User::factory()->create(); 32 | 33 | expect($user->profile)->toBeUrl(); 34 | }); 35 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: [ 9 | 'resources/css/app.css', 10 | 'resources/js/app.js', 11 | ], 12 | refresh: true, 13 | }), 14 | tailwindcss(), 15 | ], 16 | }); 17 | --------------------------------------------------------------------------------