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

63 lines
2.5 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 — see apps/widget-telegram/src/main.tsx for the
// full rationale. Capacitor Android WebView mis-reports `hover: hover`
// on touch devices, so we drive `:hover` styling off the actual
// `pointerdown.pointerType` instead of media queries. The initial guess
// based on `(any-pointer: coarse)` covers the pre-first-pointerdown
// frame so a touch device doesn't briefly show mouse-mode hover
// affordances if the user immediately taps a card.
const setInputMode = (mode: 'touch' | 'mouse'): void => {
document.documentElement.dataset.input = mode;
};
setInputMode(window.matchMedia('(any-pointer: coarse)').matches ? 'touch' : '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. Render a self-contained diagnostic
// instead of going silent. Bootstrap failed before we could read
// clientLanguage, 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/discord' })}
</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 WidgetApi BEFORE React render. The constructor attaches
// the message listener synchronously, so by the time the host's
// ClientWidgetApi fires its capabilities request on iframe `load`,
// we're already listening. Constructing inside a useEffect would race
// with the cached-bundle remount path. See widget-telegram for full
// rationale.
const api = new WidgetApi(result.bootstrap, buildCapabilities(result.bootstrap.roomId));
render(<App bootstrap={result.bootstrap} api={api} />, root);
}