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.
Патч добавляет индекс под подсчёт непрочитанных сообщений. Бейдж навигации панели считает непрочитанные входящие на
каждой отрисовке страницы, а подходящего индекса у max_chat_messages не было — счёт шёл полным сканом таблицы. На
достаточно большой переписке скан длился секунды, воркеры Octane оказывались заняты, healthcheck /up не успевал и
контейнер перезапускался. Ломающих изменений нет.
MaxMessageService::totalUnreadCount() и UnreadCountController выбирают where direction = 'in' and read_at is null,
а markRead() — ту же пару условий с ведущим max_chat_id. Существующие индексы таблицы (max_chat_id, created_at),
(user_id, chat_id, created_at) и (created_at) такую выборку не покрывают: подсчёт непрочитанного шёл
последовательным сканом max_chat_messages. Пока строк мало, это незаметно; когда история вырастает, каждая отрисовка
страницы панели занимает воркер на время скана. Поток запросов (например, при переключении вкладок) исчерпывает пул
воркеров, и контейнер становится unhealthy.
Составной индекс (direction, read_at). Отдельная аддитивная миграция
0000_02_000003_add_unread_index_to_max_chat_messages.php создаёт индекс max_chat_messages_unread_index. Он покрывает
и бейдж навигации, и JSON-контроллер непрочитанного; markRead() продолжает работать по существующему индексу с ведущим
max_chat_id. Частичный индекс (where read_at is null) был бы компактнее, но он не переносим на MySQL, поэтому выбран
обычный составной. Миграция идемпотентна: в up() стоит guard Schema::hasIndex, поэтому повторный запуск и установка,
где такой индекс уже добавлен приложением-хостом, не падают.
Обновление установки — обычный php artisan migrate: ничего переносить не нужно, новых переменных окружения и ключей
перевода нет.
Ломающих изменений нет: публичные сигнатуры, ключи и значения по умолчанию конфига не менялись, ключи перевода не затрагивались. Добавлена только миграция.
migrate; на SQLite, PostgreSQL и MySQL.MaxChatMessagesSchemaTest проверяет наличие индекса max_chat_messages_unread_index, его удаление в down() и
повторное создание в up(), а также идемпотентность (up() при уже существующем индексе не падает).composer audit без предупреждений (с
COMPOSER_IPRESOLVE=4, иначе packagist недоступен — gotcha 11). Шаг Gate 7 (чистый резолв в .ci-sim/) в этой сессии
не прогонялся: на диске хоста не осталось места под второй vendor/ — шаг остаётся за CI.laravel-max-client v1.1.0Пакет geekcodev/laravel-max-client в версии 1.1.0 переименовал реестр чатов: модель BotChat → MaxChat, таблица
max_bot_chats → max_chats, enum BotChatStatus → MaxChatStatus. Плагин приведён в соответствие:
весь кластер Chat* переименован в Max*, а конфиг-ключ модели диалога заменён с bot_chat_model на chat_model.
Внешние ключи и JSON-контракты тоже переименованы: колонка bot_chat_id в max_chat_messages теперь max_chat_id, а
поле payload счётчика непрочитанного — latest_max_chat_id вместо latest_bot_chat_id. При обновлении существующей
установки перенесите данные реестра и переименуйте внешний ключ по инструкции в README («Обновление с v1.x»).
Это breaking change публичного API: host-приложения должны обновить импорты (BotChat → MaxChat,
ChatMessageService → MaxMessageService и т.д.).
Раньше в апдейтах MAX аватар не приходит, и аватары в списке диалогов и в открытом чате не показывались. Теперь плагин
фоново подгружает профиль пользователя через MaxUserProfileService клиента (getChatMembers): при открытии чата и для
списка бесед ChatProfileRefresher решает, кому это нужно (не чаще, чем раз в cache_ttl), и ставит в очередь
RefreshChatProfilesJob. Сам UI при этом не блокируется — аватар появляется после фоновой подгрузки. Управляется блоком
profile.*; при profile.enabled=false вызовы к API полностью отключены.
На страницу чата можно вести прямую ссылку с других страниц проекта: /admin/chat?chat_id=<id чата в MAX> открывает
конкретный диалог (плагин сам находит запись реестра по chat_id). Прежний способ по внутреннему ID записи
(?chat=<id max_chats>) сохранён для обратной совместимости. Если chat_id не найден — страница открывается как
обычно, без активного диалога.
Добавлена кнопка «Удалить чат» (в шапке активного диалога, рядом с «Очистить историю»). В отличие от очистки истории,
действие помечает запись реестра статусом removed — диалог исчезает из списка оператора, а история сообщений
сохраняется. Если пользователь снова напишет, storeIncoming вернёт записи статус active — чат снова появится в
списке, и оператор не пропустит обращение.
Новые опции в config/filament-max-chat.php:
| Ключ | Что делает | По умолчанию |
|---|---|---|
chat_model |
Модель диалога (вместо bot_chat_model) |
MaxChat |
profile.enabled |
Включить фоновую подгрузку профилей/аватаров | true |
profile.prefetch |
Когда подгружать: on_open / on_list / both |
both |
profile.cache_ttl |
Не перезапрашивать профиль одного пользователя (сек) | 86400 |
profile.refresh_existing |
Обновлять ли профиль, если аватар уже заполнен | false |
161 тест, 346 assertions. PHPStan level max, PSR-12, declare(strict_types=1).
Счётчик непрочитанных и уведомления теперь работают на всех страницах админки. Раньше обновление бейджа и звук со
toast/browser-уведомлением приходили только двух путях: событие Echo chat-message.created и DOM-событие
chat-unread, которое диспатчит Livewire-компонент лишь на странице чата. На остальных страницах панели не было ни
одного периодического запроса, и счётчик в сайдбаре не обновлялся, пока вы не перезагрузите страницу. Добавлен
глобальный HTTP-poll: новый эндпоинт GET /admin/chat/unread-count (UnreadCountController) отдаёт JSON
{unread_count, latest_bot_chat_id}, а скрипт уведомлений опрашивает его на всех страницах и обновляет бейдж.
Звук/toast больше не срабатывают на собственные исходящие сообщения. Раньше уведомление появлялось на каждое событие Echo, включая ответы оператора. Теперь оповещение срабатывает только когда счётчик непрочитанных вырос — собственные отправленные сообщения его не увеличивают.
Двойные уведомления от Echo и poll исключены. Единая функция обработки сверяет последнее известное значение счётчика и играет сигнал только при его росте, поэтому событие real-time и периодический опрос не дублируют показ.
Новые опции в config/filament-max-chat.php:
| Ключ | Что делает | По умолчанию |
|---|---|---|
route.unread_count_uri |
Путь эндпоинта счётчика непрочитанного | /admin/chat/unread-count |
notifications.poll_enabled |
Включить HTTP-poll фолбэк к Echo | true |
notifications.poll_interval_seconds |
Интервал опроса | 15 |
144 теста, 317 assertions. PHPStan level max, PSR-12, declare(strict_types=1).
Счётчик непрочитанных в сайдбаре снова отображается. restoreBadge() на каждой загрузке страницы читал значение
из localStorage (пустое → 0) и убирал нативный бейдж, который рендерит Filament-sidebar через
getNavigationBadge(). Теперь при инициализации счётчик берётся из серверного бейджа сайдбара, так что он больше не
стирается.
Уведомления перестали молчать после просмотра диалога. Подавление для «активного чата» держало bot_chat_id
последнего открытого диалога в глобальной переменной и продолжало глушить звук/toast/browser-уведомление на любых
других страницах админки. Теперь __fmcActiveChatId сбрасывается при уходе со страницы чата — уведомления снова
приходят, даже если сообщение пришло из последнего просмотренного диалога.
Селектор бейджа соответствует конфигурируемому slug. Поиск пункта меню теперь строится по слагу из конфига
(ui.slug), а не по жёстко зашитому /chat.
chat-unread с актуальным общим числом непрочитанных — счётчик в сайдбаре синхронизируется сразу, не
дожидаясь следующего события или перезагрузки.Бейдж уведомлений не появлялся. CSS-селектор a[href$="/chat"] не совпадал с DOM Filament v5 — ссылка в sidebar
использует класс .fi-sidebar-item-btn. Селектор исправлен, бейдж теперь создаётся и обновляется корректно.
Кнопка «Убрать файл» появлялась во время загрузки. wire:model устанавливает $attachment сразу при выборе
файла, и кнопка была видна до завершения загрузки. Добавлены Alpine + Livewire upload events (upload:started /
upload:finished): кнопка скрыта через x-show="!uploading" пока идёт загрузка, после завершения — появляется.
Очистка file input после отправки. Событие clear-file-input диспатчится из Livewire после reset('attachment')
и слушается в Blade для очистки DOM-значения инпута.
Аватар пользователя в заголовке чата. Рядом с именем отображается круглая аватарка (фото или инициалы) — как в списке диалогов.
Модалка с информацией о пользователе. По клику на имя в заголовке открывается Alpine-модалка: по центру большая аватарка, Имя Фамилия, ID пользователя. Закрывается по клику на backdrop, кнопке «Закрыть» или Escape.
Кнопка удаления сообщения перенесена внутрь пузыря сообщения (прижата к верхнему правому углу). Иконка заменена на
серую корзину (heroicon-o-trash) — визуально единообразна с кнопкой очистки всего чата. При наведении плавно
проявляется и становится красной.
Badge уведомлений теперь рендерится нативно в Filament sidebar через .fi-badge вместо кастомного <span>.
Подавление уведомлений в активном чате. Звук, toast и browser notification не срабатывают, если оператор уже открыл диалог, из которого пришло сообщение.
Отложенный запрос разрешения на уведомления. Notification.requestPermission() вызывается при первом клике
пользователя, а не при загрузке страницы.
Broadcast-канал переименован из operator.chat в chat.channel — нейминг больше не привязан к роли оператора.
Бейдж уведомлений в sidebar. Badge теперь рендерится нативно в Filament sidebar (.fi-badge) вместо кастомного
<span>, что делает его визуально единообразным с остальными бейджами панели.
Подавление уведомлений в активном чате. Если оператор уже открыл диалог, из которого пришло сообщение — звук,
toast и browser notification не срабатывают. Отслеживание активного чата через dispatch('chat-active') в Livewire и
window.__fmcActiveChatId на клиенте.
Отложенный запрос разрешения на уведомления. Браузерный Notification.requestPermission() вызывается при первом
клике пользователя, а не при загрузке страницы — исключает блокировку браузером за незапрошенный弹窗.
Сброс file input после отправки. Инпут файлов очищался серверно, но продолжал отображать имя файла в браузере.
Добавлен wire:ignore + Alpine x-effect для корректной очистки DOM.
Кнопка удаления сообщения не появлялась. Кнопка × была скрыта через hidden, который перебивал
group-hover:flex. Заменена на opacity-0 + pointer-events-none с плавным появлением при наведении.
Заголовок страницы. «Чат оператора» / «Operator chat» переименован в «Чат» / «Chat».
Сброс file input после отправки. После отправки вложения инпутов очищался на сервере, но продолжал отображать
имя файла в браузере. Добавлен wire:ignore и Alpine x-effect для корректной очистки DOM.
Кнопка удаления сообщения не появлялась. Кнопка × на сообщениях была скрыта через hidden (display: none),
который перебивал group-hover:flex. Заменена на opacity-0 + pointer-events-none с плавным появлением при
наведении.
Заголовок страницы. «Чат оператора» / «Operator chat» переименован в «Чат» / «Chat».
Filament v5 плагин для общения оператора с пользователями MAX-мессенджера.
Страница /admin/chat — лента сообщений, список диалогов, аватарки с инициалами. Оператор может отвечать текстом с
HTML-разметкой или отправлять файлы (фото, видео, аудио, документы).
Историю можно почистить целиком или удалить отдельные сообщения.
Лента обновляется в реальном времени через Echo/Reverb. Если чат открыт на другой вкладке — появится сразу.
На остальных страницах админки работает через пуш:
Вложения хранятся на приватном диске, отдаются по защищённому роуту. HTML-разметка от оператора фильтруется по белому списку тегов MAX.
geekcodev/laravel-max-client с опубликованными миграциямиcomposer require geekcodev/filament-max-chat
php artisan vendor:publish --tag=filament-max-chat-config
Подключить плагин в AdminPanelProvider:
use GeekCo\FilamentMaxChat\FilamentMaxChatPlugin;
$panel->plugin(FilamentMaxChatPlugin::make());
Все опции в config/filament-max-chat.php:
| Ключ | Что делает | По умолчанию |
|---|---|---|
permissions.view |
Кто видит чат | chat.view |
permissions.answer |
Кто может отвечать | chat.answer |
broadcast_channel |
Канал Echo | operator.chat |
attachments.disk |
Диск для хранения файлов | local |
notifications.enabled |
Пуши на других страницах | true |
notifications.sound |
Звук при новом сообщении | true |
notifications.browser |
Browser notification | true |
135 тестов, 96% покрытие строк. PHPStan level max, PSR-12, declare(strict_types=1).
How can I help you explore Laravel packages today?