диалоги
Рабочая область консоли — это сетка из трёх зон: список, тред, панель посетителя. На телефоне это становится двумя экранами и шторкой — и шторка это именно шторка, потому что переход на отдельный маршрут размонтировал бы поле ввода и потерял черновик.
«Мои» и «Ожидают» остаются одним экраном за сегментированным переключателем — если развести их по отдельным пунктам навигации, взятие диалога из ожидания превратилось бы в акт навигации.
Порядок нижней навигации исправлен: Диалоги, Записи, Команда, Аналитика, Ещё — по тому, как часто к разделу обращаются за смену, а не по порядку боковой панели консоли.
Идентификаторы теперь настоящей формы. Пара эмодзи — настоящий механизм: существо + еда, назначается один раз и навсегда, набор и роли не меняются (Ago.Chat.Domain.VisitorEmojiDictionary). Никаких #4821: такого формата в продукте не существует.
Значок — бейдж, а не пара рядом. Крупная иконка существа по центру, еда — маленьким бейджем поверх правого нижнего края, без кружка под собой — размеры подобраны вживую.
Строка читается словами, а не парой символов. Когда имя посетителя неизвестно, вместо голых эмодзи и хеша — локализованное название пары (Лиса · Апельсин), а сам код остаётся мелким и бледным следом — на случай редкого совпадения в списке, не для доказательства уникальности. Название хранится не в Domain (домен остаётся без перевода), а в собственной i18n-таблице консоли, ключ — сам эмодзи-глиф, скопированный побайтово из VisitorEmojiDictionary.cs, чтобы не разойтись из-за невидимого variation selector'а.
«Все» — третий сегмент, а не пункт навигации и не экран за «⋮». В консоли это отдельная страница администратора; на телефоне отдельный маршрут означал бы, что оператор уходит со своего списка, чтобы посмотреть чужой. Тот же экран, тот же переключатель, что уже разводит «Мои» и «Ожидают».
Вкладки просто нет у того, кому она не положена. GET /api/v1/conversations/all закрыт правом site:configure — так решил GetAllConversationsForSiteHandler, и решил намеренно: conversation:read есть у каждого оператора и открывает только его собственную очередь. У оператора без site:configure переключатель остаётся о двух сегментах — не о трёх, из которых один серый.
Это список строк, а не таблица. Первые две строки — ровно те же, что в «Моих»: имя и время с начала диалога сверху, последние слова и время последнего сообщения под ними. Третья строка — единственное, что здесь добавлено: слева состояние (Не начат — посетитель написал, никто не взял; Назначен: имя; Закрыт), справа Сообщений: N — длина диалога целиком, а не непрочитанное. Поэтому это бледный текст в весе времени, а не синий .badge, который в этом продукте всегда означает непрочитанное.
Чисел в фильтре и на вкладках нет намеренно. Закрытых диалогов у живого сайта десятки тысяч; счётчик рядом со статусом или на самой вкладке — это COUNT(*) по всей истории на каждое открытие экрана ради цифры, которую никто не читает. Список ленивый: он тянется по мере прокрутки и нигде не знает своей полной длины.
Две из трёх строк сегодня нечем заполнить — и это честная часть кадра. GetAllConversationsForSiteHandler.ToSummary собирает ConversationSummaryDto без LastMessagePreview и LastMessageAt (поля в контракте есть, этот список их не заполняет), а поля «сколько всего сообщений» нет вообще нигде. Вторая строка и Сообщений: N — это работа на стороне ago-chat, а не только рисование. Первая строка и статус доступны уже сейчас.
Свайп влево открывает удаление — и только у администратора. Право — conversation:erase; оно входит в набор администратора, который выдают RegisterSiteHandler и MintDemoTenantHandler вместе с site:erase и site:export, и отдельно оператору не выдаётся никогда. У оператора без этого права строка не свайпается вовсе — то же правило «скрывать, а не гасить», что и со скрепкой в треде. Под строкой — сплошной --danger, иконка и подпись в две строки, без других слов.
Но «удалить» здесь — это заявка, а не удаление. POST /conversations/{id}/erase отвечает 202 Accepted (RequestConversationErasureHandler): стирание происходит потом, отдельным шагом. Значит строка не имеет права просто исчезнуть по свайпу — иначе она вернётся при следующей подгрузке и будет выглядеть как сбой. Правильное поведение — подтверждение, затем строка в состоянии «стирается».
Состояние по умолчанию именно такое. «Не начат» и «Назначен» включены, «Закрыт» выключен: закрытый диалог — это архив, и оператор должен попросить его показать, а не пролистывать мимо. На предыдущем кадре «Закрыт» включён вручную — поэтому там в списке есть закрытая строка, а здесь нет.
Три флажка, три слова, ни одной цифры — ни напротив статуса, ни на чипе, ни на вкладке «Все». Панель взята целиком у меню профиля (.popover и .pop-item), флажок — тот же .cbox, что на экране очистки хранилища. Новой формы здесь не появилось, появилось новое содержимое.
Порядок — это назначаемый сервером sequence, никогда не часы, а вторая галочка — это Message.DeliveredAt, собственное подтверждение виджета, а не квитанция об отправке. Приход нового назначения никогда не переключает экран.
Скрепка на этом кадре нарисована, но она условная. Консоль рисует «Прикрепить» всегда (Composer.tsx). Приложение рисует скрепку ровно тогда, когда у открытого диалога hasAttachmentUploadGrant истинно — это значение задаётся по умолчанию для сайта переключателем allowAttachmentUploadsByDefault на экране виджета и переключается для конкретного диалога из шторки посетителя тем, у кого есть conversation:attachment_upload_grant. Когда вложения не разрешены — скрепки нет вовсе, а не «серая»: то же правило «скрывать, а не гасить», которому уже следуют сам переключатель и кнопка закрытия диалога. Одно решение здесь моё, а не бэкенда: грант формально про загрузки посетителя, а скрепка — про оператора. Если их надо развести — это решается сейчас, одной строкой.
Показано явно, а не только описано. Одна строка на атрибут — телефон, почта, имя — и значок ⚠️ ровно тогда, когда оператор ещё не подтвердил значение. Никаких пар «непроверенный телефон» / «проверенный телефон».
Это совпадает с тем, что уже сделала консоль: старый значок Verified/Unverified убрали, потому что он был Unverified на каждой строке без исключения. Настоящее состояние — это assessment: пусто, «Подтверждено» или «Недействительно», и третье красит строку, а не добавляет вторую.
Раскрытие замаскированного значения остаётся под аудитом: одна строка на каждое раскрытие в журнале, никогда не счётчик по оператору. Переключатель приёма файлов от посетителя живёт здесь — и это тот самый переключатель, от которого зависит скрепка в треде.
На планшете возвращается собственная форма консоли: list-detail-каркас с контекстом посетителя как третьей панелью, а нижняя панель становится боковой навигационной. Тот же код, одна точка перелома — последний кадр на этой странице. Планшет — приятное дополнение, а не цель: телефон основной инструмент, и ни один этап плана не завязан на широкий экран.
Каналы уведомлений — понятие уровня ОС, у веба аналога нет. Экран нарисован для мира, где push уже есть: так выглядит обещание. Всё выключено за одной полосой, называющей задачу бэкенда, которая должна приехать первой.
Экран показывает настройки уведомлений целиком и честно помечает то, что пока недоступно — вместо пустого экрана с одним объяснением, чего приложение не может. Так виден весь набор переключателей и логика каждого.
Пять каналов, по одному на вид события, потому что Android разрешает пользователю отключить канал мимо приложения — и это правильно: «диалог назначен мне» и «в команде написали» не должны выключаться вместе.