vojo/apps/widget-telegram/src/main.tsx

91 lines
4.2 KiB
TypeScript
Raw 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.

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);
}