├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── build.js ├── composer.json ├── config └── filachat.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ ├── 00001_create_filachat_agents_table.php.stub │ ├── 00002_create_filachat_conversations_table.php.stub │ └── 00003_create_filachat_messages_table.php.stub ├── postcss.config.cjs ├── resources ├── css │ └── filachat.css ├── dist │ └── .gitkeep ├── js │ └── index.js ├── lang │ ├── ar.json │ └── en │ │ └── filachat.php └── views │ ├── .gitkeep │ └── filachat │ ├── components │ ├── chat-box.blade.php │ ├── chat-list.blade.php │ └── search-conversation.blade.php │ └── index.blade.php ├── src ├── Commands │ ├── FilaChatCommand.php │ └── FilaChatCreateAgentCommand.php ├── Enums │ └── RoleType.php ├── Events │ ├── FilaChatMessageEvent.php │ ├── FilaChatMessageReadEvent.php │ ├── FilaChatMessageReceiverIsAwayEvent.php │ └── FilaChatUserTypingEvent.php ├── Facades │ └── FilaChat.php ├── FilaChat.php ├── FilaChatPlugin.php ├── FilaChatServiceProvider.php ├── Livewire │ ├── ChatBox.php │ ├── ChatList.php │ └── SearchConversation.php ├── Models │ ├── FilaChatAgent.php │ ├── FilaChatConversation.php │ └── FilaChatMessage.php ├── Pages │ └── FilaChat.php ├── Services │ └── ChatListService.php ├── Testing │ └── TestsFilaChat.php └── Traits │ ├── CanGetOriginalFileName.php │ ├── CanValidateAudio.php │ ├── CanValidateDocument.php │ ├── CanValidateImage.php │ ├── CanValidateVideo.php │ └── HasFilaChat.php └── stubs └── .gitkeep /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filachat` will be documented in this file. 4 | 5 | ## 1.0.0 - 202X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) jaocero <199ocero@gmail.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FilaChat 2 | 3 |
4 | 5 | ![Header](https://raw.githubusercontent.com/199ocero/filachat/main/art/jaocero-filachat.jpg) 6 | 7 |
8 | 9 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/jaocero/filachat.svg?style=flat-square)](https://packagist.org/packages/jaocero/filachat) 10 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/199ocero/filachat/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/199ocero/filachat/actions?query=workflow%3Arun-tests+branch%3Amain) 11 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/199ocero/filachat/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/199ocero/filachat/actions?query=workflow%3A"Fix+PHP+code+styling"+branch%3Amain) 12 | [![Total Downloads](https://img.shields.io/packagist/dt/jaocero/filachat.svg?style=flat-square)](https://packagist.org/packages/jaocero/filachat) 13 | 14 | 15 | Filachat is a plugin for adding real-time customer support chat to your application. It provides tools for both customer and agent chat interfaces, with features for managing and maintaining conversations. You can also disable role constraints to let users chat with each other without restrictions. 16 | 17 | > [!IMPORTANT] 18 | > This plugin has two roles: `agent` and `user`. When role restrictions are enabled, `agents` cannot chat with each other, and `users` cannot chat with each other. Only `agents` and `users` can chat with each other, and vice versa. If role restrictions are disabled, `agents` and `users` can freely chat with one another without any restrictions. 19 | 20 | > [!CAUTION] 21 | > This plugin has not been tested in a production environment. Use it at your own risk. 22 | 23 | ## Installation 24 | 25 | You can install the package via composer: 26 | 27 | ```bash 28 | composer require jaocero/filachat 29 | ``` 30 | 31 | Run the following command to install FilaChat, which will take care of all migrations and configurations. 32 | 33 | ```bash 34 | php artisan filachat:install 35 | ``` 36 | 37 | You can view the full content of the config file here: [config/filachat.php](https://github.com/199ocero/filachat/blob/main/config/filachat.php) 38 | 39 | Next, execute the following command to generate assets in your public folder. 40 | 41 | ```bash 42 | php artisan filament:assets 43 | ``` 44 | 45 | > [!NOTE] 46 | > This step is optional if you want to enable role restrictions. You only need to create an agent if you want to set up role-based chat support. 47 | 48 | When you first install this plugin, you won’t have any `agents` set up yet. Agents are like admins who can provide chat support to your customers or users. 49 | 50 | To create an `agent`, use the command below: 51 | 52 | ```bash 53 | php artisan filachat:agent-create 54 | ``` 55 | 56 | Next, you need to apply the `HasFilaChat` trait to your models, whether it’s the `agent` model or the `user` model. 57 | 58 | ```php 59 | **Custom Theme Installation** 75 | > [Filament Docs](https://filamentphp.com/docs/3.x/panels/themes#creating-a-custom-theme) 76 | 77 | Then add the plugin's views to your `tailwind.config.js` file. 78 | 79 | ```js 80 | content: [ 81 | ... 82 | './vendor/jaocero/filachat/resources/views/**/**/*.blade.php', 83 | ... 84 | ] 85 | ``` 86 | 87 | ## Usage 88 | You can now use this plugin and add it to your FilamentPHP panel provider. 89 | ```php 90 | plugins([ 102 | FilaChatPlugin::make() 103 | ]); 104 | } 105 | 106 | //... 107 | } 108 | ``` 109 | 110 | > [!IMPORTANT] 111 | > To use this plugin, you need to have Laravel Reverb installed and enable FilamentPHP broadcasting in your application. 112 | 113 | For the final step, you need to set up Laravel Reverb for your application. See [Reverb](https://laravel.com/docs/11.x/reverb) for more details. After that, enable broadcasting for your FilamentPHP application by following this [guide](https://laraveldaily.com/post/configure-laravel-reverb-filament-broadcasting) by Laravel Daily. 114 | 115 | Then everytime you start your application in your local environment, you will need to run the following command to enable broadcasting: 116 | 117 | ```bash 118 | php artisan reverb:start 119 | ``` 120 | 121 | When using file uploads, Livewire has a default file size limit of 12 MB. To change this limit, you need to publish the Livewire configuration file using the command `php artisan livewire:publish --config` and then adjust the `rule`. 122 | 123 | ```php 124 | [ 129 | 'rules' => 'max:20000', // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) 130 | ], 131 | //... 132 | ]; 133 | ``` 134 | 135 | You also need to adjust the `post_max_size` and `upload_max_filesize` settings in your `php.ini` file. 136 | 137 | ```ini 138 | post_max_size = 20MB 139 | upload_max_filesize = 20MB 140 | ``` 141 | 142 | ## Testing 143 | 144 | ```bash 145 | composer test 146 | ``` 147 | 148 | ## Changelog 149 | 150 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 151 | 152 | ## Contributing 153 | 154 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 155 | 156 | ## Security Vulnerabilities 157 | 158 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 159 | 160 | ## Credits 161 | 162 | - [Jay-Are Ocero](https://github.com/199ocero) 163 | - [All Contributors](../../contributors) 164 | 165 | ## License 166 | 167 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 168 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const isDev = process.argv.includes('--dev') 4 | 5 | async function compile(options) { 6 | const context = await esbuild.context(options) 7 | 8 | if (isDev) { 9 | await context.watch() 10 | } else { 11 | await context.rebuild() 12 | await context.dispose() 13 | } 14 | } 15 | 16 | const defaultOptions = { 17 | define: { 18 | 'process.env.NODE_ENV': isDev ? `'development'` : `'production'`, 19 | }, 20 | bundle: true, 21 | mainFields: ['module', 'main'], 22 | platform: 'neutral', 23 | sourcemap: isDev ? 'inline' : false, 24 | sourcesContent: isDev, 25 | treeShaking: true, 26 | target: ['es2020'], 27 | minify: !isDev, 28 | plugins: [{ 29 | name: 'watchPlugin', 30 | setup: function (build) { 31 | build.onStart(() => { 32 | console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 33 | }) 34 | 35 | build.onEnd((result) => { 36 | if (result.errors.length > 0) { 37 | console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors) 38 | } else { 39 | console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 40 | } 41 | }) 42 | } 43 | }], 44 | } 45 | 46 | compile({ 47 | ...defaultOptions, 48 | entryPoints: ['./resources/js/index.js'], 49 | outfile: './resources/dist/filachat.js', 50 | }) 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jaocero/filachat", 3 | "description": "FilaChat is a plugin for integrating real-time customer support chat into your application. Provides tools for both customer and agent chat interfaces, with features for managing and maintaining chat conversations.", 4 | "keywords": [ 5 | "jaocero", 6 | "laravel", 7 | "filachat" 8 | ], 9 | "homepage": "https://github.com/jaocero/filachat", 10 | "support": { 11 | "issues": "https://github.com/jaocero/filachat/issues", 12 | "source": "https://github.com/jaocero/filachat" 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Jay-Are Ocero", 18 | "email": "199ocero@gmail.com", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "filament/filament": "^3.0", 25 | "laravel/prompts": "^0.1.24", 26 | "livewire/livewire": "^3.5", 27 | "spatie/laravel-package-tools": "^1.15.0" 28 | }, 29 | "require-dev": { 30 | "laravel/pint": "^1.0", 31 | "nunomaduro/collision": "^7.9", 32 | "orchestra/testbench": "^8.0", 33 | "pestphp/pest": "^2.1", 34 | "pestphp/pest-plugin-arch": "^2.0", 35 | "pestphp/pest-plugin-laravel": "^2.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "JaOcero\\FilaChat\\": "src/", 40 | "JaOcero\\FilaChat\\Database\\Factories\\": "database/factories/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "JaOcero\\FilaChat\\Tests\\": "tests/" 46 | } 47 | }, 48 | "scripts": { 49 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 50 | "test": "vendor/bin/pest", 51 | "test-coverage": "vendor/bin/pest --coverage", 52 | "format": "vendor/bin/pint" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true, 58 | "phpstan/extension-installer": true 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "JaOcero\\FilaChat\\FilaChatServiceProvider" 65 | ], 66 | "aliases": { 67 | "FilaChat": "JaOcero\\FilaChat\\Facades\\FilaChat" 68 | } 69 | } 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /config/filachat.php: -------------------------------------------------------------------------------- 1 | true, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Show menu item 20 | |-------------------------------------------------------------------------- 21 | | 22 | | This option controls whether this plugin registers a menu item in the 23 | | sidebar. If disabled, you can manually register a navigation item in a 24 | | different part of the panel. 25 | | 26 | */ 27 | 'show_in_menu' => true, 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | User Model 32 | |-------------------------------------------------------------------------- 33 | | 34 | | This option specifies the user model used in the chat system. You can 35 | | customize this if you have a different user model in your application. 36 | | 37 | */ 38 | 'user_model' => \App\Models\User::class, 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | User Searchable Columns 43 | |-------------------------------------------------------------------------- 44 | | 45 | | This option specifies the searchable columns for the user model. This is used 46 | | to search for users in the chat. 47 | | 48 | */ 49 | 'user_searchable_columns' => [ 50 | 'name', 51 | 'email', 52 | ], 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | User Chat List Display Column 57 | |-------------------------------------------------------------------------- 58 | | 59 | | This option specifies the column to be displayed when selecting the user 60 | | in the chat list. 61 | | 62 | */ 63 | 'user_chat_list_display_column' => 'name', 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Agent Model 68 | |-------------------------------------------------------------------------- 69 | | 70 | | This option specifies the agent model used in the chat system. You can 71 | | customize this if you have a different agent model in your application. 72 | | 73 | */ 74 | 'agent_model' => \App\Models\User::class, 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Agent Searchable Columns 79 | |-------------------------------------------------------------------------- 80 | | 81 | | This option specifies the searchable columns for the agent model. This is used 82 | | to search for agents in the chat. 83 | | 84 | */ 85 | 'agent_searchable_columns' => [ 86 | 'name', 87 | 'email', 88 | ], 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Agent Chat List Display Column 93 | |-------------------------------------------------------------------------- 94 | | 95 | | This option specifies the column to be displayed when selecting the agent 96 | | in the chat list. 97 | | 98 | */ 99 | 'agent_chat_list_display_column' => 'name', 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Sender Name Column 104 | |-------------------------------------------------------------------------- 105 | | 106 | | This option specifies the column name for the sender's name. You can 107 | | customize this if your user model uses a different column name. This also 108 | | use to search for users in the chat. 109 | | 110 | */ 111 | 'sender_name_column' => 'name', 112 | 113 | /* 114 | |-------------------------------------------------------------------------- 115 | | Receiver Name Column 116 | |-------------------------------------------------------------------------- 117 | | 118 | | This option specifies the column name for the receiver's name. You can 119 | | customize this if your user model uses a different column name. This also 120 | | use to search for users in the chat. 121 | | 122 | */ 123 | 'receiver_name_column' => 'name', 124 | 125 | /* 126 | |-------------------------------------------------------------------------- 127 | | Upload Files 128 | |-------------------------------------------------------------------------- 129 | | 130 | | This option specifies the mime types and the type of disk to be used. 131 | | 132 | */ 133 | 'disk' => 'public', 134 | // this configuration is only use if the disk is S3 135 | 's3' => [ 136 | 'directory' => 'attachments', 137 | 'visibility' => 'public', 138 | ], 139 | // these are the mime types that are allowed and you can remove if you want 140 | 'mime_types' => [ 141 | // audio 142 | 'audio/m4a', 143 | 'audio/wav', 144 | 'audio/mpeg', 145 | 'audio/ogg', 146 | 'audio/aac', 147 | 'audio/flac', 148 | 'audio/midi', 149 | 150 | // images 151 | 'image/png', 152 | 'image/jpeg', 153 | 'image/jpg', 154 | 'image/gif', 155 | 156 | // videos 157 | 'video/mp4', 158 | 'video/avi', 159 | 'video/quicktime', 160 | 'video/webm', 161 | 'video/x-matroska', 162 | 'video/x-flv', 163 | 'video/mpeg', 164 | 165 | // documents 166 | 'application/pdf', 167 | 'application/msword', 168 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 169 | 'text/csv', 170 | 'text/plain', 171 | 'application/vnd.ms-excel', 172 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 173 | 'application/vnd.ms-powerpoint', 174 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 175 | ], 176 | /* 177 | | If you want to change the maximum file size above 12mb you need to publish 178 | | livewire config file and change the value for rules. Example below is from livewire config file. 179 | | 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) 180 | */ 181 | 'max_file_size' => 12288, // default livewire 12MB converted to kilobytes 182 | 'min_file_size' => 1, 183 | // this option here is for number of files to be uploaded 184 | 'max_files' => 10, 185 | 'min_files' => 0, 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | Route Slug 190 | |-------------------------------------------------------------------------- 191 | | 192 | | This option specifies the route slug used in the chat system. You can 193 | | customize this if you have a different route slug in your application. 194 | | 195 | */ 196 | 'slug' => 'filachat', 197 | 198 | /* 199 | |-------------------------------------------------------------------------- 200 | | Navigation Label 201 | |-------------------------------------------------------------------------- 202 | | 203 | | This option specifies the navigation label used in the sidebar. 204 | */ 205 | 'navigation_label' => 'FilaChat', 206 | 207 | /* 208 | |-------------------------------------------------------------------------- 209 | | Navigation Badge 210 | |-------------------------------------------------------------------------- 211 | | 212 | | This option specifies the user number of unread message badge in the sidebar. 213 | */ 214 | 'navigation_display_unread_messages_count' => false, 215 | 216 | /* 217 | |-------------------------------------------------------------------------- 218 | | Navigation Icon 219 | |-------------------------------------------------------------------------- 220 | | 221 | | This option specifies the navigation icon used in the chat navigation. You can 222 | | customize this if you have a different icon in your application. 223 | | 224 | */ 225 | 'navigation_icon' => 'heroicon-o-chat-bubble-bottom-center', 226 | 227 | /* 228 | |-------------------------------------------------------------------------- 229 | | Navigation Sort 230 | |-------------------------------------------------------------------------- 231 | | 232 | | This option specifies the navigation sort used in the chat navigation. You can 233 | | customize this if you have a different sort order in your application. 234 | | 235 | */ 236 | 237 | 'navigation_sort' => 1, 238 | 239 | /* 240 | |-------------------------------------------------------------------------- 241 | | Max Content Width 242 | |-------------------------------------------------------------------------- 243 | | 244 | | This option specifies the maximum width of the chat page. You can 245 | | customize this if you have a different width in your application. You can use 246 | | all enum values from \Filament\Support\Enums\MaxWidth. 247 | | 248 | */ 249 | 'max_content_width' => \Filament\Support\Enums\MaxWidth::Full, 250 | 251 | /* 252 | |-------------------------------------------------------------------------- 253 | | Timezone 254 | |-------------------------------------------------------------------------- 255 | | 256 | | This option specifies the timezone used in the chat system. You can 257 | | customize this if you have a different timezone in your application. Please 258 | | see supported timezones here: https://www.php.net/manual/en/timezones.php 259 | | 260 | */ 261 | 'timezone' => env('APP_TIMEZONE', 'UTC'), 262 | ]; 263 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('agentable_id'); 14 | $table->string('agentable_type'); 15 | $table->timestamps(); 16 | 17 | $table->unique(['agentable_id', 'agentable_type']); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('filachat_agents'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/00002_create_filachat_conversations_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('senderable_id'); 14 | $table->string('senderable_type'); 15 | $table->unsignedBigInteger('receiverable_id'); 16 | $table->string('receiverable_type'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('filachat_conversations'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/00003_create_filachat_messages_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->foreignId('filachat_conversation_id')->constrained('filachat_conversations')->onDelete('cascade'); 14 | $table->text('message')->nullable(); 15 | $table->json('attachments')->nullable(); 16 | $table->json('original_attachment_file_names')->nullable(); 17 | $table->json('reactions')->nullable(); 18 | $table->boolean('is_starred')->default(false); 19 | $table->json('metadata')->nullable(); 20 | $table->foreignId('reply_to_message_id')->nullable()->constrained('filachat_messages')->onDelete('cascade'); 21 | $table->unsignedBigInteger('senderable_id'); 22 | $table->string('senderable_type'); 23 | $table->unsignedBigInteger('receiverable_id'); 24 | $table->string('receiverable_type'); 25 | $table->timestamp('last_read_at')->nullable(); 26 | $table->timestamp('edited_at')->nullable(); 27 | $table->timestamp('sender_deleted_at')->nullable(); 28 | $table->timestamp('receiver_deleted_at')->nullable(); 29 | $table->softDeletes(); 30 | $table->timestamps(); 31 | 32 | // Adding indexes to polymorphic relationship columns 33 | $table->index(['senderable_id', 'senderable_type']); 34 | $table->index(['receiverable_id', 'receiverable_type']); 35 | }); 36 | } 37 | 38 | public function down() 39 | { 40 | Schema::dropIfExists('filachat_messages'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /resources/css/filachat.css: -------------------------------------------------------------------------------- 1 | .filachat-filepond .filepond--root { 2 | max-height: 20rem; 3 | } -------------------------------------------------------------------------------- /resources/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/199ocero/filachat/621044411428547012677b0106d4d0fd2d9b6e50/resources/dist/.gitkeep -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/199ocero/filachat/621044411428547012677b0106d4d0fd2d9b6e50/resources/js/index.js -------------------------------------------------------------------------------- /resources/lang/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "Write a message...": "أكتب رسالة...", 3 | "Your Message": "رسالتك", 4 | "Upload Files": "تحميل الملفات", 5 | "You cannot write a message for other user...": "لا يمكنك كتابة رسالة لمستخدم آخر...", 6 | "You cannot write a message for other agent...": "لا يمكنك كتابة رسالة لوكيل آخر...", 7 | "Something went wrong": "حدث خطأ ما", 8 | "File not found": "لم يتم العثور على الملف", 9 | "No Users Found.": "لم يتم العثور على مستخدمين.", 10 | "No Agents Found.": "لم يتم العثور على وكلاء.", 11 | "Loading Users...": "جارٍ تحميل المستخدمين...", 12 | "Loading Agents...": "جارٍ التحميل...", 13 | "Search User by Name or Email": "بحث المستخدم عن طريق الاسم أو البريد الإلكتروني", 14 | "Search Agent by Name or Email": "وكيل البحث بالاسم أو البريد الإلكتروني", 15 | "Select User by Name or Email": "حدد المستخدم حسب الاسم أو البريد الإلكتروني", 16 | "Select Agent by Name or Email": "حدد الوكيل حسب الاسم أو البريد الإلكتروني", 17 | "To": "إلي", 18 | "To Agent": "إلى الوكيل", 19 | "To User": "إلي المستخدم", 20 | "Add": "إضافة", 21 | "Create Conversation": "إنشاء محادثة", 22 | "User": "مستخدم", 23 | "Agent": "وكيل", 24 | "Seen at": "أرسلت في", 25 | "Loading more messages...": "جارٍ تحميل المزيد من الرسائل...", 26 | "No selected conversation": "لم يتم تحديد أي محادثة", 27 | "Active Conversations": "المحادثات النشطة", 28 | "Search any messages...": "البحث في أي رسائل...", 29 | "No conversations yet": "لا توجد محادثات حتى الآن", 30 | "Search Messages in your Conversations": "ابحث في الرسائل في محادثاتك", 31 | "No results found.": "لم يتم العثور على نتائج." 32 | } 33 | -------------------------------------------------------------------------------- /resources/lang/en/filachat.php: -------------------------------------------------------------------------------- 1 | 3 |
6 | @if ($selectedConversation) 7 | 8 |
9 | 12 |
13 |

{{ $selectedConversation->other_person_name }}

14 | @php 15 | if (auth()->id() === $selectedConversation->receiverable_id) { 16 | $isOtherPersonAgent = $selectedConversation->senderable->isAgent(); 17 | } else { 18 | $isOtherPersonAgent = $selectedConversation->receiverable->isAgent(); 19 | } 20 | @endphp 21 |

22 | @if ($isOtherPersonAgent) 23 | {{__('Agent')}} 24 | @else 25 | {{__('User')}} 26 | @endif 27 |

28 |
29 |
30 | 31 | 32 |
61 | 62 |
63 | 66 |
67 |

68 | 69 | 70 | 71 | 72 | 73 |

74 |
75 |
76 | 77 | @foreach ($conversationMessages as $index => $message) 78 |
79 | @php 80 | $nextMessage = $conversationMessages[$index + 1] ?? null; 81 | $nextMessageDate = $nextMessage ? \Carbon\Carbon::parse($nextMessage->created_at)->setTimezone(config('filachat.timezone', 'app.timezone'))->format('Y-m-d') : null; 82 | $currentMessageDate = \Carbon\Carbon::parse($message->created_at)->setTimezone(config('filachat.timezone', 'app.timezone'))->format('Y-m-d'); 83 | 84 | // Show date badge if the current message is the last one of the day 85 | $showDateBadge = $currentMessageDate !== $nextMessageDate; 86 | @endphp 87 | 88 | @if ($showDateBadge) 89 |
90 | 91 | {{ \Carbon\Carbon::parse($message->created_at)->setTimezone(config('filachat.timezone', 'app.timezone'))->format('F j, Y') }} 92 | 93 |
94 | @endif 95 | @if ($message->senderable_id !== auth()->user()->id) 96 | @php 97 | $previousMessageDate = isset($conversationMessages[$index - 1]) ? \Carbon\Carbon::parse($conversationMessages[$index - 1]->created_at)->setTimezone(config('filachat.timezone', 'app.timezone'))->format('Y-m-d') : null; 98 | 99 | $currentMessageDate = \Carbon\Carbon::parse($message->created_at)->setTimezone(config('filachat.timezone', 'app.timezone'))->format('Y-m-d'); 100 | 101 | $previousSenderId = $conversationMessages[$index - 1]->senderable_id ?? null; 102 | 103 | // Show avatar if the current message is the first in a consecutive sequence or a new day 104 | $showAvatar = $message->senderable_id !== auth()->user()->id && ($message->senderable_id !== $previousSenderId || $currentMessageDate !== $previousMessageDate); 105 | @endphp 106 | 107 |
108 | @if ($showAvatar) 109 | 112 | @else 113 |
114 | @endif 115 |
116 | @if ($message->message) 117 |

{{ $message->message }}

118 | @endif 119 | @if ($message->attachments && count($message->attachments) > 0) 120 | @foreach ($message->attachments as $attachment) 121 | @php 122 | $originalFileName = $this->getOriginalFileName($attachment, $message->original_attachment_file_names); 123 | @endphp 124 |
125 |
126 | @php 127 | $icon = 'heroicon-m-x-mark'; 128 | 129 | if($this->validateImage($attachment)) { 130 | $icon = 'heroicon-m-photo'; 131 | } 132 | 133 | if ($this->validateDocument($attachment)) { 134 | $icon = 'heroicon-m-paper-clip'; 135 | } 136 | 137 | if ($this->validateVideo($attachment)) { 138 | $icon = 'heroicon-m-video-camera'; 139 | } 140 | 141 | if ($this->validateAudio($attachment)) { 142 | $icon = 'heroicon-m-speaker-wave'; 143 | } 144 | 145 | @endphp 146 | 147 |
148 |

149 | {{ $originalFileName }} 150 |

151 |
152 | @endforeach 153 | @endif 154 |

155 | @php 156 | $createdAt = \Carbon\Carbon::parse($message->created_at)->setTimezone(config('filachat.timezone', 'app.timezone')); 157 | 158 | if ($createdAt->isToday()) { 159 | $date = $createdAt->format('g:i A'); 160 | } else { 161 | $date = $createdAt->format('M d, Y g:i A'); 162 | } 163 | @endphp 164 | {{ $date }} 165 |

166 |
167 |
168 | @else 169 | 170 |
171 |
172 | @if ($message->message) 173 |

{{ $message->message }}

174 | @endif 175 | @if ($message->attachments && count($message->attachments) > 0) 176 | @foreach ($message->attachments as $attachment) 177 | @php 178 | $originalFileName = $this->getOriginalFileName($attachment, $message->original_attachment_file_names); 179 | @endphp 180 |
181 |
182 | @php 183 | $icon = 'heroicon-m-x-circle'; 184 | 185 | if($this->validateImage($attachment)) { 186 | $icon = 'heroicon-m-photo'; 187 | } 188 | 189 | if ($this->validateDocument($attachment)) { 190 | $icon = 'heroicon-m-paper-clip'; 191 | } 192 | 193 | if ($this->validateVideo($attachment)) { 194 | $icon = 'heroicon-m-video-camera'; 195 | } 196 | 197 | if ($this->validateAudio($attachment)) { 198 | $icon = 'heroicon-m-speaker-wave'; 199 | } 200 | 201 | @endphp 202 | 203 |
204 |

205 | {{ $originalFileName }} 206 |

207 |
208 | @endforeach 209 | @endif 210 |

211 | @php 212 | $createdAt = \Carbon\Carbon::parse($message->created_at)->setTimezone(config('filachat.timezone', 'app.timezone')); 213 | 214 | if ($createdAt->isToday()) { 215 | $date = $createdAt->format('g:i A'); 216 | } else { 217 | $date = $createdAt->format('M d, Y g:i A'); 218 | } 219 | @endphp 220 | {{ $date }} 221 |

222 |
223 | 238 |
239 | @endif 240 |
241 | @endforeach 242 | 243 | @if ($this->paginator->hasMorePages()) 244 |
245 |
{{__('Loading more messages...')}}
246 |
247 | @endif 248 | 249 |
250 | 251 | 252 | 253 | 254 |
255 |
256 |
257 | {{ $this->form }} 258 |
259 |
260 | 261 |
262 |
263 | 264 | 265 |
266 | @else 267 |
268 |
269 | 270 |
271 |

272 | {{__('No selected conversation')}} 273 |

274 |
275 | @endif 276 | 277 |
278 | @script 279 | 293 | @endscript 294 | -------------------------------------------------------------------------------- /resources/views/filachat/components/chat-list.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use JaOcero\FilaChat\Pages\FilaChat; 3 | @endphp 4 | 5 | @props(['selectedConversation']) 6 |
7 |
8 | 9 | 10 | {{ $this->conversations->count() }} 11 | 12 |
13 | 14 | 15 | 18 |
19 | {{ $this->createConversationSmallSizeAction }} 20 |
21 | 22 | 23 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | 35 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /resources/views/filachat/components/search-conversation.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use JaOcero\FilaChat\Pages\FilaChat; 3 | @endphp 4 | 5 | 6 | 7 | {{__('Search Messages in your Conversations')}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{-- Dropdown results --}} 15 | @if(count($messages) > 0) 16 |
17 | 47 |
48 | @elseif(!empty($search)) 49 |
50 |
51 |

52 | {{__('No results found.')}} 53 |

54 |
55 |
56 | @endif 57 |
58 | -------------------------------------------------------------------------------- /resources/views/filachat/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | 6 | 7 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /src/Commands/FilaChatCommand.php: -------------------------------------------------------------------------------- 1 | info('Starting FilaChat installation...'); 17 | $this->publishAssets(); 18 | $this->runMigrations(); 19 | $this->comment('All done'); 20 | 21 | return self::SUCCESS; 22 | } 23 | 24 | protected function publishAssets() 25 | { 26 | $this->info('Publishing assets...'); 27 | 28 | // Publish migrations 29 | Artisan::call('vendor:publish', [ 30 | '--provider' => 'JaOcero\FilaChat\FilaChatServiceProvider', 31 | '--tag' => 'filachat-migrations', 32 | ]); 33 | 34 | // Publish configuration 35 | Artisan::call('vendor:publish', [ 36 | '--provider' => 'JaOcero\FilaChat\FilaChatServiceProvider', 37 | '--tag' => 'filachat-config', 38 | ]); 39 | 40 | $this->info('Assets published.'); 41 | } 42 | 43 | protected function runMigrations() 44 | { 45 | $this->info('Running migrations...'); 46 | Artisan::call('migrate'); 47 | $this->info('Migrations completed.'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/FilaChatCreateAgentCommand.php: -------------------------------------------------------------------------------- 1 | info('Creating new agent...'); 19 | $this->createAgent(); 20 | 21 | return self::SUCCESS; 22 | } 23 | 24 | protected function createAgent() 25 | { 26 | // Prompt for agentable ID 27 | $agentableId = text( 28 | required: true, 29 | label: 'Enter the agent ID', 30 | placeholder: 'E.g. 24', 31 | hint: 'This will be used to identify the agent belongs to ' . config('filachat.agent_model') . ' model.', 32 | validate: function ($value) { 33 | if (! is_numeric($value)) { 34 | return 'The ID must be a number'; 35 | } 36 | 37 | if ($value <= 0) { 38 | return 'The ID must be greater than 0'; 39 | } 40 | } 41 | ); 42 | 43 | // Check if the agentable ID exists in the database 44 | $isExistingRecord = config('filachat.agent_model')::find($agentableId); 45 | 46 | if (! $isExistingRecord) { 47 | $this->error('Agent using this ID with model ' . config('filachat.agent_model') . ' does not exist.'); 48 | 49 | return; 50 | } 51 | 52 | // Check if the combination already exists 53 | $existingAgent = FilaChatAgent::where('agentable_id', $agentableId) 54 | ->where('agentable_type', config('filachat.agent_model')) 55 | ->first(); 56 | 57 | if ($existingAgent) { 58 | $this->error('Agent using this ID with model ' . config('filachat.agent_model') . ' already exists.'); 59 | 60 | return; 61 | } 62 | 63 | // Create the new agent 64 | FilaChatAgent::create([ 65 | 'agentable_id' => $agentableId, 66 | 'agentable_type' => config('filachat.agent_model'), 67 | ]); 68 | 69 | $this->info('Agent created successfully.'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Enums/RoleType.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | public function broadcastOn() 35 | { 36 | return new Channel('filachat'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Events/FilaChatMessageReadEvent.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function broadcastOn() 34 | { 35 | return new Channel('filachat'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Events/FilaChatMessageReceiverIsAwayEvent.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function broadcastOn() 34 | { 35 | return new Channel('filachat'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Events/FilaChatUserTypingEvent.php: -------------------------------------------------------------------------------- 1 | pages([ 19 | FilaChat::class, 20 | ]); 21 | } 22 | 23 | public function boot(Panel $panel): void 24 | { 25 | // 26 | } 27 | 28 | public static function make(): static 29 | { 30 | return app(static::class); 31 | } 32 | 33 | public static function get(): static 34 | { 35 | /** @var static $plugin */ 36 | $plugin = filament(app(static::class)->getId()); 37 | 38 | return $plugin; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/FilaChatServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 37 | ->hasCommands($this->getCommands()); 38 | 39 | $configFileName = $package->shortName(); 40 | 41 | if (file_exists($package->basePath("/../config/{$configFileName}.php"))) { 42 | $package->hasConfigFile($configFileName); 43 | } 44 | 45 | if (file_exists($package->basePath('/../database/migrations'))) { 46 | $package->hasMigrations($this->getMigrations()); 47 | } 48 | 49 | if (file_exists($package->basePath('/../resources/lang'))) { 50 | $package->hasTranslations(); 51 | } 52 | 53 | if (file_exists($package->basePath('/../resources/views'))) { 54 | $package->hasViews(static::$viewNamespace); 55 | } 56 | } 57 | 58 | public function packageRegistered(): void 59 | { 60 | parent::packageRegistered(); 61 | $this->loadJsonTranslationsFrom(__DIR__ . '/../resources/lang/'); 62 | } 63 | 64 | public function packageBooted(): void 65 | { 66 | // Asset Registration 67 | FilamentAsset::register( 68 | $this->getAssets(), 69 | $this->getAssetPackageName() 70 | ); 71 | 72 | FilamentAsset::registerScriptData( 73 | $this->getScriptData(), 74 | $this->getAssetPackageName() 75 | ); 76 | 77 | // Icon Registration 78 | FilamentIcon::register($this->getIcons()); 79 | 80 | // Handle Stubs 81 | if (app()->runningInConsole()) { 82 | foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) { 83 | $this->publishes([ 84 | $file->getRealPath() => base_path("stubs/filachat/{$file->getFilename()}"), 85 | ], 'filachat-stubs'); 86 | } 87 | } 88 | 89 | // Testing 90 | Testable::mixin(new TestsFilaChat); 91 | 92 | // Livewire 93 | Livewire::component('filachat-chat-list', ChatList::class); 94 | Livewire::component('filachat-chat-box', ChatBox::class); 95 | Livewire::component('filachat-search-conversation', SearchConversation::class); 96 | } 97 | 98 | protected function getAssetPackageName(): ?string 99 | { 100 | return 'jaocero/filachat'; 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | protected function getAssets(): array 107 | { 108 | return [ 109 | // AlpineComponent::make('filachat', __DIR__ . '/../resources/dist/components/filachat.js'), 110 | Css::make('filachat-styles', __DIR__ . '/../resources/css/filachat.css')->loadedOnRequest(), 111 | // Js::make('filachat-scripts', __DIR__ . '/../resources/dist/filachat.js'), 112 | ]; 113 | } 114 | 115 | /** 116 | * @return array 117 | */ 118 | protected function getCommands(): array 119 | { 120 | return [ 121 | FilaChatCommand::class, 122 | FilaChatCreateAgentCommand::class, 123 | ]; 124 | } 125 | 126 | /** 127 | * @return array 128 | */ 129 | protected function getIcons(): array 130 | { 131 | return []; 132 | } 133 | 134 | /** 135 | * @return array 136 | */ 137 | protected function getRoutes(): array 138 | { 139 | return []; 140 | } 141 | 142 | /** 143 | * @return array 144 | */ 145 | protected function getScriptData(): array 146 | { 147 | return []; 148 | } 149 | 150 | /** 151 | * @return array 152 | */ 153 | protected function getMigrations(): array 154 | { 155 | return [ 156 | '00001_create_filachat_agents_table', 157 | '00002_create_filachat_conversations_table', 158 | '00003_create_filachat_messages_table', 159 | ]; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Livewire/ChatBox.php: -------------------------------------------------------------------------------- 1 | form->fill(); 52 | 53 | if ($this->selectedConversation) { 54 | $this->conversationMessages = collect(); 55 | $this->loadMoreMessages(); 56 | } 57 | } 58 | 59 | public function form(Form $form): Form 60 | { 61 | $isRoleEnabled = config('filachat.enable_roles'); 62 | $isAgent = auth()->user()->isAgent(); 63 | 64 | if ($this->selectedConversation) { 65 | if (auth()->id() === $this->selectedConversation->receiverable_id) { 66 | $isOtherPersonAgent = $this->selectedConversation->senderable->isAgent(); 67 | } else { 68 | $isOtherPersonAgent = $this->selectedConversation->receiverable->isAgent(); 69 | } 70 | } else { 71 | $isOtherPersonAgent = false; 72 | } 73 | 74 | return $form 75 | ->schema([ 76 | Forms\Components\FileUpload::make('attachments') 77 | ->hiddenLabel() 78 | ->multiple() 79 | ->storeFileNamesIn('original_attachment_file_names') 80 | ->fetchFileInformation() 81 | ->disk(config('filachat.disk')) 82 | ->directory(fn () => config('filachat.disk') == 's3' ? config('filachat.s3.directory') : 'attachments') 83 | ->visibility(fn () => config('filachat.disk') == 's3' ? config('filachat.s3.visibility') : 'public') 84 | ->acceptedFileTypes(config('filachat.mime_types')) 85 | ->maxSize(config('filachat.max_file_size')) 86 | ->minSize(config('filachat.min_file_size')) 87 | ->maxFiles(config('filachat.max_files')) 88 | ->minFiles(config('filachat.min_files')) 89 | ->panelLayout('grid') 90 | ->extraAttributes([ 91 | 'class' => 'filachat-filepond', 92 | ]) 93 | ->visible(fn () => $this->showUpload), 94 | Forms\Components\Split::make([ 95 | Forms\Components\Actions::make([ 96 | Forms\Components\Actions\Action::make('show_hide_upload') 97 | ->hiddenLabel() 98 | ->icon('heroicon-m-plus') 99 | ->color('gray') 100 | ->tooltip(__('Upload Files')) 101 | ->action(fn () => $this->showUpload = ! $this->showUpload), 102 | ]) 103 | ->grow(false), 104 | Forms\Components\Textarea::make('message') 105 | ->hiddenLabel() 106 | ->live(debounce: 500) 107 | ->afterStateUpdated(function (?string $old, ?string $state) { 108 | // check and set the receiver id 109 | if (auth()->id() === $this->selectedConversation->receiverable_id) { 110 | $receiverableId = $this->selectedConversation->senderable_id; 111 | } else { 112 | $receiverableId = $this->selectedConversation->receiverable_id; 113 | } 114 | // check if the user is typing by comparing the old state and the new state and broadcast the typing event 115 | if ($state != $old) { 116 | broadcast(new FilaChatUserTypingEvent($this->selectedConversation->id, true, $receiverableId)); 117 | } 118 | }) 119 | ->placeholder(function () use ($isRoleEnabled, $isAgent, $isOtherPersonAgent) { 120 | if ($isRoleEnabled) { 121 | 122 | // if both in the conversation are normal users 123 | if (! $isAgent && ! $isOtherPersonAgent) { 124 | return __('You cannot write a message for other user...'); 125 | } 126 | 127 | // if both in the conversation are agents 128 | if ($isAgent && $isOtherPersonAgent) { 129 | return __('You cannot write a message for other agent...'); 130 | } 131 | } 132 | 133 | return __('Write a message...'); 134 | }) 135 | ->disabled(function () use ($isRoleEnabled, $isAgent, $isOtherPersonAgent) { 136 | 137 | if ($isRoleEnabled) { 138 | 139 | // if both in the conversation are normal users 140 | if (! $isAgent && ! $isOtherPersonAgent) { 141 | return true; 142 | } 143 | 144 | // if both in the conversation are agents 145 | if ($isAgent && $isOtherPersonAgent) { 146 | return true; 147 | } 148 | 149 | // if one in the conversation is an agent 150 | if ($isAgent && ! $isOtherPersonAgent) { 151 | return false; 152 | } 153 | 154 | // if one in the conversation is a normal user 155 | if (! $isAgent && $isOtherPersonAgent) { 156 | return false; 157 | } 158 | } 159 | 160 | // if roles are not enabled 161 | return false; 162 | }) 163 | ->required(function (Get $get) { 164 | if (is_array($get('attachments')) && count($get('attachments')) > 0) { 165 | return false; 166 | } 167 | 168 | return true; 169 | }) 170 | ->rows(1) 171 | ->autosize() 172 | ->grow(true), 173 | ]) 174 | ->verticallyAlignEnd(), 175 | ]) 176 | ->columns('full') 177 | ->extraAttributes([ 178 | 'class' => 'p-1', 179 | ]) 180 | ->statePath('data'); 181 | } 182 | 183 | public function sendMessage(): void 184 | { 185 | $data = $this->form->getState(); 186 | $rawData = $this->form->getRawState(); 187 | 188 | try { 189 | DB::transaction(function () use ($data) { 190 | if (auth()->id() === $this->selectedConversation->receiverable_id) { 191 | $receiverableId = $this->selectedConversation->senderable_id; 192 | $receiverableType = $this->selectedConversation->senderable_type; 193 | } else { 194 | $receiverableId = $this->selectedConversation->receiverable_id; 195 | $receiverableType = $this->selectedConversation->receiverable_type; 196 | } 197 | 198 | $newMessage = FilaChatMessage::query()->create([ 199 | 'filachat_conversation_id' => $this->selectedConversation->id, 200 | 'message' => $data['message'] ?? null, 201 | 'attachments' => isset($data['attachments']) && count($data['attachments']) > 0 ? $data['attachments'] : null, 202 | 'original_attachment_file_names' => isset($data['original_attachment_file_names']) && count($data['original_attachment_file_names']) > 0 ? $data['original_attachment_file_names'] : null, 203 | 'senderable_id' => auth()->id(), 204 | 'senderable_type' => auth()->user()::class, 205 | 'receiverable_id' => $receiverableId, 206 | 'receiverable_type' => $receiverableType, 207 | ]); 208 | 209 | $this->conversationMessages->prepend($newMessage); 210 | 211 | $this->showUpload = false; 212 | 213 | $this->form->fill(); 214 | 215 | $this->selectedConversation->updated_at = now(); 216 | 217 | $this->selectedConversation->save(); 218 | 219 | broadcast(new FilaChatMessageEvent( 220 | $this->selectedConversation->id, 221 | $newMessage->id, 222 | $receiverableId, 223 | auth()->id(), 224 | )); 225 | }); 226 | } catch (\Exception $exception) { 227 | Notification::make() 228 | ->title(__('Something went wrong')) 229 | ->body($exception->getMessage()) 230 | ->danger() 231 | ->persistent() 232 | ->send(); 233 | } 234 | } 235 | 236 | #[On('echo:filachat,.JaOcero\\FilaChat\\Events\\FilaChatMessageEvent')] 237 | public function broadcastNewMessage($data) 238 | { 239 | if ($data['type'] === FilaChatMessageEvent::class) { 240 | 241 | /** 242 | * This will only be executed if the conversation 243 | * is the selected conversation 244 | */ 245 | if ($data['conversationId'] && $data['conversationId'] === $this->selectedConversation?->id) { 246 | 247 | $message = FilaChatMessage::find($data['messageId']); 248 | 249 | $this->conversationMessages->prepend($message); 250 | 251 | if ($message->receiverable_id === auth()->id() && $message->receiverable_type === auth()->user()::class) { 252 | $message->last_read_at = now(); 253 | $message->save(); 254 | 255 | broadcast(new FilaChatMessageReadEvent($this->selectedConversation->id)); 256 | } 257 | } else { 258 | 259 | if ($data['receiverId'] === auth()->id()) { 260 | broadcast(new FilaChatMessageReceiverIsAwayEvent($data['conversationId'])); 261 | } 262 | } 263 | 264 | /** 265 | * Refresh the conversation list if the sender or receiver 266 | * is the current authenticated user 267 | */ 268 | if ($data['receiverId'] === auth()->id() || $data['senderId'] === auth()->id()) { 269 | $this->dispatch('load-conversations'); 270 | } 271 | } 272 | } 273 | 274 | public function loadMoreMessages() 275 | { 276 | $this->conversationMessages->push(...$this->paginator->getCollection()); 277 | 278 | $this->currentPage = $this->currentPage + 1; 279 | 280 | $this->dispatch('chat-box-preserve-scroll-position'); 281 | } 282 | 283 | #[Computed()] 284 | public function paginator() 285 | { 286 | return $this->selectedConversation->messages()->latest()->paginate(10, ['*'], 'page', $this->currentPage); 287 | } 288 | 289 | public function downloadFile(string $path, string $originalFileName) 290 | { 291 | // Check if the file exists 292 | if (Storage::disk(config('filachat.disk'))->exists($path)) { 293 | return Storage::disk(config('filachat.disk'))->download($path, $originalFileName); 294 | } 295 | 296 | return abort(404, __('File not found')); 297 | } 298 | 299 | public function render() 300 | { 301 | return view('filachat::filachat.components.chat-box'); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Livewire/ChatList.php: -------------------------------------------------------------------------------- 1 | loadConversations(); 31 | } 32 | 33 | #[On('load-conversations')] 34 | public function loadConversations(): void 35 | { 36 | $this->conversations = auth()->user()->allConversations() 37 | ->with(['senderable', 'receiverable', 'messages' => function ($query) { 38 | $query->latest(); 39 | }]) 40 | ->latest('updated_at') 41 | ->get(); 42 | } 43 | 44 | public function createConversationMediumSizeAction(): Action 45 | { 46 | return $this->createConversationAction(name: 'createConversationMediumSize', isLabelHidden: false); 47 | } 48 | 49 | public function createConversationSmallSizeAction(): Action 50 | { 51 | return $this->createConversationAction(name: 'createConversationSmallSizeAction', isLabelHidden: true); 52 | } 53 | 54 | public function createConversationAction(string $name, bool $isLabelHidden = false): Action 55 | { 56 | $isRoleEnabled = config('filachat.enable_roles'); 57 | 58 | $isAgent = auth()->user()->isAgent(); 59 | 60 | return Action::make($name) 61 | ->label(__('Create Conversation')) 62 | ->hiddenLabel($isLabelHidden) 63 | ->icon('heroicon-o-chat-bubble-left-ellipsis') 64 | ->extraAttributes([ 65 | 'class' => 'w-full', 66 | ]) 67 | ->form([ 68 | Forms\Components\Select::make('receiverable_id') 69 | ->label(function () use ($isRoleEnabled, $isAgent) { 70 | if ($isRoleEnabled) { 71 | if ($isAgent) { 72 | return __('To User'); 73 | } 74 | 75 | return __('To Agent'); 76 | } 77 | 78 | return __('To'); 79 | }) 80 | ->placeholder(function () use ($isRoleEnabled, $isAgent) { 81 | if ($isRoleEnabled && ! $isAgent) { 82 | return __('Select Agent by Name or Email'); 83 | } 84 | 85 | return __('Select User by Name or Email'); 86 | }) 87 | ->searchPrompt(function () use ($isRoleEnabled, $isAgent) { 88 | if ($isRoleEnabled && ! $isAgent) { 89 | return __('Search Agent by Name or Email'); 90 | } 91 | 92 | return __('Search User by Name or Email'); 93 | }) 94 | ->loadingMessage(function () use ($isRoleEnabled, $isAgent) { 95 | if ($isRoleEnabled && ! $isAgent) { 96 | return __('Loading Agents...'); 97 | } 98 | 99 | return __('Loading Users...'); 100 | }) 101 | ->noSearchResultsMessage(function () use ($isRoleEnabled, $isAgent) { 102 | if ($isRoleEnabled && ! $isAgent) { 103 | return __('No Agents Found.'); 104 | } 105 | 106 | return __('No Users Found.'); 107 | }) 108 | ->getSearchResultsUsing(fn (string $search): array => ChatListService::make()->getSearchResults($search)->toArray()) 109 | ->getOptionLabelUsing(fn ($value): ?string => ChatListService::make()->getOptionLabel($value)) 110 | ->searchable() 111 | ->required(), 112 | Forms\Components\Textarea::make('message') 113 | ->label(__('Your Message')) 114 | ->placeholder(__('Write a message...')) 115 | ->required() 116 | ->autosize(), 117 | ])->modalSubmitActionLabel(__('Add')) 118 | ->modalWidth(MaxWidth::Large) 119 | ->action(fn (array $data) => ChatListService::make()->createConversation($data)); 120 | } 121 | 122 | public function render(): Application | Factory | View | \Illuminate\View\View 123 | { 124 | return view('filachat::filachat.components.chat-list'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Livewire/SearchConversation.php: -------------------------------------------------------------------------------- 1 | messages = collect(); 23 | } 24 | 25 | #[On('close-modal')] 26 | public function clearSearch(): void 27 | { 28 | $this->search = ''; 29 | $this->currentPage = 1; 30 | $this->messages = collect(); 31 | $this->loadMessages(); 32 | } 33 | 34 | public function loadMessages(): void 35 | { 36 | $this->messages->push(...$this->paginator->getCollection()); 37 | 38 | $this->currentPage = $this->currentPage + 1; 39 | } 40 | 41 | public function loadMoreMessages(): void 42 | { 43 | $this->loadMessages(); 44 | } 45 | 46 | #[Computed()] 47 | public function paginator(): \Illuminate\Contracts\Pagination\LengthAwarePaginator | LengthAwarePaginator 48 | { 49 | $searchTerm = trim($this->search); 50 | 51 | $messages = new LengthAwarePaginator([], 0, 10, $this->currentPage); 52 | 53 | if (! empty($searchTerm)) { 54 | $messages = FilaChatMessage::query() 55 | ->with(['conversation', 'senderable', 'receiverable']) 56 | ->where(function ($query) { 57 | $query->where(function ($query) { 58 | $query->where('senderable_id', auth()->id()) 59 | ->where('senderable_type', auth()->user()::class); 60 | }) 61 | ->orWhere(function ($query) { 62 | $query->where('receiverable_id', auth()->id()) 63 | ->where('receiverable_type', auth()->user()::class); 64 | }); 65 | }) 66 | ->where('message', 'like', '%' . $searchTerm . '%') 67 | ->latest() 68 | ->paginate(10, ['*'], 'page', $this->currentPage); 69 | } 70 | 71 | return $messages; 72 | } 73 | 74 | public function updatedSearch(): void 75 | { 76 | $this->currentPage = 1; 77 | $this->messages = collect(); 78 | $this->loadMessages(); 79 | } 80 | 81 | public function render() 82 | { 83 | return view('filachat::filachat.components.search-conversation', [ 84 | 'messages' => $this->messages, 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Models/FilaChatAgent.php: -------------------------------------------------------------------------------- 1 | morphTo(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Models/FilaChatConversation.php: -------------------------------------------------------------------------------- 1 | morphTo(); 23 | } 24 | 25 | public function receiverable(): MorphTo 26 | { 27 | return $this->morphTo(); 28 | } 29 | 30 | public function messages(): HasMany 31 | { 32 | return $this->hasMany(FilaChatMessage::class, 'filachat_conversation_id', 'id'); 33 | } 34 | 35 | public function latestMessage() 36 | { 37 | return $this->messages()->latest()->first(); 38 | } 39 | 40 | public function getLastMessageTimeAttribute() 41 | { 42 | $latestMessage = $this->latestMessage(); 43 | 44 | return $latestMessage ? $latestMessage->created_at : null; 45 | } 46 | 47 | public function getLatestMessageAttribute() 48 | { 49 | $latestMessage = $this->latestMessage(); 50 | 51 | if ($latestMessage->message) { 52 | return $latestMessage->message; 53 | } 54 | 55 | $attachmentCount = count($latestMessage->attachments); 56 | $fileWord = $attachmentCount > 1 ? 'files' : 'file'; 57 | 58 | return 'Sent ' . $attachmentCount . ' ' . $fileWord . '.'; 59 | } 60 | 61 | public function getUnreadCountAttribute() 62 | { 63 | return $this->messages() 64 | ->whereNull('last_read_at') 65 | ->where('senderable_type', auth()->user()->getMorphClass()) 66 | ->where('senderable_id', '!=', auth()->id()) 67 | ->count(); 68 | } 69 | 70 | public function getSenderNameAttribute() 71 | { 72 | return $this->getName($this->senderable, config('filachat.sender_name_column')); 73 | } 74 | 75 | public function getReceiverNameAttribute() 76 | { 77 | return $this->getName($this->receiverable, config('filachat.receiver_name_column')); 78 | } 79 | 80 | public function getOtherPersonNameAttribute() 81 | { 82 | $authUserId = auth()->user()->id; 83 | 84 | if ($this->senderable_id === $authUserId) { 85 | return $this->getName($this->receiverable, config('filachat.receiver_name_column')); 86 | } 87 | 88 | if ($this->receiverable_id === $authUserId) { 89 | return $this->getName($this->senderable, config('filachat.sender_name_column')); 90 | } 91 | 92 | return 'Unknown Name'; 93 | } 94 | 95 | public function getIsSenderAttribute() 96 | { 97 | $latestMessage = $this->latestMessage(); 98 | 99 | if ($latestMessage->senderable_id === auth()->user()->id) { 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | protected function getName($user, $column) 107 | { 108 | return $user ? $user->{$column} : 'Unknown Name'; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Models/FilaChatMessage.php: -------------------------------------------------------------------------------- 1 | 'array', 37 | 'original_attachment_file_names' => 'array', 38 | 'reactions' => 'array', 39 | 'metadata' => 'array', 40 | 'is_starred' => 'boolean', 41 | 'last_read_at' => 'datetime', 42 | 'edited_at' => 'datetime', 43 | 'sender_deleted_at' => 'datetime', 44 | 'receiver_deleted_at' => 'datetime', 45 | ]; 46 | 47 | public function conversation(): BelongsTo 48 | { 49 | return $this->belongsTo(FilaChatConversation::class, 'filachat_conversation_id', 'id'); 50 | } 51 | 52 | public function senderable(): MorphTo 53 | { 54 | return $this->morphTo(); 55 | } 56 | 57 | public function receiverable(): MorphTo 58 | { 59 | return $this->morphTo(); 60 | } 61 | 62 | public function isRead(): bool 63 | { 64 | return $this->last_read_at !== null; 65 | } 66 | 67 | public function getOtherPersonNameAttribute() 68 | { 69 | $authUserId = auth()->user()->id; 70 | 71 | if ($this->senderable_id === $authUserId) { 72 | return $this->getName($this->receiverable, config('filachat.receiver_name_column')); 73 | } 74 | 75 | if ($this->receiverable_id === $authUserId) { 76 | return $this->getName($this->senderable, config('filachat.sender_name_column')); 77 | } 78 | 79 | return 'Unknown Name'; 80 | } 81 | 82 | protected function getName($user, $column) 83 | { 84 | return $user ? $user->{$column} : 'Unknown Name'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Pages/FilaChat.php: -------------------------------------------------------------------------------- 1 | 0 ? 'danger' : parent::getNavigationBadgeColor(); 38 | } 39 | 40 | public static function getNavigationBadge(): ?string 41 | { 42 | if (config('filachat.navigation_display_unread_messages_count')) { 43 | return FilaChatMessage::query() 44 | ->where('last_read_at', null) 45 | ->where('receiverable_id', auth()->id())->count(); 46 | } 47 | 48 | return parent::getNavigationBadge(); 49 | } 50 | 51 | public static function getNavigationIcon(): string | Htmlable | null 52 | { 53 | return config('filachat.navigation_icon'); 54 | } 55 | 56 | public static function getNavigationSort(): ?int 57 | { 58 | return config('filachat.navigation_sort'); 59 | } 60 | 61 | public function mount(?int $id = null): void 62 | { 63 | if ($id) { 64 | $this->selectedConversation = FilaChatConversation::findOrFail($id); 65 | 66 | $message = FilaChatMessage::query() 67 | ->where('filachat_conversation_id', $this->selectedConversation->id) 68 | ->where('last_read_at', null) 69 | ->where('receiverable_id', auth()->id()) 70 | ->where('receiverable_type', auth()->user()::class); 71 | 72 | if ($message->exists()) { 73 | $message->update(['last_read_at' => now()]); 74 | 75 | broadcast(new FilaChatMessageReadEvent($this->selectedConversation->id)); 76 | } 77 | } 78 | } 79 | 80 | public function getTitle(): string 81 | { 82 | return __(config('filachat.navigation_label')); 83 | } 84 | 85 | public function getMaxContentWidth(): MaxWidth | string | null 86 | { 87 | return config('filachat.max_content_width'); 88 | } 89 | 90 | public function getHeading(): string | Htmlable 91 | { 92 | return ''; // should be empty by default 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Services/ChatListService.php: -------------------------------------------------------------------------------- 1 | isRoleEnabled = config('filachat.enable_roles'); 36 | $this->isAgent = auth()->user()->isAgent(); 37 | $this->userModelClass = config('filachat.user_model'); 38 | $this->agentModelClass = config('filachat.agent_model'); 39 | $this->userChatListDisplayColumn = config('filachat.user_chat_list_display_column'); 40 | $this->agentChatListDisplayColumn = config('filachat.agent_chat_list_display_column'); 41 | 42 | // Check if the user model class exists 43 | if (! class_exists($this->userModelClass)) { 44 | throw new InvalidArgumentException('User model class ' . $this->userModelClass . ' not found'); 45 | } 46 | 47 | // Check if the agent model class exists 48 | if (! class_exists($this->agentModelClass)) { 49 | throw new InvalidArgumentException('Agent model class ' . $this->agentModelClass . ' not found'); 50 | } 51 | 52 | // Validate that all specified columns exist in the user model 53 | foreach (config('filachat.user_searchable_columns') as $column) { 54 | $userTable = (new $this->userModelClass)->getTable(); 55 | if (! Schema::hasColumn($userTable, $column)) { 56 | throw new InvalidArgumentException('Column ' . $column . ' not found in ' . $userTable); 57 | } 58 | } 59 | $this->userSearchableColumns = config('filachat.user_searchable_columns'); 60 | 61 | // Validate that all specified columns exist in the agent model 62 | foreach (config('filachat.agent_searchable_columns') as $column) { 63 | $agentTable = (new $this->agentModelClass)->getTable(); 64 | if (! Schema::hasColumn($agentTable, $column)) { 65 | throw new InvalidArgumentException('Column ' . $column . ' not found in ' . $agentTable); 66 | } 67 | } 68 | $this->agentSearchableColumns = config('filachat.agent_searchable_columns'); 69 | } 70 | 71 | public static function make(): self 72 | { 73 | return new self; 74 | } 75 | 76 | public function getSearchResults(string $search): Collection 77 | { 78 | $searchTerm = '%' . $search . '%'; 79 | 80 | if ($this->isRoleEnabled) { 81 | 82 | $agentIds = $this->agentModelClass::getAllAgentIds(); 83 | 84 | if ($this->isAgent) { 85 | return $this->userModelClass::query() 86 | ->whereNotIn('id', $agentIds) 87 | ->where(function ($query) use ($searchTerm) { 88 | foreach ($this->userSearchableColumns as $column) { 89 | $query->orWhere($column, 'like', $searchTerm); 90 | } 91 | }) 92 | ->select( 93 | DB::raw("CONCAT('user_', id) as user_key"), 94 | DB::raw("$this->userChatListDisplayColumn as user_value") 95 | ) 96 | ->get() 97 | ->pluck('user_value', 'user_key'); 98 | } 99 | 100 | return $this->agentModelClass::query() 101 | ->whereIn('id', $agentIds) 102 | ->where(function ($query) use ($searchTerm) { 103 | foreach ($this->agentSearchableColumns as $column) { 104 | $query->orWhere($column, 'like', $searchTerm); 105 | } 106 | }) 107 | ->select( 108 | DB::raw("CONCAT('agent_', id) as agent_key"), 109 | DB::raw("$this->agentChatListDisplayColumn as agent_value") 110 | ) 111 | ->get() 112 | ->pluck('agent_value', 'agent_key'); 113 | } else { 114 | if ($this->userModelClass === $this->agentModelClass) { 115 | return $this->userModelClass::query() 116 | ->whereNot('id', auth()->id()) 117 | ->where(function ($query) use ($searchTerm) { 118 | foreach ($this->userSearchableColumns as $column) { 119 | $query->orWhere($column, 'like', $searchTerm); 120 | } 121 | }) 122 | ->select( 123 | DB::raw("CONCAT('user_', id) as user_key"), 124 | DB::raw("$this->userChatListDisplayColumn as user_value") 125 | ) 126 | ->get() 127 | ->pluck('user_value', 'user_key'); 128 | } 129 | 130 | $userModel = $this->userModelClass::query() 131 | ->whereNot('id', auth()->id()) 132 | ->where(function ($query) use ($searchTerm) { 133 | foreach ($this->userSearchableColumns as $column) { 134 | $query->orWhere($column, 'like', $searchTerm); 135 | } 136 | }) 137 | ->select( 138 | DB::raw("CONCAT('user_', id) as user_key"), 139 | DB::raw("$this->userChatListDisplayColumn as user_value") 140 | ) 141 | ->get() 142 | ->pluck('user_value', 'user_key'); 143 | 144 | $agentModel = $this->agentModelClass::query() 145 | ->whereNot('id', auth()->id()) 146 | ->where(function ($query) use ($searchTerm) { 147 | foreach ($this->agentSearchableColumns as $column) { 148 | $query->orWhere($column, 'like', $searchTerm); 149 | } 150 | }) 151 | ->select( 152 | DB::raw("CONCAT('agent_', id) as agent_key"), 153 | DB::raw("$this->agentChatListDisplayColumn as agent_value") 154 | ) 155 | ->get() 156 | ->pluck('agent_value', 'agent_key'); 157 | 158 | return $userModel->merge($agentModel); 159 | } 160 | } 161 | 162 | public function getOptionLabel(string $value): ?string 163 | { 164 | if (preg_match('/^user_(\d+)$/', $value, $matches)) { 165 | $id = (int) $matches[1]; 166 | 167 | return $this->userModelClass::find($id)->{$this->userChatListDisplayColumn}; 168 | } 169 | 170 | if (preg_match('/^agent_(\d+)$/', $value, $matches)) { 171 | $id = (int) $matches[1]; 172 | 173 | return $this->agentModelClass::find($id)->{$this->agentChatListDisplayColumn}; 174 | } 175 | 176 | return null; 177 | } 178 | 179 | public function createConversation(array $data) 180 | { 181 | try { 182 | DB::transaction(function () use ($data) { 183 | $receiverableId = $data['receiverable_id']; 184 | 185 | if (preg_match('/^user_(\d+)$/', $receiverableId, $matches)) { 186 | $receiverableType = $this->userModelClass; 187 | $receiverableId = (int) $matches[1]; 188 | } 189 | 190 | if (preg_match('/^agent_(\d+)$/', $receiverableId, $matches)) { 191 | $receiverableType = $this->agentModelClass; 192 | $receiverableId = (int) $matches[1]; 193 | } 194 | 195 | $foundConversation = FilaChatConversation::query() 196 | ->where(function ($query) use ($receiverableId, $receiverableType) { 197 | $query->where(function ($query) { 198 | $query->where('senderable_id', auth()->id()) 199 | ->where('senderable_type', auth()->user()::class); 200 | }) 201 | ->orWhere(function ($query) use ($receiverableId, $receiverableType) { 202 | $query->where('senderable_id', $receiverableId) 203 | ->where('senderable_type', $receiverableType); 204 | }); 205 | }) 206 | ->where(function ($query) use ($receiverableId, $receiverableType) { 207 | $query->where(function ($query) use ($receiverableId, $receiverableType) { 208 | $query->where('receiverable_id', $receiverableId) 209 | ->where('receiverable_type', $receiverableType); 210 | }) 211 | ->orWhere(function ($query) { 212 | $query->where('receiverable_id', auth()->id()) 213 | ->where('receiverable_type', auth()->user()::class); 214 | }); 215 | }) 216 | ->first(); 217 | 218 | if (! $foundConversation) { 219 | $conversation = FilaChatConversation::query()->create([ 220 | 'senderable_id' => auth()->id(), 221 | 'senderable_type' => auth()->user()::class, 222 | 'receiverable_id' => $receiverableId, 223 | 'receiverable_type' => $receiverableType, 224 | ]); 225 | } else { 226 | $conversation = $foundConversation; 227 | } 228 | 229 | $message = FilaChatMessage::query()->create([ 230 | 'filachat_conversation_id' => $conversation->id, 231 | 'senderable_id' => auth()->id(), 232 | 'senderable_type' => auth()->user()::class, 233 | 'receiverable_id' => $receiverableId, 234 | 'receiverable_type' => $receiverableType, 235 | 'message' => $data['message'], 236 | ]); 237 | 238 | $conversation->updated_at = now(); 239 | 240 | $conversation->save(); 241 | 242 | broadcast(new FilaChatMessageEvent( 243 | $conversation->id, 244 | $message->id, 245 | $receiverableId, 246 | auth()->id(), 247 | )); 248 | 249 | return redirect(FilaChat::getUrl(tenant: filament()->getTenant()) . '/' . $conversation->id); 250 | }); 251 | } catch (\Exception $exception) { 252 | Notification::make() 253 | ->title('Something went wrong') 254 | ->body($exception->getMessage()) 255 | ->danger() 256 | ->persistent() 257 | ->send(); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Testing/TestsFilaChat.php: -------------------------------------------------------------------------------- 1 | validAudioExtensions)) { 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/CanValidateDocument.php: -------------------------------------------------------------------------------- 1 | validDocumentExtensions)) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Traits/CanValidateImage.php: -------------------------------------------------------------------------------- 1 | validImageExtensions)) { 19 | return true; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Traits/CanValidateVideo.php: -------------------------------------------------------------------------------- 1 | validVideoExtensions)) { 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Traits/HasFilaChat.php: -------------------------------------------------------------------------------- 1 | morphMany(FilaChatConversation::class, 'senderable'); 14 | } 15 | 16 | public function receivedConversations(): MorphMany 17 | { 18 | return $this->morphMany(FilaChatConversation::class, 'receiverable'); 19 | } 20 | 21 | public function allConversations() 22 | { 23 | return FilaChatConversation::where(function ($query) { 24 | $query->where('senderable_type', $this->getMorphClass()) 25 | ->where('senderable_id', $this->id); 26 | })->orWhere(function ($query) { 27 | $query->where('receiverable_type', $this->getMorphClass()) 28 | ->where('receiverable_id', $this->id); 29 | }); 30 | } 31 | 32 | public function agents(): MorphMany 33 | { 34 | return $this->morphMany(FilaChatAgent::class, 'agentable'); 35 | } 36 | 37 | public static function getAllAgentIds(): array 38 | { 39 | return FilaChatAgent::query()->where('agentable_type', config('filachat.agent_model'))->pluck('agentable_id')->toArray(); 40 | } 41 | 42 | public function isAgent(): bool 43 | { 44 | return $this->agents()->exists(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stubs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/199ocero/filachat/621044411428547012677b0106d4d0fd2d9b6e50/stubs/.gitkeep --------------------------------------------------------------------------------