A chat plugin for Filament v4 that supports configurable chat sources, one-to-one and group conversations, text and file attachments (via Spatie Media Library), read/unread tracking, search, and real-time updates via polling or broadcasting (Reverb/Pusher).
Install via Composer:
composer require zedmagdy/filament-chat
Publish and run the migrations:
php artisan vendor:publish --tag="filament-chat-migrations"
php artisan migrate
If you haven't already, publish the Spatie Media Library migration as well:
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="filament-chat-config"
Optionally publish the views for customization:
php artisan vendor:publish --tag="filament-chat-views"
HasChats trait to your User modelnamespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use ZEDMagdy\FilamentChat\Traits\HasChats;
class User extends Authenticatable
{
use HasChats;
// ...
}
This gives your User model these relationships:
chatParticipations() - all chat participationsconversations() - all conversations the user is part ofsentMessages() - all messages sent by the userThe quickest way is with the Artisan command:
# Interactive (prompts for name and model)
php artisan make:chat-source
# Non-interactive
php artisan make:chat-source Staff --model=User
This generates two files:
app/Chat/StaffChatSource.php — the chat source classapp/Filament/Pages/StaffChatPage.php — the Filament pageThen skip to step 4 to register it.
A chat source defines a category of chat (e.g. staff-to-staff, patient support). Create one class per source:
namespace App\Chat;
use App\Filament\Pages\StaffChatPage;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use ZEDMagdy\FilamentChat\ChatSource;
class StaffChatSource extends ChatSource
{
public function getKey(): string
{
return 'staff';
}
public function getLabel(): string
{
return 'Staff Chat';
}
public function getIcon(): string
{
return 'heroicon-o-chat-bubble-left-right';
}
public function getParticipantModel(): string
{
return User::class;
}
public function getPageClass(): string
{
return StaffChatPage::class;
}
// Optional: filter which users can be added to new conversations
public function getAvailableParticipantsQuery(): Builder
{
return User::query()->where('role', 'staff');
}
// Optional: allow users to create new conversations (default: true)
public function allowsNewConversations(): bool
{
return true;
}
// Optional: enable group chats for this source (default: false)
public function allowsGroupChats(): bool
{
return true;
}
// Optional: customize navigation
public function getNavigationGroup(): ?string
{
return 'Communication';
}
public function getNavigationSort(): ?int
{
return 1;
}
// Optional: customize how participant names are displayed
public function getParticipantDisplayName(\Illuminate\Database\Eloquent\Model $participant): string
{
return $participant->name;
}
// Optional: provide avatar URLs
public function getParticipantAvatarUrl(\Illuminate\Database\Eloquent\Model $participant): ?string
{
return $participant->avatar_url;
}
}
Each chat source needs a thin Filament page class:
namespace App\Filament\Pages;
use ZEDMagdy\FilamentChat\Pages\ChatSourcePage;
class StaffChatPage extends ChatSourcePage
{
protected static string $chatSourceKey = 'staff';
}
That's it. The page inherits its navigation label, icon, group, sort, and slug from the chat source.
namespace App\Providers\Filament;
use App\Chat\StaffChatSource;
use Filament\Panel;
use Filament\PanelProvider;
use ZEDMagdy\FilamentChat\FilamentChatPlugin;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
// ... other config
->plugin(
FilamentChatPlugin::make()
->sources([
StaffChatSource::class,
])
);
}
}
You can register multiple sources for different chat contexts:
// app/Chat/PatientChatSource.php
namespace App\Chat;
use App\Filament\Pages\PatientChatPage;
use App\Models\Patient;
use ZEDMagdy\FilamentChat\ChatSource;
class PatientChatSource extends ChatSource
{
public function getKey(): string
{
return 'patient';
}
public function getLabel(): string
{
return 'Patient Messages';
}
public function getIcon(): string
{
return 'heroicon-o-heart';
}
public function getParticipantModel(): string
{
return Patient::class; // any model with HasChats trait
}
public function getPageClass(): string
{
return PatientChatPage::class;
}
}
// app/Filament/Pages/PatientChatPage.php
namespace App\Filament\Pages;
use ZEDMagdy\FilamentChat\Pages\ChatSourcePage;
class PatientChatPage extends ChatSourcePage
{
protected static string $chatSourceKey = 'patient';
}
Register both in your panel:
->plugin(
FilamentChatPlugin::make()
->sources([
StaffChatSource::class,
PatientChatSource::class,
])
)
An aggregate page presents a single read + reply inbox spanning an explicit set of source keys. It does not support starting new conversations (use the per-source pages for that); each row shows a badge indicating which source the conversation belongs to.
Generate one with the make:chat-aggregate command:
# Interactive (prompts for name and source keys)
php artisan make:chat-aggregate
# Non-interactive
php artisan make:chat-aggregate "All Messages" --sources=staff,support --no-interaction
This creates app/Chat/AllMessagesAggregateChatSource.php:
<?php
declare(strict_types=1);
namespace App\Chat;
use App\Filament\Pages\AllMessagesChatPage;
use ZEDMagdy\FilamentChat\AggregateChatSource;
class AllMessagesAggregateChatSource extends AggregateChatSource
{
public function getKey(): string
{
return 'all-messages';
}
public function getLabel(): string
{
return 'All Messages';
}
public function getIcon(): string
{
return 'heroicon-o-inbox-stack';
}
public function getPageClass(): string
{
return AllMessagesChatPage::class;
}
/**
* @return array<int, string>
*/
public function getSourceKeys(): array
{
return ['staff', 'support'];
}
}
and app/Filament/Pages/AllMessagesChatPage.php:
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use ZEDMagdy\FilamentChat\Pages\AggregateChatSourcePage;
class AllMessagesChatPage extends AggregateChatSourcePage
{
protected static string $aggregateKey = 'all-messages';
}
Register it on the plugin alongside your sources:
use App\Chat\AllMessagesAggregateChatSource;
use App\Chat\StaffChatSource;
use App\Chat\SupportChatSource;
use ZEDMagdy\FilamentChat\FilamentChatPlugin;
FilamentChatPlugin::make()
->sources([
StaffChatSource::class,
SupportChatSource::class,
])
->aggregates([
AllMessagesAggregateChatSource::class,
])
The source keys listed in
getSourceKeys()must match thegetKey()of registeredChatSourceclasses. Unknown keys are ignored. New-conversation creation is intentionally unavailable on aggregate pages.
Users can start new conversations by clicking the + button in the chat sidebar. This opens a modal where they select a participant (or multiple for group chats).
Behavior:
allowsGroupChats() returns true. Users provide a group name and select multiple participants.getAvailableParticipantsQuery() — the current user is excluded automatically.Disabling conversation creation:
Override allowsNewConversations() in your chat source to hide the button:
public function allowsNewConversations(): bool
{
return false; // Users can only see existing conversations
}
This is useful for system-managed chats where conversations are created programmatically (e.g. a support ticket system that auto-creates a chat per ticket).
use ZEDMagdy\FilamentChat\Models\Conversation;
use ZEDMagdy\FilamentChat\Models\Participant;
use ZEDMagdy\FilamentChat\Models\Message;
// Create a direct conversation
$conversation = Conversation::create([
'source' => 'staff',
'type' => 'direct',
]);
// Add participants
Participant::create([
'conversation_id' => $conversation->id,
'participantable_id' => $user1->id,
'participantable_type' => $user1->getMorphClass(),
]);
Participant::create([
'conversation_id' => $conversation->id,
'participantable_id' => $user2->id,
'participantable_type' => $user2->getMorphClass(),
]);
// Send a message
$message = Message::create([
'conversation_id' => $conversation->id,
'senderable_id' => $user1->id,
'senderable_type' => $user1->getMorphClass(),
'body' => 'Hello!',
]);
// Create a group conversation
$group = Conversation::create([
'source' => 'staff',
'type' => 'group',
'name' => 'Project Team',
]);
// Send a system message (no sender)
Message::create([
'conversation_id' => $group->id,
'body' => 'User1 created the group',
]);
The Message model uses Spatie Media Library. Attachments are stored in the chat-attachments media collection:
$message = Message::create([
'conversation_id' => $conversation->id,
'senderable_id' => $user->id,
'senderable_type' => $user->getMorphClass(),
'body' => 'Check out this file',
]);
// Add an attachment
$message->addMedia($pathToFile)
->toMediaCollection('chat-attachments');
// Get attachments
$message->getMedia('chat-attachments');
In the UI, the MessageInput Livewire component uses Filament's SpatieMediaLibraryFileUpload for seamless file uploads.
Out of the box, the chat window polls for new messages. Configure the interval in your .env or config:
FILAMENT_CHAT_REALTIME_MODE=polling
// config/filament-chat.php
'realtime' => [
'mode' => 'polling',
'polling_interval' => '5s',
],
For real-time updates via WebSockets:
FILAMENT_CHAT_REALTIME_MODE=broadcasting
The package broadcasts MessageSent and MessagesRead events on private channels (chat.conversation.{id}). Channel authorization is handled automatically - only conversation participants can listen.
Make sure your Laravel broadcasting is configured (Reverb, Pusher, etc.) and that your frontend includes the Echo setup.
// config/filament-chat.php
return [
'table_prefix' => 'chat_',
'realtime' => [
'mode' => env('FILAMENT_CHAT_REALTIME_MODE', 'polling'),
'polling_interval' => '5s',
],
'attachments' => [
'disk' => env('FILAMENT_CHAT_DISK', 'public'),
'collection' => 'chat-attachments',
'max_files' => 4,
'max_file_size' => 10240, // KB
'accepted_types' => [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
],
],
'messages_per_page' => 50,
'conversations_per_page' => 25,
'theme' => [
'sent_bg' => 'primary-500',
'received_bg' => 'gray-100',
'sent_text' => 'white',
'received_text' => 'gray-900',
],
// Override with your own model classes
'models' => [
'conversation' => \ZEDMagdy\FilamentChat\Models\Conversation::class,
'message' => \ZEDMagdy\FilamentChat\Models\Message::class,
'participant' => \ZEDMagdy\FilamentChat\Models\Participant::class,
],
];
You can extend the built-in models and register them in the config:
namespace App\Models;
use ZEDMagdy\FilamentChat\Models\Conversation as BaseConversation;
class Conversation extends BaseConversation
{
// Add your custom logic
}
// config/filament-chat.php
'models' => [
'conversation' => \App\Models\Conversation::class,
],
The package dispatches the following events:
| Event | Broadcasts | Description |
|---|---|---|
MessageSent |
Yes | Fired when a message is sent. Broadcasts on chat.conversation.{id}. |
ConversationCreated |
No | Fired when a new conversation is created. |
MessagesRead |
Yes | Fired when a user reads messages. Broadcasts read receipts. |
Listen to them in your EventServiceProvider or with Event::listen():
use ZEDMagdy\FilamentChat\Events\MessageSent;
Event::listen(MessageSent::class, function (MessageSent $event) {
// Send a notification, update counters, etc.
$event->message;
$event->message->conversation;
$event->message->senderable;
});
composer test
Upgrade note: The internal Livewire components now use a
source-keysarray prop instead ofsource-key. If you overrodefilament-chat::pages.chat-sourceor embeddedchat-list/chat-window/chat-searchdirectly, pass:source-keys="$this->getSourceKeys()"instead of:source-key.
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
How can I help you explore Laravel packages today?