geekcodev / filament-max-chat
Filament plugin: operator chat with MAX messenger users, on top of geekcodev/laravel-max-client.
Requires
- php: ^8.4
- ext-dom: *
- ext-fileinfo: *
- ext-libxml: *
- filament/filament: ^5.0
- geekcodev/laravel-max-client: ^1.2.2
- geekcodev/max-php-client: ^1.1.10
- laravel/framework: ^13.0
- livewire/livewire: ^4.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- larastan/larastan: ^3.0
- orchestra/testbench: ^11.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Filament-плагин: чат оператора с пользователями MAX-мессенджера. Строится поверх
geekcodev/laravel-max-client
(реестр чатов max_chats/max_users) и geekcodev/max-php-client
(API MAX).
Возможности:
- список активных диалогов (непрочитанные — бейджем), лента сообщений, отметки «прочитано»;
- ответы оператора: HTML-форматирование (тулбар), вложения (фото/видео/аудио/файлы) через
uploadMedia; - входящие медиа (фото, стикер, видео, аудио, файл) скачиваются в приватное хранилище и отдаются через авторизованный роут, а контакт, геолокация, превью ссылки и inline-клавиатура показываются из метаданных (см. «Вложения сообщений»);
- real-time обновления через Echo/Reverb (private-канал) + fallback
wire:poll; без broadcast открытая лента синхронизируется опросом (правки, удаления и очистка подтягиваются следующим тиком, а не мгновенно); - синхронизация изменений из MAX: апдейты
message_created/message_edited/message_removed,dialog_cleared,dialog_removedиchat_title_changedобрабатываются плагином (ключupdates.auto_handle, по умолчанию включён), а открытая лента правится сразу по broadcast-событиямchat-message.created/updated/deleted/clearedиchat-removed. На вебхуке доставка ограничена типами подписки MAX (адаптер^1.2.2подписывает на все нужные типы, но уже созданную подписку нужно пересоздать — см. «Обновление до v1.1.6 (типы апдейтов в подписке MAX)» ниже); long polling доставляет все типы; - глобальные уведомления и счётчик непрочитанного на всех страницах панели: Echo + HTTP-poll фолбэк (опционально,
notifications.poll_enabled, интервалnotifications.poll_interval_seconds); - права настраиваются строками (
chat.view/chat.answerпо умолчанию) — совместимо со spatie/laravel-permission и Gate; - всё (канал broadcast, диск вложений, лимиты, навигация, slug страницы) — в конфиге.
Требования
- PHP ^8.4, Laravel ^13.0
- Filament ^5.0 (панель v5), Livewire ^4.1
geekcodev/laravel-max-client^1.2.2 +geekcodev/max-php-client^1.1.10. Constraint вcomposer.json—^1.2.2: форма реестра 1.2 (одна строка на чат, PKchat_id, связи вmax_chat_users) появилась в 1.2.0, полоса миграций0000_00— в 1.2.1 (в 1.2.0 ещё0001_01_01_*, и с ней чистая установка падает), а расширенная подписка на типы апдейтов — в 1.2.2, поэтому минимальная рабочая версия именно1.2.2. Фактически установленную версию всегда сверяйcomposer show geekcodev/laravel-max-client.- Миграции laravel-max-client подгружаются автоматически, публиковать их не нужно. Плагин занимает полосу
0000_02:max_chat_messages.max_chat_idссылается наmax_chatsизlaravel-max-client(0000_00), аmax_chat_messages.operator_id— наusersиз приложения (0000_01), поэтому обе таблицы обязаны применяться раньше. Полоса в имени миграции — это позиция пакета в общем порядке миграций приложения и всех пакетов, который Laravel строит по именам файлов. Требование «опубликованные миграции» было верно только до v1.1.1 и больше не действует. Полосы несамостоятельны:0000_02плагина работает только с адаптером, чьи миграции лежат в0000_00, поэтому обновляйтеlaravel-max-clientдо версии с полосой0000_00(v1.2.0 ещё использует0001_01_01_*, и с ней чистая установка снова падает на позднейmax_chats). - Для real-time: совместимый broadcaster (например, Laravel Reverb) и
window.Echoв панели
Установка
composer require geekcodev/filament-max-chat
php artisan migrate # миграция max_chat_messages загружается автоматически из пакета
Подключение к панели:
use GeekCo\FilamentMaxChat\FilamentMaxChatPlugin; public function panel(Panel $panel): Panel { return $panel // ... ->plugin(FilamentMaxChatPlugin::make()); }
Подключение стилей (Tailwind) — добавьте в resources/css/filament.css:
@import 'vendor/geekcodev/filament-max-chat/resources/css/filament-max-chat.css';
Права (пример со spatie/laravel-permission):
Role::findByName('operator')->givePermissionTo(['chat.view', 'chat.answer']);
Приём входящих сообщений
По умолчанию плагин ведёт историю переписки сам: в boot() он регистрирует слушатель события MaxUpdateReceived
пакета laravel-max-client и переводит апдейты в таблицу max_chat_messages — создание, правку и удаление сообщения,
очистку и удаление диалога, смену названия чата. Слушатель не ставится, пока в конфиге выключен updates.auto_handle.
Бизнес-логика апдейтов (команды, приветствие, заявки) остаётся за приложением; в плагине крутится только то, что
относится к ленте. Нажатие inline-кнопки (message_callback) плагин не обрабатывает: это действие пользователя, а не
сообщение.
Если историю ведёт собственный обработчик, отключите автообработку (FILAMENT_MAX_CHAT_UPDATES_AUTO_HANDLE=false)
и вызывайте методы сами:
match ($event->update->updateType) { UpdateType::MessageCreated => $this->chatMessages->storeIncoming($event->update), UpdateType::MessageEdited => $this->chatMessages->applyIncomingEdit($event->update), UpdateType::MessageRemoved => $this->chatMessages->applyIncomingRemoval($event->update), UpdateType::DialogCleared => $this->chatMessages->applyIncomingClear($event->update), UpdateType::DialogRemoved => $this->chatMessages->applyIncomingChatRemoval($event->update), UpdateType::ChatTitleChanged => $this->chatMessages->applyIncomingTitle($event->update), default => null, };
Методы ведут себя так:
storeIncoming()создаёт входящее сообщение и чат, а при повторной доставке того жеmessage_idвозвращает уже сохранённую строку, не дублируя её и не переотправляя broadcast-событие — вы и автообработка плагина в паре не задвоят историю;applyIncomingEdit()переписывает текст уже сохранённого сообщения; идентификатор берётся изmessage.body.mid, потому что вmessage_editedверхнегоmessage_idнет;applyIncomingRemoval()удаляет строку истории и файл вложения с приватного диска, приводя её в соответствие с MAX;applyIncomingClear()чистит локальную историю диалога и файлы вложений без вызова MAX API: на стороне MAX переписка уже очищена, поэтому рассылатьdeleteMessageповторно нельзя (для очистки диалога это делает неclearHistory(), а этот метод);applyIncomingChatRemoval()помечает чат статусом Removed, как кнопка удаления в интерфейсе: история и вложения сохраняются, чат уходит из списка оператора;applyIncomingTitle()пишетtitleтолько уже известному чату, пустое значение игнорируется;- правка и удаление ищут сообщение по паре «
chat_id+message_id», а очистка и удаление диалога работают на уровне чата, поэтому событие из другого чата не тронет вашу историю, а правка без нового текста и удаление с пустымattachmentничего не портят; - после правки, удаления и очистки уходят broadcast-события
chat-message.updated,chat-message.deletedиchat-message.clearedв тот же private-канал, что иchat-message.created, а после удаления диалога —chat-removed; открытая лента перечитывается сразу по broadcast, а без него — следующимwire:poll.
Вложения сообщений
Оператор видит все типы вложений MAX, а не только медиа. Скачиваются на приватный диск и отдаются через авторизованный
роут GET route.uri изображение, стикер (обычно webp; код стикера сохраняется в метаданных), видео (mp4 из
payload.url.mp4 — при отсутствии ссылки остаётся только подпись «видео»), аудио и файл. Остальные типы файла не имеют
и показываются из метаданных: контакт — карточкой «имя + телефон» (имя из max_info, иначе из vCard FN; телефон
из
vcf_phone, иначе разбирается из vcf_info, ссылка tel:), геолокация — ссылкой на OpenStreetMap с координатами,
превью ссылки (share) — заголовком, описанием и самой ссылкой (картинку превью не скачиваем: это чужой домен, а не
CDN MAX), inline-клавиатура — подписями кнопок (нажимать их оператор не может). Метаданные складываются в
JSON-колонку max_chat_messages.attachment — по одному массиву на вложение.
Вложения без данных (медиа без payload, нераспознанный контакт, геолокация без координат, пустая клавиатура) дают
подпись-заглушку, а не пустой пузырь. Длины строковых полей (заголовок, описание, имя, телефон, текст кнопки)
ограничиваются при сохранении, а url вложения сохраняется и показывается ссылкой только для схем http/https — это
защищает и от скачивания по чужому протоколу (file:, data:), и от href="javascript:" в ленте.
Конфигурация
php artisan vendor:publish --tag=filament-max-chat-config # config/filament-max-chat.php php artisan vendor:publish --tag=filament-max-chat-views # views для правки UI
Ключевые параметры:
| Ключ | По умолчанию | Описание |
|---|---|---|
permissions.view / permissions.answer |
chat.view / chat.answer |
Права на просмотр чата / отправку ответов |
broadcast_channel |
chat.channel |
Private-канал событий chat-message.created/updated/deleted/cleared и chat-removed |
updates.auto_handle |
true |
Автообработка апдейтов MAX (создание/правка/удаление сообщения, очистка и удаление диалога, смена названия) |
chat_model |
пакетный Models\MaxChat |
Модель диалога (расширение MaxChat клиента); переопределяйте подклассом |
user_model |
Illuminate\Foundation\Auth\User |
Модель оператора для связи operator_id |
attachments.* |
local, chat-attachments, 25 MiB |
Диск, каталог, лимиты, mime-список |
route.* |
вкл., /admin/chat/messages/{message}/attachment, /admin/chat/unread-count |
Роуты отдачи вложений и счётчика непрочитанного, URL редиректа гостя |
ui.* |
см. конфиг | Иконка/подпись/sort/slug навигации, интервал poll, лимит сообщений |
notifications.* |
вкл., звук, browser, poll 15s | Уведомления о новых сообщениях + HTTP-poll фолбэк к Echo |
Архитектура
Services\MaxMessageService— история сообщений (storeIncoming,storeOutgoing,applyIncomingEdit,applyIncomingRemoval,applyIncomingClear,applyIncomingChatRemoval,applyIncomingTitle,conversations,markRead);Console\MaxChatUpgradeCommand(max-chat:upgrade) — перевод истории переписки на форму реестра чатов v1.2.0;Services\MaxChatSender— отправка ответов (sendFormatted,sendAttachment) через ApiClient;Services\MaxAttachmentStore— приватное хранение вложений (метаданные в JSON-колонке);Support\TextSanitizer— санитизация HTML под whitelist тегов MAX (TextFormat: html);Listeners\HandleIncomingMaxUpdate— автообработка апдейтов MAX (регистрируется провайдером приupdates.auto_handle);Events\MaxMessageCreated,Events\MaxMessageUpdated,Events\MaxMessageDeleted,Events\MaxMessagesCleared,Events\MaxChatRemoved— ShouldBroadcast в private-канал;Livewire\OperatorChat(алиасfilament-max-chat) +Pages\OperatorChat.
Модель Models\MaxChat расширяет GeekCo\LaravelMaxClient\Models\MaxChat связями
messages/lastMessage и работает с той же таблицей max_chats — реестр чатов клиента (chats.enabled /
MAX_CHATS_ENABLED) продолжает работать без изменений.
С laravel-max-client v1.2.0 в max_chats одна строка на чат, первичный ключ — сам chat_id, а пользователи чата лежат
в max_chat_users. Поэтому в max_chat_messages.max_chat_id хранится chat_id чата в MAX, а собеседник оператора
берётся через interlocutor() (первый пользователь чата, который не бот). Связи maxUser() и колонки
max_chats.user_id больше нет. Название для интерфейса даёт displayName() — у группы и канала это title из
getChat(), у диалога имя собеседника.
Аватар в списке, шапке и карточке даёт avatarUrl() (fullAvatarUrl() — для увеличенной в карточке): у группы и канала
это иконка max_chats.icon_url из getChat(), у диалога — аватар собеседника. В группе и канале каждое входящее
сообщение подписано своим отправителем: имя и аватар берутся из связи MaxMessage::sender() (по user_id самого
сообщения), а не из единственного собеседника чата.
На узком экране список диалогов и переписка показываются по очереди: без выбранного чата виден только список, после выбора — лента и форма ответа, а в шапке появляется кнопка возврата к списку. На широком экране обе панели стоят рядом. Контейнер держит минимальную высоту, поэтому на телефоне в горизонтальной ориентации лента сообщений не выдавливается формой ответа почти в ноль.
MaxMessageService сам заводит связь «чат — пользователь» в max_chat_users при сохранении сообщения: ответы оператора
и storeIncomingForUser() приходят без апдейта MAX, где эту связь обычно регистрирует слушатель пакета.
Если бота только что добавили в группу или канал, участники которых появились в MAX раньше плагина, связей в
max_chat_users ещё нет и собеседника (interlocutor()) не существует. На отправку ответов это не влияет: POST /messages принимает chat_id без user_id, поэтому адресатом ответа оператора становится сам чат. Локальная история
такие сообщения тоже сохраняет — max_chat_messages.user_id nullable, а в ленте вместо имени показывается «Канал»,
«Группа» или «Без отправителя» по типу чата. Счётчики непрочитанного и отметка прочтения от user_id не зависят.
Часть таких чатов находится поиском только по тексту сообщений: искать по имени нечего, пока состав не синхронизирован.
Состав чата синхронизирует адаптер — в текущей версии он берёт участников только из апдейтов, которые несут
user, поэтому у канала и группы без собственных сообщений список участников остаётся неполным. План доработки
адаптера: .agents/plans/2026-10-02-laravel-max-client-chat-member-sync.md (файл не попадает в архив пакета).
Кнопка «Очистить историю» удаляет только сообщения диалога (через MaxMessageService::clearHistory). Отдельное действие
«Удалить чат» (MaxMessageService::removeChat) помечает запись реестра статусом removed — диалог исчезает из списка
чатов оператора (список фильтрует только Active), при этом история сообщений сохраняется.
Если пользователь снова напишет в удалённый чат, MaxMessageService::storeIncoming (через upsertChat) вернёт статус
записи обратно в active — диалог снова появится в списке оператора с сохранённой историей. Это осознанное поведение:
оператор не пропускает реальные обращения, а «удаление» действует как уборка диалога из списка до следующего сообщения.
Открытие конкретного чата по ссылке
На страницу чата можно вести прямую ссылку на конкретный диалог с внешних страниц — по идентификатору чата в MAX:
/admin/chat?chat_id=<id чата в MAX>— плагин найдёт запись реестра поchat_idи откроет диалог;/admin/chat?chat=<id чата в MAX>— алиас того же параметра для обратной совместимости.
С версии 1.1.0 внутреннего ID записи реестра нет: первичный ключ max_chats — сам chat_id, поэтому оба параметра
значат одно и то же.
Пример сформировать такую ссылку из вашего кода:
route(OperatorChat::getRouteName(), ['chat_id' => $chat->chat_id])
Если chat_id не найден в реестре max_chats, страница откроется как обычно (без активного диалога) — это безопасно
при прямом переходе по ссылке.
Обновление до v1.1.6 (типы апдейтов в подписке MAX)
С v1.1.6 плагин сам ведёт историю по апдейтам MAX. Через вебхук MAX доставляет только типы, перечисленные в
подписке, которую создаёт команда max:subscribe из laravel-max-client. В laravel-max-client v1.2.2 список по
умолчанию расширен с шести до 13 типов (включая message_edited, message_removed, dialog_cleared,
dialog_removed, chat_title_changed), поэтому на свежей установке правки, удаления, очистка и переименование доходят.
Long polling (php artisan max:listen) типы не фильтрует вообще.
Порядок обновления существующей установки:
composer update geekcodev/laravel-max-client # до ^1.2.2 php artisan max:subscribe https://<домен>/max/webhook # пересоздать подписку — одного вызова достаточно php artisan max:subscriptions # сверить: в списке должны быть все нужные типы
Ключевой шаг — повторный max:subscribe: обновление пакета уже созданную подписку не меняет, а POST на существующий URL
её обновляет. Без переподписки симптом «обновился, а правки не приходят» выглядит как баг релиза. Список типов можно
переопределить ключом конфига laravel-max-client webhook.update_types (env MAX_WEBHOOK_UPDATE_TYPES) — это фильтр
доставки подписки, long polling он не затрагивает.
В v1.1.6 добавлены ключи переводов для новых типов вложений. Пакет подключает свои lang-файлы сам
(loadTranslationsFrom), поэтому по умолчанию ничего делать не нужно. Если вы публиковали языковые файлы себе для
правки строк, новая версия всё равно подхватит добавленные ключи из пакета: Laravel сливает пакетный файл с
опубликованным, и ключи, которых нет в вашей копии, берутся из пакета. Перепубликация
(php artisan vendor:publish --tag=filament-max-chat-lang) нужна, только если вы хотите заменить значения новых строк в
своей копии.
Обновление с v1.0.x на v1.1.0 (новая схема реестра чатов)
laravel-max-client v1.2.0 перестроил реестр чатов: вместо строки на пару «пользователь + чат» в max_chats стало одна
строка на чат с первичным ключом chat_id, а связи с пользователями вынесены в max_chat_users. Пересборку таблицы
делает команда адаптера php artisan max:upgrade.
composer require geekcodev/filament-max-chat:^1.1.0 php artisan migrate # max_chat_users миграциями адаптера + nullable user_id миграцией плагина php artisan max-chat:upgrade # переносит max_chat_id на chat_id, пересобирает реестр и возвращает FK
php artisan migrate создаёт max_chat_messages и аддитивную миграцию, снимающую NOT NULL с
max_chat_messages.user_id. Вторая нужна для каналов и групп, у которых на момент установки ещё нет ни одной связи в
max_chat_users (см. «Схема реестра v1.2.0»): собеседника в реестре нет, а колонка пустая. Она обратная — откат
подставляет chat_id вместо null, чтобы не падать на данных. На перевод истории она не влияет:
max-chat:upgrade работает с max_chat_id.
Команда max-chat:upgrade сама зовёт max:upgrade адаптера между своими двумя фазами, поэтому запускать адаптер
отдельно не нужно и нельзя: пока на max_chats висит FK из max_chat_messages, max:upgrade упадёт на пересборке
таблицы.
Порядок обязателен и продиктован ограничениями СУБД:
- пока на
max_chatsвисит FK изmax_chat_messagesсо суррогатнойid, пересборка таблицы падает с ошибкойcannot drop table max_chats because other objects depend on it— поэтому FK снимается самой командой до вызоваmax:upgrade(вместе с переносом значений и расширением колонок); - перенести значения надо до пересборки: соответствие «старый
id→chat_id» после неё исчезает вместе с колонкойid; - вернуть FK на
max_chats.chat_idможно только после пересборки, когда эта колонка станет первичным ключом, — поэтому это вторая фаза, то есть шаг послеmax:upgrade.
Значения переносятся через временную таблицу соответствия одним запросом, поэтому перевод работает и на MySQL, и на
PostgreSQL, и на SQLite. На чистой установке обе фазы ничего не делают. Отдельной миграции перевода у плагина нет
намеренно: php artisan migrate проходит целиком до max:upgrade, поэтому перенос значений в миграции означал бы, что
без max-chat:upgrade таблица осталась бы наполовину переведённой. Сейчас без команды схема остаётся целиком старой, а
перевод выполняется целиком и возобновляемо. Сам перевод необратим: после max:upgrade колонки
max_chats.id больше нет, поэтому откатывать плагин поверх нового адаптера нельзя. Если max:upgrade упал, команду
достаточно повторить — обе фазы идемпотентны.
Если своя модель чата или свои запросы к max_chats, проверьте их после обновления: колонки max_chats.id и
max_chats.user_id больше нет, а max_chat_messages.max_chat_id содержит chat_id чата в MAX.
Обновление с v1.x (миграция max_bot_chats → max_chats)
laravel-max-client v1.1.0 переименовал реестр чатов: модель BotChat → MaxChat, таблица max_bot_chats →
max_chats, enum BotChatStatus → MaxChatStatus. Плагин использует новую модель Models\MaxChat (config-ключ
chat_model) и FK max_chat_id на таблицу max_chats. В max_chat_messages внешний ключ переименован с
bot_chat_id на max_chat_id, а в JSON-контрактах latest_bot_chat_id заменён на latest_max_chat_id.
При обновлении существующей установки перенесите данные реестра в новую таблицу вручную, а затем переименуйте колонку внешнего ключа в таблице сообщений:
CREATE TABLE max_chats LIKE max_bot_chats; INSERT INTO max_chats SELECT * FROM max_bot_chats; DROP TABLE max_bot_chats; ALTER TABLE max_chat_messages RENAME COLUMN bot_chat_id TO max_chat_id;
Альтернативно — пересоздайте реестр с чистого листа: включите MAX_CHATS_ENABLED и переподпишитесь на
bot_added/bot_started, чтобы пакет заново наполнил max_chats.
Тестирование и разработка
PHP/Composer на хосте не требуются — всё через Docker (образ ghcr.io/geekcodev/php:8.4-bookworm, Orchestra Testbench):
docker compose up -d --build # контейнер app (PHP 8.4) docker compose run --rm app composer install docker compose exec -T app composer test # PHPUnit (SQLite in-memory) docker compose exec -T app composer analyse # PHPStan level max (Larastan) docker compose exec -T app composer lint # PHP-CS-Fixer (--dry-run) docker compose exec -T app composer format # PHP-CS-Fixer (исправить) docker compose exec -T app composer coverage # PHPUnit + гейт покрытия ≥95% строк docker compose exec -T app composer security-audit # composer audit
Флаг -T у docker compose exec обязателен для неинтерактивных запусков: без него вывод PHPUnit/PHPStan ломается при
перенаправлении. Xdebug в контейнере включён, для покрытия режим задаётся явно:
XDEBUG_MODE=coverage docker compose run --rm -e XDEBUG_MODE=coverage app composer coverage
Для профилирования/отладки подключайтесь к host.docker.internal:9003.
История изменений
Значимые пункты дублируются здесь из .agents/release/RELEASE_NOTES_vX.Y.Z.md; полные release notes лежат в
.agents/release/.
| Версия | Дата | Итог |
|---|---|---|
| v1.1.6 | 2026-10-17 | Автообработка апдейтов MAX плагином (updates.auto_handle): создание/правка/удаление сообщения, очистка и удаление диалога, смена названия чата; broadcast chat-message.updated/deleted/cleared и chat-removed (+ Livewire chat-reload, закрытие ленты), фолбэк message.body.mid в message_edited, идемпотентность storeIncoming(). Вложения: распознаются все девять типов — стикер, контакт (карточка «имя + телефон»), геолокация, превью ссылки и inline-клавиатура показываются из метаданных, а не пустым пузырём; ссылки не-http(s) не скачиваются и не кликабельны. message_callback плагин не обрабатывает. Ранее выпущенная правка поиска (PostgreSQL 42P10, регистронезависимость) входит в релиз |
| v1.1.5 | 2026-10-12 | Индекс (direction, read_at) под подсчёт непрочитанных: бейдж навигации больше не сканирует max_chat_messages целиком (миграция 0000_02_000003, идемпотентная) |
| v1.1.4 | 2026-10-12 | Минимальная высота контейнера чата: на телефоне в горизонтальной ориентации лента сообщений не выдавливается формой ответа; лента забирает остаток высоты, форма не сжимается |
| v1.1.3 | 2026-10-12 | Одноколоночная вёрстка чата на телефоне: список диалогов и переписка показываются по очереди, после выбора чата есть кнопка возврата к списку (десктопные две колонки без изменений) |
| v1.1.2 | 2026-10-12 | Аватар группы и канала берётся из иконки чата max_chats.icon_url, каждое входящее сообщение в группе подписано своим отправителем (MaxMessage::sender()), профили обновляются по всем не-бот участникам чата |
| v1.1.1 | 2026-10-03 | Миграции плагина перенесены в полосу 0000_02 общего порядка: чистая установка на PostgreSQL и MySQL снова проходит, внешний ключ на max_chats больше не создаётся раньше таблицы. Переименование миграций (Schema::hasTable() в up(), повторный migrate — no-op) |
| v1.1.0 | 2026-09-04 | Совместимость с laravel-max-client 1.2: адресация истории по chat_id, MaxChat::interlocutor()/displayName(), поиск по всем пользователям чата, хранение сообщений чатов без собеседника (user_id nullable). Ломающий переход (нужны php artisan migrate и php artisan max-chat:upgrade) |
| v1.0.9 | 2026-09-03 | Несколько вложений с предпросмотром и вставкой ссылки, storeIncomingForUser для действий пользователя без апдейта MAX |
| v1.0.0–v1.0.8 | 2026-08-25…2026-09-01 | Первая реализация плагина: страница чата, лента, ответы и вложения, real-time доставка и глобальный счётчик непрочитанного, аватары и карточка чата |