import { createRoot } from "react-dom/client"; import { Outlet, RouterProvider, createRootRoute, createRoute, createHashHistory, createRouter, } from "@tanstack/react-router"; import "./index.css"; import "@methanium/ui/index.css"; import NotFound from "@/routes/404"; import AppLayout from "@/routes/app/layout"; import { createSettingsRoute } from "@tensamin/settings"; import OnboardingGate from "@tensamin/onboarding"; import Home from "@/routes/app/home"; import ChatScreen from "@tensamin/chat/screen"; import CallScreen from "@tensamin/call/screen"; import Login from "@/routes/screens/login"; import CallPopout from "@tensamin/call/popout"; import ChatContext from "@tensamin/chat/context"; import { useCall, useInitializeCall } from "@tensamin/call/store"; import { useIsSpeaking } from "@tensamin/call/speakingState"; import { Provider as MTPProvider } from "@tensamin/mtp"; import UserProvider from "@tensamin/user/context"; import DeeplinkContext from "@tensamin/tauri/deeplinkHandler"; import NotificationsProvider from "@tensamin/notifications/context"; import TAuthWrapper from "@tensamin/tauth/context"; import { ErrorScreen, ThemeProvider, useTheme } from "@methanium/ui"; import z from "zod"; import { useEffect, useRef, useState, type ReactNode } from "react"; import Storage from "@tensamin/storage/context"; import Session from "@tensamin/storage/session"; import Crypto from "@tensamin/crypto/context"; import DesktopMediaProvider from "@tensamin/shared/desktopMedia"; import { log } from "@tensamin/shared/log"; import CacheSync from "@tensamin/cache/sync"; import { useStorage } from "@tensamin/storage/context"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { useIsMobile, Toaster, TooltipProvider } from "@methanium/ui"; import { isTauri } from "@tauri-apps/api/core"; const wrapper = document.getElementById("root"); if (!wrapper) { throw new Error("Missing root element"); } if (localStorage.getItem("log_level") === null) { console.log( "%cDo not paste anything in here, there is a 101.1% chance you're being scammed.", "color: red; font-size: 24px; font-weight: bold; background: #220000; padding: 8px 12px; border: 3px solid red;", ); } // @ts-expect-error Declare global function window.setLogLevelToMax = () => { localStorage.setItem("log_level", String(1000)); location.reload(); }; function LoginWrapper({ children }: { children: ReactNode }) { const [loggedIn, setLoggedIn] = useState(null); const { load, secureStorage } = useStorage(); const navigate = useNavigate(); const location = useLocation(); useEffect(() => { if (secureStorage === null) return; let active = true; Promise.all([load("user_id"), load("mtp_keyring")]) .then(([userId, keyring]) => { if (!active) return; if (userId !== 0 && keyring !== "") { setLoggedIn(true); if (location.pathname === "/login") { void navigate({ to: "/", replace: true }); } return; } setLoggedIn(false); void navigate({ to: "/login", replace: true, }); }) .catch((error) => { log(0, "login", "red", "Failed to load login state", error); }); return () => { active = false; }; }, [load, location.pathname, navigate, secureStorage]); if (loggedIn !== true && location.pathname !== "/login") { return null; } return children; } function ThemeStorageBridge() { const { load, save } = useStorage(); const { themeColor, setThemeColor, themePalette, setThemePalette, themePrimaryColor, setThemePrimaryColor, themePolarity, setThemePolarity, themeTint, setThemeTint, themeBorderRadius, setThemeBorderRadius, themeCustomCss, setThemeCustomCss, parentThemeId, setParentThemeId, applyThemePreset, themeDesign, setThemeDesign, } = useTheme(); const loadedRef = useRef(false); useEffect(() => { let active = true; Promise.all([ load("theme_color"), load("theme_palette"), load("theme_primary_color"), load("theme_polarity"), load("theme_tint"), load("theme_border_radius"), load("theme_custom_css"), load("theme_parent"), load("theme_design"), ]).then( ([ color, palette, primaryColor, polarity, tint, borderRadius, customCss, parent, design, ]) => { if (!active) { return; } if (customCss === "" && parent) applyThemePreset(parent); else setParentThemeId(parent || null); setThemeColor(color); setThemePalette(palette); setThemePrimaryColor(primaryColor); setThemePolarity(polarity); setThemeTint(tint); setThemeBorderRadius(borderRadius); if (customCss !== "") setThemeCustomCss(customCss); setThemeDesign(design); loadedRef.current = true; }, ); return () => { active = false; }; }, [ load, applyThemePreset, setThemeBorderRadius, setThemeColor, setThemeCustomCss, setParentThemeId, setThemeDesign, setThemePalette, setThemePolarity, setThemePrimaryColor, setThemeTint, ]); useEffect(() => { if (loadedRef.current) save("theme_color", themeColor); }, [save, themeColor]); useEffect(() => { if (loadedRef.current) save("theme_palette", themePalette); }, [save, themePalette]); useEffect(() => { if (loadedRef.current) save("theme_primary_color", themePrimaryColor); }, [save, themePrimaryColor]); useEffect(() => { if (loadedRef.current) save("theme_polarity", themePolarity); }, [save, themePolarity]); useEffect(() => { if (loadedRef.current) save("theme_tint", themeTint); }, [save, themeTint]); useEffect(() => { if (loadedRef.current) save("theme_border_radius", themeBorderRadius); }, [save, themeBorderRadius]); useEffect(() => { if (loadedRef.current) save("theme_custom_css", themeCustomCss); }, [save, themeCustomCss]); useEffect(() => { if (loadedRef.current) save("theme_parent", parentThemeId ?? ""); }, [parentThemeId, save]); useEffect(() => { if (loadedRef.current) save("theme_design", themeDesign); }, [save, themeDesign]); return null; } function RootShell() { const isMobile = useIsMobile(); return (
); } function AppShell() { return ( ); } function createCallTrayIcon(color: string, speaking: boolean) { const canvas = document.createElement("canvas"); canvas.width = 32; canvas.height = 32; const context = canvas.getContext("2d"); if (!context) return undefined; context.globalAlpha = speaking ? 1 : 0.55; context.fillStyle = color; context.beginPath(); context.arc(16, 16, 13, 0, Math.PI * 2); context.fill(); if (speaking) { context.globalAlpha = 0.3; context.fillStyle = "#ffffff"; context.fill(); } return canvas.toDataURL("image/png"); } function CallInit() { useInitializeCall(); const { load } = useStorage(); const { themeColor, themePalette, themePrimaryColor, themePolarity, themeTint, themeCustomCss, } = useTheme(); const [localUserId, setLocalUserId] = useState(-1); const [primaryColor, setPrimaryColor] = useState(""); const inCall = useCall((state) => state.state === "open"); const speaking = useIsSpeaking(localUserId); useEffect(() => { let active = true; load("user_id").then((userId) => { if (active) setLocalUserId(userId); }); return () => { active = false; }; }, [load]); useEffect(() => { const frame = requestAnimationFrame(() => { setPrimaryColor( getComputedStyle(document.documentElement) .getPropertyValue("--primary") .trim(), ); }); return () => cancelAnimationFrame(frame); }, [ themeColor, themeCustomCss, themePalette, themePolarity, themePrimaryColor, themeTint, ]); useEffect(() => { const iconDataUrl = primaryColor ? createCallTrayIcon(primaryColor, speaking) : undefined; void window.tensaminDesktop?.call ?.setStatus?.({ inCall, speaking: inCall && speaking, iconDataUrl: inCall ? iconDataUrl : undefined, }) .catch((error: unknown) => { console.error("Failed to update desktop call status", error); }); }, [inCall, primaryColor, speaking]); return null; } const rootRoute = createRootRoute({ component: RootShell, errorComponent: ({ error }: { error: Error }) => ( ), }); const appRoute = createRoute({ getParentRoute: () => rootRoute, id: "app", component: AppShell, notFoundComponent: NotFound, }); const settingsRoute = createSettingsRoute(appRoute); const homeRoute = createRoute({ getParentRoute: () => appRoute, path: "/", component: Home, staticData: { showMobileNavbar: true, }, }); const chatRoute = createRoute({ getParentRoute: () => appRoute, path: "chat", component: ChatScreen, validateSearch: z.object({ id: z.number().optional(), }), staticData: { showMobileNavbar: false, }, }); const callRoute = createRoute({ getParentRoute: () => appRoute, path: "call", component: CallScreen, staticData: { showMobileNavbar: false, }, }); const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: "login", component: Login, staticData: { showMobileNavbar: false, }, }); const routeTree = rootRoute.addChildren([ appRoute.addChildren([homeRoute, chatRoute, callRoute, settingsRoute]), loginRoute, ]); const router = createRouter({ routeTree, history: window.location.protocol === "file:" ? createHashHistory() : undefined, defaultNotFoundComponent: NotFound, }); declare module "@tanstack/react-router" { interface Register { router: typeof router; } } createRoot(wrapper).render();