/* eslint-disable import/first */ import React from 'react'; import { createRoot } from 'react-dom/client'; import { enableMapSet } from 'immer'; import '@fontsource/inter/variable.css'; import '@fontsource-variable/jetbrains-mono/index.css'; import 'folds/dist/style.css'; import { configClass, varsClass } from 'folds'; enableMapSet(); import './app/styles/global.css'; import './index.css'; import { trimTrailingSlash } from './app/utils/common'; import App from './app/pages/App'; // import i18n (needs to be bundled ;)) import './app/i18n'; import { pushSessionToSW } from './sw-session'; import { getFallbackSession } from './app/state/sessions'; import { setupExternalLinkHandler } from './app/utils/capacitor'; import { setupExternalLinkHandler as setupElectronExternalLinkHandler } from './app/utils/electron'; document.body.classList.add(configClass, varsClass); setupExternalLinkHandler(); setupElectronExternalLinkHandler(); // Input-mode detector for hover/focus styling. Capacitor's Android Chromium // WebView synthesises `:hover` and `:focus-visible` on the focused element // after a tap and never clears them until the next interaction elsewhere, // so without a gate every tap leaves a sticky highlight on the tapped // button. The widget-telegram bundle uses the same pattern — see // `apps/widget-telegram/src/main.tsx`. matchMedia interaction queries are // unreliable on Android WebView (`hover: hover` reports TRUE on a pure-touch // device); the only honest signal is `pointerdown.pointerType`, which the // WebView reports truthfully. Initial mode is 'mouse' so desktop/hybrid // users get hover affordances on first paint — a touch user cannot trigger // hover before tapping, and our capture-phase listener moves the attribute // to 'touch' in the same frame as any synthesised :hover would paint. const setVojoInputMode = (mode: 'touch' | 'mouse'): void => { document.documentElement.dataset.input = mode; }; setVojoInputMode('mouse'); window.addEventListener( 'pointerdown', (event) => { setVojoInputMode(event.pointerType === 'mouse' ? 'mouse' : 'touch'); }, { passive: true, capture: true } ); // Register Service Worker if ('serviceWorker' in navigator) { const swUrl = import.meta.env.MODE === 'production' ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js` : `/dev-sw.js?dev-sw`; const sendSessionToSW = () => { const session = getFallbackSession(); pushSessionToSW(session?.baseUrl, session?.accessToken); }; navigator.serviceWorker.register(swUrl).then(sendSessionToSW); navigator.serviceWorker.ready.then(sendSessionToSW); navigator.serviceWorker.addEventListener('message', (ev) => { const { type } = ev.data ?? {}; if (type === 'requestSession') { sendSessionToSW(); return; } if (type === 'notificationClick') { const { roomId, isInvite } = ev.data ?? {}; window.dispatchEvent(new CustomEvent('vojo:pushNavigate', { detail: { roomId, isInvite } })); return; } if (type === 'pushSubscriptionChange') { window.dispatchEvent(new CustomEvent('vojo:pushSubscriptionChange')); } }); } const mountApp = () => { const rootContainer = document.getElementById('root'); if (rootContainer === null) { // Bootstrap failure — only surfaces if `index.html` is broken; logging // is the only path here since React hasn't mounted yet. // eslint-disable-next-line no-console console.error('Root container element not found!'); return; } const root = createRoot(rootContainer); root.render(); }; mountApp();