geekcodev/filament-max-chat
Filament plugin for an operator chat with MAX messenger users. View dialogs with unread badges, reply with HTML formatting and media attachments, receive real-time Echo/Reverb updates with a poll fallback, and configure permissions, channel, disk and navigation via config.
This is a Laravel package for MAX chat messaging; v1.1.5 is a pure performance patch (one additive migration), so upgrading is composer update + php artisan migrate. No new env vars, config keys, translation keys, or public signature changes.
Where to look first:
MaxMessageService::totalUnreadCount() — the single source of truth for the unread badge and the JSON unread counter. Call this instead of writing your own where direction = 'in' and read_at is null query.UnreadCountController — the JSON endpoint behind the panel navigation badge; mirror its query shape if you build your own counters.MaxMessageService::markRead() — marks a chat read via max_chat_id plus the same direction/read_at predicate.0000_02_000003_add_unread_index_to_max_chat_messages.php, which adds max_chat_messages_unread_index on (direction, read_at).First use case: render the panel navigation badge on a page, hit the JSON unread endpoint, then mark one chat read and confirm the badge decrements. Then run EXPLAIN on the unread query and confirm max_chat_messages_unread_index is used — that single check validates you got the whole point of this release.
Always go through the service for unread counts. The new composite index (direction, read_at) covers exactly the predicate direction = 'in' and read_at is null. If you hand-roll the same query in a dashboard widget or an API resource, you still get index coverage — but if you add extra columns to the select or where, you may fall back to a scan. Prefer totalUnreadCount() and cache the result at the component level.
Keep markRead() on the existing path. It is deliberately keyed by max_chat_id and continues to use the existing (max_chat_id, created_at) index. Don't refactor it to filter by read_at first "for consistency" — that would trade a cheap index seek for the new index's wider scan.
Badge on every render. The navigation badge is computed on each page render, which is why the missing index hurt so much. With the index in place this is cheap, but on high-traffic dashboards consider a short-lived per-user cache (a few seconds) or compute it in a view composer so multiple layouts don't each trigger the query.
Deploy order for Octane. Run php artisan migrate as a separate step, then php artisan octane:reload so workers pick up new code. The release notes describe workers being pinned by full scans until /up failed and the container restarted — reloading without migrating leaves you with the old plan and the same failure mode.
Host apps that already added an index. up() guards with Schema::hasIndex, so re-running is safe. Keep the name max_chat_messages_unread_index if you customize or copy the migration; if you fork the migration, preserve the guard so downstream installs that already indexed the table don't fail.
Multi-database installs. The index is a plain composite rather than a partial index specifically for portability across SQLite, PostgreSQL, and MySQL. If you were hoping for WHERE read_at IS NULL compaction, you won't get it from this package — add it yourself only if you're MySQL/Postgres-only and can maintain a divergent migration.
Testing your integration. The package ships MaxChatMessagesSchemaTest covering index presence, down() removal, re-creation, and idempotency. If you override migrations in a host app, add an equivalent assertion in your own suite.
CREATE INDEX on that same table can take time and, on older MySQL, lock writes. Plan the first deploy (maintenance window or online DDL) rather than treating it as a no-op migration.down() drops the index. Rolling back on a mature install silently returns unread counts to full scans. That is the original incident, reintroduced — avoid rollbacks of this migration in production.hasIndex guard is name-based. If your host app already created an equivalent index under a different name, the guard won't see it and you'll end up with two indexes on nearly the same columns. Check SHOW INDEX after migrating.EXPLAIN (or EXPLAIN ANALYZE) on both the badge query and the JSON controller query in staging, with realistic row counts. A correct index plus a bad query shape still scans./up, not as an obviously slow endpoint. If /up flaps under tab-switching load, check unread-count query plans before blaming infrastructure.composer audit behind restricted networks. The package's own gate needed COMPOSER_IPRESOLVE=4 to reach Packagist. If you run the package test suite or audit in a locked-down CI, set that env var rather than disabling audit.migrate; nothing to republish or re-translate.composer install in a clean directory as part of the upgrade smoke test.How can I help you explore Laravel packages today?