91 lines
4.2 KiB
TypeScript
91 lines
4.2 KiB
TypeScript
import { render } from 'preact';
|
||
import { readBootstrap } from './bootstrap';
|
||
import { App } from './App';
|
||
import { createT } from './i18n';
|
||
import { WidgetApi, buildCapabilities } from './widget-api';
|
||
import './styles.css';
|
||
|
||
// Input-mode detector for hover styling. CSS gates `:hover` and
|
||
// `:focus-visible` rules on `:root[data-input="mouse"]` because Capacitor's
|
||
// Android Chromium WebView synthesises `:hover` on the focused element
|
||
// after a tap and never clears it until the next interaction elsewhere —
|
||
// without the gate, every tap leaves a sticky hover state on the tapped
|
||
// card («card greys out after tap and only un-greys when you tap a
|
||
// different button»).
|
||
//
|
||
// Truth comes from `pointerdown.pointerType`. The capture-phase listener
|
||
// runs in the same task as any post-tap `:hover` synthesis, so a touch
|
||
// tap on Android lands in 'touch' mode in the same render frame as the
|
||
// synthesised hover would paint.
|
||
//
|
||
// Initial mode is plain 'mouse'. matchMedia-based guessing was tried
|
||
// here and dropped — every interaction-media query is mis-reported on at
|
||
// least one shipping device: Capacitor Android WebView falsely matches
|
||
// `hover: hover` and `any-pointer: fine` on pure-touch phones;
|
||
// Samsung / OnePlus / Moto Androids expose a virtual-mouse HID and
|
||
// falsely match `pointer: fine`; older Firefox-on-Windows desktops
|
||
// reported `pointer: coarse` despite a real mouse. Defaulting to 'mouse'
|
||
// is strictly no worse than any of those queries on any device: a
|
||
// desktop / hybrid user gets hover affordances from frame zero, and a
|
||
// touch user cannot trigger `:hover` before tapping because there is no
|
||
// pointer hovering anything — by the time the first tap fires
|
||
// `:hover` (synthesised), our listener has already moved the attribute
|
||
// to 'touch'. Pen / stylus also lands in 'touch' (pointerType is `pen`,
|
||
// matched by the `!== 'mouse'` branch).
|
||
const setInputMode = (mode: 'touch' | 'mouse'): void => {
|
||
document.documentElement.dataset.input = mode;
|
||
};
|
||
setInputMode('mouse');
|
||
window.addEventListener(
|
||
'pointerdown',
|
||
(event) => {
|
||
setInputMode(event.pointerType === 'mouse' ? 'mouse' : 'touch');
|
||
},
|
||
{ passive: true, capture: true }
|
||
);
|
||
|
||
const root = document.getElementById('app');
|
||
if (!root) {
|
||
throw new Error('#app root element missing — index.html out of sync');
|
||
}
|
||
|
||
const result = readBootstrap(window.location.search);
|
||
|
||
if (!result.ok) {
|
||
// Either someone opened the widget URL directly (no host params), or a
|
||
// host bug failed to provide them. Either way render a self-contained
|
||
// diagnostic instead of going silent. Bootstrap failed before we could
|
||
// read clientLanguage from the URL, so let createT fall back to
|
||
// navigator.language.
|
||
const t = createT();
|
||
render(
|
||
<div class="app">
|
||
<div class="error-banner">
|
||
<strong>{t('bootstrap.failed')}</strong>
|
||
{t('bootstrap.missing-params', { names: result.missing.join(', ') })}{' '}
|
||
{t('bootstrap.embedded-only', { route: '/bots/telegram' })}
|
||
</div>
|
||
</div>,
|
||
root
|
||
);
|
||
} else {
|
||
// Apply initial theme synchronously so the first paint isn't flashed
|
||
// through the wrong palette.
|
||
document.documentElement.dataset.theme = result.bootstrap.theme;
|
||
|
||
// Instantiate the WidgetApi BEFORE React render. The constructor attaches
|
||
// the `window.addEventListener('message', ...)` listener synchronously,
|
||
// so by the time the host's ClientWidgetApi fires its capabilities
|
||
// request on iframe `load` we're already listening.
|
||
//
|
||
// The pre-fix flow built the WidgetApi inside App.tsx's useEffect, which
|
||
// runs AFTER React's first commit. On a fresh mount the bundle parse +
|
||
// initial render took long enough for the host's request to arrive
|
||
// after the listener was attached, so it worked by accident. On the
|
||
// *second* mount (after «Show chat» → «Show widget») the bundle is
|
||
// browser-cached and parses near-instantly; the host's request raced
|
||
// ahead of useEffect, the listener missed it, and capability handshake
|
||
// hung forever — only the «Соединение с Vojo…» diag line ever showed.
|
||
const api = new WidgetApi(result.bootstrap, buildCapabilities(result.bootstrap.roomId));
|
||
render(<App bootstrap={result.bootstrap} api={api} />, root);
|
||
}
|