Files

128 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Интерфейс
Визуальная система — `docs/identity/brief.md`, эталон экрана чата — `docs/identity/screens.html`. Здесь — состав экранов, поведение и тексты. Все тексты — русские, строчными, как в моке; заглавная только в начале предложений из нескольких слов.
## Каркас
Одна страница `index.html`, роутинг по hash: `#/` — список (на десктопе — первый чат), `#/dm/<nick>`, `#/room/<id>`, `#/room/<id>/members`, `#/contact/<nick>`, `#/settings`, `#/new`. Десктоп (≥ 760 px): сайдбар 224 px + чат. Мобильный: один экран за раз, «назад» — в шапке слева.
Без inline-стилей и inline-скриптов (CSP). Рендер — `document.createElement` и `textContent`; `innerHTML` не используется нигде: сообщения — пользовательские данные.
Страница ведёт себя как приложение, а не как документ (ADR-075): масштабирования нет, резинового отскока нет, выделение снято с шапки, сайдбара, кнопок и разделителей — но не с текста сообщений, отпечатков, полей ввода и версии в подвале сайдбара: их копируют. Установленное на «Домой» приложение объявлено мета-строками, имя на экране — `bare`, системная полоса остаётся над страницей. Подвал сайдбара и строка ввода отступают от нижней системной полосы iPhone; у строки весь нижний отступ становится 6 px, пока край уже закрыт экранной клавиатурой (ADR-076).
## Вход и регистрация
Одна страница, два режима переключателем «вход / регистрация». Логотип-знак и `bare` сверху.
Поля: `ник`, `пароль`. В регистрации дополнительно `инвайт-код`, если `config.inviteRequired`, и текст под паролем:
> пароль — это ключ шифрования, а не запись в базе. восстановления нет. не короче 12 символов; лучше — фраза из нескольких слов.
Кнопка одна, в стиле строки ввода. Пока идёт PBKDF2 — состояние «вычисляем ключ…», кнопка заблокирована. Ошибки — строкой под формой цветом `mark`: «неверный ник или пароль», «ник занят», «ник: 2–32 символа, a–z, 0–9, _», «нужен инвайт-код», «инвайт-код не подходит». Форму ника и длину пароля клиент проверяет сам, до PBKDF2, в обоих режимах. Остальные состояния — «Тексты состояний».
Если на устройстве лежат ключи другого ника, до вычисления ключа — подтверждение «на этом устройстве история @nick. вход под другим ником удалит её.» с кнопками «экспортировать», «удалить» и «отмена» (ADR-029, ADR-051). «экспортировать» скачивает `.bare` прежнего аккаунта и подтверждение не закрывает; сессия для этого не нужна. База стирается после успешного входа или регистрации; отказ сервера её не трогает.
## Список чатов (сайдбар)
Секции «каналы» и «личные», как в моке. Активный чат — инверсия (ink на bone). Непрочитанные — число цветом `mark` справа. Порядок — по `lastId` по убыванию. Внизу — «ты: @nick», по нажатию — настройки. Над секциями — строка `+ новый чат`.
В подвале рядом с «ты: @nick» — версия и время коммита из `GET /api/config`: `версия · дд.мм чч:мм`, время в местной зоне, цветом `stone`, размером подписи. Не кнопка, но выделяется: её копируют. Времени нет — остаётся версия; конфигурации ещё нет — строки нет. В сайдбаре 224 px рядом они не помещаются: версия занимает вторую строку и стоит справа; на мобильном, где сайдбар во всю ширину, обе стоят в одной строке. Ник, которому не хватило места, обрезается многоточием (ADR-067).
Версия — серверная. Оболочка в браузере бывает старше: она приезжает из кэша, а обновление применяется отдельно и ждёт пустых полей (ADR-068).
## Новый чат (`#/new`)
Один экран с переключателем двух взаимоисключающих действий: «личный чат / новая комната». Видна только форма выбранного действия; начальный режим — личный чат, но поле само не получает фокус.
«Личный чат»: текст «напишите человеку по нику.», поле «ник человека» с подсказкой `@ник`, кнопка «написать». Ошибки: «такого ника нет», «нельзя писать себе».
«Новая комната»: текст «создайте новую приватную комнату. сначала в ней будете только вы; участников добавите следующим шагом.» и пояснение «если вас добавят в чужую комнату, она появится в списке сама.». Поле «название новой комнаты» с подсказкой `#название`, кнопка «создать комнату». Имя не ищет и не открывает существующую комнату: оно не уникально, а доступ даёт владелец добавлением по нику. После создания открывается экран участников новой комнаты (ADR-078).
## Чат
Шапка: имя (`#general` / `@marta`), по нажатию — участники или карточка контакта. У комнаты в той же кнопке стоит подпись «· участники N»; это число состава, не присутствующих онлайн. Без темы и «N онлайн».
Лента: поток блоков сверху вниз; подряд идущие сообщения одного автора — без повтора автора над группой. На телефоне и планшете свои сообщения выровнены по правому краю, чужие — по левому; на десктопе все сообщения стоят слева (ADR-077). Десктоп определяется точным указателем с наведением, а не шириной окна: широкий планшет остаётся планшетом. Свой ник остаётся цветом `mark`. Ширина блока сообщения ограничена — иначе выравнивать нечего, — текст внутри блока всегда выровнен по левому краю. Разделители дат и «новые» — во всю ширину ленты: линия с датой; «новые» — линия цветом `mark` перед первым непрочитанным, исчезает при следующем открытии чата. Pending — текст цветом `stone`; failed — с пометкой «не отправлено · повторить». Нерасшифрованное — курсивом: «не удалось расшифровать: ключ изменился» / «…: нет ключа комнаты». Время — `ts` в локальной зоне, `ЧЧ:ММ`.
Лента открывается последними 50 сообщениями и стоит в конце. Прокрутка к верхнему краю подгружает следующие 50; то, что человек читает, при этом не двигается. Загруженное остаётся в разметке целиком — виртуализации нет (ADR-053).
Ввод: рамка 1 px ink, слева `>` цветом `mark`, подсказка «сообщение в #general» / «сообщение»; имя комнаты в подсказке обрезается до 12 символов многоточием — «сообщение в #длинноеимя…» (ADR-061), в шапке оно остаётся полным. Enter — отправить, Shift+Enter — перенос; на мобильном Enter — перенос, отправка — кнопка `>` справа. Подсказка «enter — отправить» только на десктопе. Лимит 4000 — счётчик появляется после 3500; нативный `maxLength` не пускает лишнее. Тап по неинтерактивному месту истории снимает фокус со строки и закрывает клавиатуру. Пока открыта экранная клавиатура, весь нижний отступ строки заменяется 6 px; при физической клавиатуре и после закрытия обычный отступ сохраняется (ADR-076).
Строка сообщения — `textarea` (ADR-076): она принимает только текст, сама держит вставку, выделение, отмену, подсказку, предел и состояние `disabled`. Растёт под содержимое, дальше семи строк — прокрутка. Системную панель со стрелками и «готово» iOS рисует поверх клавиатуры и для `textarea`, и для `contenteditable`; у веб-страницы нет поддерживаемого API, чтобы её скрыть. Полное управление этой панелью потребовало бы отдельной нативной iOS-оболочки; собственная экранная клавиатура в веб-клиенте не делается.
Предупреждение о ключе — полоса над вводом цветом `mark`: «ключ @marta изменился. сверьте отпечаток лично. [доверять новому ключу]». Ввод заблокирован до подтверждения. Полоса одна: предупреждение о ключе перебивает отказ отправки и «нет соединения» (ADR-038).
Комната, из состава которой нас больше нет (вышли сами, убрал владелец, комната удалена), — та же полоса цветом `mark`: «вы больше не участник комнаты». Ввод заблокирован, лента остаётся. Эта полоса перебивает и предупреждение о ключе (ADR-044).
Отказ отправки — та же полоса над вводом цветом `mark` с текстом из поля `error` последнего неотправленного сообщения (ADR-033): «проверьте часы на устройстве: расхождение больше 5 минут», «слишком часто, попробуйте позже», «сервер не справился, попробуйте позже». Полоса исчезает при следующей попытке. Ввод не блокируется.
Первое отправленное сообщение за всю историю устройства → запрос разрешения на уведомления (см. «Уведомления»).
## Карточка контакта (`#/contact/<nick>`)
`@nick`, отпечаток 64 hex группами по 4 в две строки, строка «сверьте с собеседником голосом или лично». Если есть `pending` — оба отпечатка с пометками «старый» и «новый» и кнопка «доверять новому ключу». Кнопка «убрать из списка».
`pending` снимает и сервер, снова отдавший доверенный ключ: смены ключа не случилось, состояние закрывается само и молча (ADR-040).
## Участники (`#/room/<id>/members`)
Заголовок «участники» и список ников; у владельца — пометка «владелец». Владельцу: строка ввода `@ник` + «добавить», у каждого участника «убрать». Всем: «выйти из комнаты»; владельцу — «удалить комнату» с подтверждением «комната будет удалена у всех участников.» и кнопками «удалить» и «отмена». Если клиент-владелец получил `needsRekey` и не может выполнить rekey из-за неподтверждённого ключа — полоса: «нужен новый ключ комнаты: подтвердите ключ @x». Тот же текст — строкой состояния формы, когда неподтверждённый ключ обрывает добавление или удаление участника; ников в нём бывает несколько, через запятую (ADR-038).
## Настройки (`#/settings`)
- «ты: @nick», свой отпечаток.
- «уведомления»: состояние (`включены` / `выключены` / `запрещены в браузере`), кнопка «включить» или «выключить». `запрещены в браузере` — разрешение отклонено или уведомлений в браузере нет вовсе; кнопки в этом состоянии нет (ADR-046). На iOS вне PWA — состояние `выключены` и вместо кнопки текст про установку, тот же, что в баннере.
- «установить приложение»: кнопка «установить», если есть `beforeinstallprompt`; на iOS вне PWA — инструкция «поделиться → на экран «домой»». Устанавливать нечего — раздела нет.
- «устройства»: список `id` (первые 8 символов), дата появления, «удалить». У своей строки кнопки нет: вместо неё пометка «это устройство». Своё устройство отсюда не отцепляется — это делает «выйти», где спрашивают про историю (ADR-052).
- «история»: «занято N МБ» — оценка браузера, до десятых, пока меньше десяти, дальше целые; браузер, который её не даёт, строки не показывает (ADR-052). «экспорт» → скачивание `.bare`; «импорт» → выбор файла → «добавлено N сообщений» / «архив создан другим аккаунтом» / «файл повреждён». Число согласуется со словом: «добавлено 1 сообщение», «добавлено 2 сообщения», «добавлено 5 сообщений» (ADR-051).
- «сменить пароль»: старый, новый, повтор; чекбокс «выйти на других устройствах». Ответ — «пароль изменён».
- «выйти»: подтверждение «история на этом устройстве будет удалена. экспортировать сначала?» с кнопками «экспортировать», «выйти», «отмена». «экспортировать» — то же скачивание, что и в разделе «история»; подтверждение оно не закрывает (ADR-051).
- «удалить аккаунт»: пароль + подтверждение «аккаунт и вся история будут удалены навсегда.» с кнопками «удалить» и «отмена».
## Баннер установки (iOS)
Показывается при `iPhone|iPad` и `navigator.standalone !== true`, над списком чатов: «уведомления на iOS работают только у установленного приложения: поделиться → на экран «домой»». Крестик — «×» с подписью «закрыть» для экранного диктора — ставит `installBannerDismissed`, повтор не показывается.
## Уведомления
Запрос разрешения — после первого успешно отправленного сообщения, один раз (`notificationsAsked`). После `granted``pushManager.subscribe` с `vapidPublicKey` и `PUT /api/devices/{id}/push`. Отказ — молча; включить можно в настройках.
Выключенные кнопкой уведомления сами не включаются: ни первым сообщением, ни запуском приложения. Обратно их включает только кнопка (ADR-049).
Уведомление: заголовок — `@nick` отправителя или `#имя комнаты`, текст — «новое сообщение», нажатие открывает этот чат. Содержимого сообщения в уведомлении нет: сервер его не знает (ADR-011). Пуш о собственном сообщении не приходит (ADR-045).
## Сеть и состояния
- SSE переподключается браузером; после `ready` клиент перечитывает комнаты и контакты и повторяет `pending`.
- Вкладок одного профиля бывает несколько; поток событий держит одна из них, остальные получают изменения от неё и выглядят так же (ADR-035).
- Без сети: полоса «нет соединения» цветом `stone` над вводом; ввод не блокируется — сообщения уходят в `pending`.
- `clock_skew` — «проверьте часы на устройстве: расхождение больше 5 минут».
- Приложение обновляется само: клиент спрашивает сервер о новой оболочке при запуске и при возвращении в приложение, не чаще раза в минуту, включает установленную версию и перезагружает страницу. Перезагрузка вкладки ждёт, пока пусты все её поля и строка сообщения: набранное она бы унесла. Опустевшее поле она замечает и там, где о нём не сказало ни одно событие — отправленное сообщение, уход с экрана: к отложенному вкладка возвращается по таймеру (ADR-072). Вкладок бывает несколько (ADR-035), и каждая ждёт своих полей: соседняя может включить новую оболочку раньше, но перезагрузиться под набранным текстом не заставит. Без сети проверка проходит молча (ADR-068).
- Перезагружается вкладка только тогда, когда версия оболочки и правда сменилась. Тот же файл воркера, отданный сервером заново, её не трогает: перезагрузка вернула бы ту же страницу. Между перезагрузками — не меньше 30 секунд; сколько бы релизов ни вышло за жизнь вкладки, доезжают все (ADR-070).
- `401 unauthenticated` на любом запросе — выход на экран входа с сохранением IndexedDB (сессия истекла, история остаётся). Исключение одно: служебный выход перед повторным входом при смене пароля и удалении аккаунта (ADR-031) — там этот ответ означает, что сессии и так нет.
## Тексты состояний
Общие для всех форм строки (ADR-028). Ошибка — цветом `mark`, ответ об успехе — цветом `mute`, место одно.
| состояние | текст |
|---|---|
| запрос не дошёл | «нет соединения» |
| код ответа, на который нет сценария (`internal`, `too_large`, прочее) | «сервер не справился, попробуйте позже» |
| `429 rate_limited` | «слишком часто, попробуйте позже» |
| пароль короче 12 символов | «пароль: не короче 12 символов» |
| новый пароль и повтор различаются | «пароли не совпадают» |
| ключевой блоб не разобран, не расшифрован или не соответствует публичному ключу | «ключ аккаунта повреждён» |
| `iter` блоба не равен ответу `GET /api/kdf` | «параметры ключа не совпали» |
| `401 invalid_credentials` в настройках | «неверный пароль» |
| архив не собрался: истории не прочитать или ключей аккаунта на устройстве нет | «экспорт не удался» |
| разобранный архив не дошёл до базы: нет места или ключей аккаунта | «импорт не удался» |
## Доступность
Семантика: `nav`, `main`, `form`, `button`, `ul/li` для списков; `aria-live="polite"` на ленте; фокус в строку ввода при открытии чата на десктопе; контраст ink/bone и mark/bone не ниже 4.5:1; цели нажатия на мобильном не меньше 44 px.
Строка сообщения — нативная `textarea` (ADR-076): роль многострочного поля и состояние `disabled` сообщает сам браузер; доступное имя повторяет текст видимой подсказки через `aria-label`, кнопка `>` объявляется как «отправить».
Масштабирование запрещено — и щипок, и двойной тап (ADR-075). Это сознательный размен: интерфейс ведёт себя как приложение, а человек, которому нужно увеличить мелкое, средства лишается. Размеры шрифтов от запрета не меняются: поля ввода остаются 14 px, как в `docs/identity/brief.md`. Браузер вправе запрет проигнорировать.