iOS: запрет масштабирования, версия в подвале, самообновление, строка ввода

Отзыв с iPhone после первого живого прогона.

Масштабирование запрещено (ADR-066): viewport как в соседнем sixlines плюс
viewport-fit=cover, мета-теги установленного приложения, touch-action против
двойного тапа и страховка на JS — гашение жестов iOS, потому что мета-строку
Safari вправе проигнорировать. Размеры шрифтов не тронуты: 14 px из брифа
остаются, поднимать поля до 16 px владелец отклонил. Цена — доступность
для тех, кому зум нужен — записана в ADR и в ui.md честно.

Версия в подвале (ADR-065, 067): GET /api/config отдаёт короткую ревизию
и время коммита. Именно коммита, а не сборки: штамп компиляции в бинаре
means каждая пересборка даёт новый хеш, и обещание ADR-022 «бинарь собирается
из тега байт в байт» перестаёт проверяться.

Самообновление PWA (ADR-068, 070): в режиме приложения на iPhone перезагрузки
нет, и старая версия жила бы вечно. Проверка при возврате в приложение,
включение ждущей оболочки, перезагрузка — но не тогда, когда в строке набран
текст. Замок от петли сравнивает версию оболочки, а не считает перезагрузки:
счётчик отменял саму фичу, долгоживущая вкладка переставала обновляться.
Замерено: было 30, 60, 120, 240 секунд между релизами, стало ровно 30.

Строка ввода — редактируемый блок (ADR-069): iOS рисует над клавиатурой
полосу со стрелками и «готово» для полей формы, и убрать её со страницы
нельзя. Для contenteditable не рисует. Режим plaintext-only с проверкой,
что он применился, и запасным путём с разбором вставки: невалидное значение
атрибута в части браузеров делает блок нередактируемым вовсе. Чтение
и запись только через textContent, innerHTML в клиенте по-прежнему нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
2026-08-23 15:11:13 +03:00
co-authored by Claude Opus 5
parent cfd0ec0ce9
commit 383e374f0f
31 changed files with 1462 additions and 79 deletions
+30 -2
View File
@@ -5,6 +5,12 @@ import * as pwa from "../pwa.js";
import { INSTALL_IOS, clear, el, mark } from "./dom.js";
import { mount } from "./chats.js";
// Время коммита — в местной зоне и коротко: день с месяцем и часы
// с минутами (ADR-067). Год не показывается: версия отвечает на вопрос
// «что сейчас работает», а не ведёт летопись.
const BUILD_DAY = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit" });
const BUILD_TIME = new Intl.DateTimeFormat("ru-RU", { hour: "2-digit", minute: "2-digit" });
// frame отдаёт корень, место под экран и отписку списка чатов.
// screen — что показывать на мобильном, где виден один экран за раз:
// "list" или "screen". active — чат, который сейчас открыт.
@@ -38,17 +44,39 @@ function side(ctx, active) {
list.append(add, items);
nav.append(list);
const foot = el("div", "foot");
const me = el("button", "me");
me.type = "button";
const dot = el("i");
dot.setAttribute("aria-hidden", "true");
me.append(dot, el("span", null, `ты: @${ctx.me.nick}`));
me.append(dot, el("span", "me__nick", `ты: @${ctx.me.nick}`));
me.addEventListener("click", () => ctx.go("#/settings"));
nav.append(me);
foot.append(me);
const version = build(ctx.config);
if (version !== null) {
foot.append(el("span", "build", version));
}
nav.append(foot);
return { nav, dispose: mount(items, ctx, active) };
}
// build — версия и время коммита из GET /api/config (ADR-065, ADR-067).
// Конфигурации нет — офлайн-старт до первого ответа сервера — значит,
// и строки нет: выдумывать версию не из чего. Время без версии не бывает:
// её сервер отдаёт всегда, хотя бы как «unknown».
function build(config) {
const version = config?.version;
if (typeof version !== "string" || version === "") {
return null;
}
const at = config?.commitAt;
if (!Number.isFinite(at) || at <= 0) {
return version;
}
return `${version} · ${BUILD_DAY.format(at)} ${BUILD_TIME.format(at)}`;
}
// banner — баннер установки на iOS: пуши там работают только
// у установленного приложения (ADR-011). Крестик закрывает его насовсем.
async function banner(place) {