asignua/filament-chat
Filament Chat adds real-time messaging to Filament admin panels. Drop-in chat components let you build conversations, direct messages, and threaded chats with a clean, native Filament UI, ideal for support inboxes, team communication, or customer chat inside your app.
add_last_read_at_to_filament_chat_participants. Run php artisan migrate (or re-publish and migrate if you vendor the migrations) before touching any read-state feature.ChatConfig first — it is the single switchboard for behaviour flags (e.g. ChatConfig::readTimeAvailable()), and most "why is this off?" questions end there.ChatDock on the panel/layout where the chat should live, and make sure your User (or participant model) satisfies whatever contract the package expects.ChatService, add participants, send a message, and confirm the dock updates. Then wire the smallest possible browser listener for filament-chat-incoming (a console.log) so you can see the event shape {conversation, message, open} before building sound or desktop notifications on it.Look at, in this order: ChatService (all mutations and signals), ChatConfig (feature gates), ChatDock (front-end constants such as ChatDock::EVENT_INCOMING), then your extension/hook class.
Extensions and hooks. Hook into the message lifecycle by registering an extension that receives the message. As of v1.5.0, messages returned by list/find queries carry their conversation via setRelation, so $message->conversation inside a hook is free — no N+1, and no crash under Model::preventLazyLoading(). Treat that relation as guaranteed and read from it directly rather than re-querying.
Silent UI refresh with ChatService::changed($conversation). Use this when something about the conversation changed but there is no new message: a participant marked something, a read receipt moved, a status badge flipped. It tells members to re-render the conversation — no message row, no bell, no toast, no filament-chat-incoming event. The distinction is deliberate: changed() is a render hint, not a notification channel. If you find yourself calling changed() to make a bell appear, you want a message instead.
Sound / desktop notification via filament-chat-incoming. Listen on the browser for ChatDock::EVENT_INCOMING (string: filament-chat-incoming). Payload is {conversation, message, open}. Typical Alpine or vanilla setup: keep a Set of seen message IDs, and on each event skip if the ID is already present, skip if open is true (the user is looking at the conversation), then fire the sound or new Notification(...). This works with or without a socket — under polling, the event is emitted when the polled counter grows — so the same listener covers both transports.
Read state. conversation_participants.last_read_at is the source of truth for "when did this person last read this conversation". Build unread badges, read receipts, or "last seen" indicators off it, and guard the whole feature behind ChatConfig::readTimeAvailable() so instances that have not run the third migration degrade cleanly to the old behaviour.
Compose the signals. A well-behaved chat screen usually needs all three: messages for content and bells, changed() for out-of-band state, and filament-chat-incoming for cross-tab/desktop attention. Keep them in separate code paths so you can reason about each.
ChatConfig::readTimeAvailable() before assuming your read-state code ran.changed() is not a notification. No bell, no toast, no browser event. If a user is expected to notice, send a message.notifications.toasts. Turning toasts off does not turn filament-chat-incoming off; turning the event off is not a config toggle. Budget for both firing.message.id rather than on timestamps.open. Firing a desktop notification for the conversation currently on screen is the fastest way to get the feature disabled by your users; gate on open first.setRelation fix protects hook code that reads $message->conversation. If you copy messages out, serialise them, or re-fetch by ID in your own code, hold the conversation relation yourself — otherwise preventLazyLoading() will bite you again in a place the package no longer covers.ChatDock::EVENT_INCOMING before adding sound logic — most "no sound" reports are a listener registered too early or an open flag that was true.ChatConfig. When behaviour differs between environments, diff the published config before diffing the code.How can I help you explore Laravel packages today?