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(
{t('bootstrap.failed')} {t('bootstrap.missing-params', { names: result.missing.join(', ') })}{' '} {t('bootstrap.embedded-only', { route: '/bots/telegram' })}
, 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(, root); }