Files
modeling-app/src/App.tsx
Zookeeper Lee 46b6707e3a Fix the black screen of death (#7238)
* Fix the black screen of death

* fmt

* make check

* Clean up

* Fix up zoom to fit

* Change how emulateNetworkConditions work

* Do NOT use browser's offline/online mechanisms

* Fix test
2025-05-29 15:02:12 -04:00

244 lines
7.4 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import toast from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise'
import {
useLoaderData,
useLocation,
useNavigate,
useSearchParams,
} from 'react-router-dom'
import { AppHeader } from '@src/components/AppHeader'
import { EngineStream } from '@src/components/EngineStream'
import Gizmo from '@src/components/Gizmo'
import { LowerRightControls } from '@src/components/LowerRightControls'
import { useLspContext } from '@src/components/LspProvider'
import { ModelingSidebar } from '@src/components/ModelingSidebar/ModelingSidebar'
import { UnitsMenu } from '@src/components/UnitsMenu'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { useQueryParamEffects } from '@src/hooks/useQueryParamEffects'
import { useEngineConnectionSubscriptions } from '@src/hooks/useEngineConnectionSubscriptions'
import { useHotKeyListener } from '@src/hooks/useHotKeyListener'
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths'
import {
billingActor,
sceneInfra,
codeManager,
kclManager,
settingsActor,
} from '@src/lib/singletons'
import { maybeWriteToDisk } from '@src/lib/telemetry'
import type { IndexLoaderData } from '@src/lib/types'
import { engineStreamActor, useSettings, useToken } from '@src/lib/singletons'
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
import { BillingTransition } from '@src/machines/billingMachine'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import { ShareButton } from '@src/components/ShareButton'
import {
needsToOnboard,
TutorialRequestToast,
} from '@src/routes/Onboarding/utils'
import { reportRejection } from '@src/lib/trap'
import { DownloadAppToast } from '@src/components/DownloadAppToast'
import { WasmErrToast } from '@src/components/WasmErrToast'
import openWindow from '@src/lib/openWindow'
import {
APP_DOWNLOAD_PATH,
DOWNLOAD_APP_TOAST_ID,
ONBOARDING_TOAST_ID,
WASM_INIT_FAILED_TOAST_ID,
} from '@src/lib/constants'
import { isPlaywright } from '@src/lib/isPlaywright'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
maybeWriteToDisk()
.then(() => {})
.catch(() => {})
export function App() {
useQueryParamEffects()
const { project, file } = useLoaderData() as IndexLoaderData
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
// We need the ref for the outermost div so we can screenshot the app for
// the coredump.
const ref = useRef<HTMLDivElement>(null)
// Stream related refs and data
const [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const projectName = project?.name || null
const projectPath = project?.path || null
// Run LSP file open hook when navigating between projects or files
useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [onProjectOpen, projectName, projectPath, file])
useHotKeyListener()
const settings = useSettings()
const authToken = useToken()
useHotkeys('backspace', (e) => {
e.preventDefault()
})
useHotkeyWrapper(
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + PATHS.SETTINGS),
{
splitKey: '|',
}
)
useHotkeyWrapper(['mod + s'], () => {
toast.success('Your work is auto-saved in real-time')
})
useEngineConnectionSubscriptions()
useEffect(() => {
// Not too useful for regular flows but on modeling view refresh,
// fetch the token count. The regular flow is the count is initialized
// by the Projects view.
billingActor.send({ type: BillingTransition.Update, apiToken: authToken })
// Tell engineStream to wait for dependencies to start streaming.
engineStreamActor.send({ type: EngineStreamTransition.WaitForDependencies })
// When leaving the modeling scene, cut the engine stream.
return () => {
// When leaving the modeling scene, cut the engine stream.
// Stop is more serious than Pause
engineStreamActor.send({ type: EngineStreamTransition.Stop })
}
}, [])
// Show a custom toast to users if they haven't done the onboarding
// and they're on the web
useEffect(() => {
const onboardingStatus =
settings.app.onboardingStatus.current ||
settings.app.onboardingStatus.default
const needsOnboarded =
!isDesktop() &&
searchParams.size === 0 &&
needsToOnboard(location, onboardingStatus)
if (needsOnboarded) {
toast.success(
() =>
TutorialRequestToast({
onboardingStatus: settings.app.onboardingStatus.current,
navigate,
codeManager,
kclManager,
}),
{
id: ONBOARDING_TOAST_ID,
duration: Number.POSITIVE_INFINITY,
icon: null,
}
)
}
}, [settings.app.onboardingStatus])
useEffect(() => {
const needsDownloadAppToast =
!isDesktop() &&
!isPlaywright() &&
searchParams.size === 0 &&
!settings.app.dismissWebBanner.current
if (needsDownloadAppToast) {
toast.success(
() =>
DownloadAppToast({
onAccept: () => {
openWindow(`${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}`)
.then(() => {
toast.dismiss(DOWNLOAD_APP_TOAST_ID)
})
.catch(reportRejection)
},
onDismiss: () => {
toast.dismiss(DOWNLOAD_APP_TOAST_ID)
settingsActor.send({
type: 'set.app.dismissWebBanner',
data: { level: 'user', value: true },
})
},
}),
{
id: DOWNLOAD_APP_TOAST_ID,
duration: Number.POSITIVE_INFINITY,
icon: null,
}
)
}
}, [])
useEffect(() => {
const needsWasmInitFailedToast = !isDesktop() && kclManager.wasmInitFailed
if (needsWasmInitFailedToast) {
toast.success(
() =>
WasmErrToast({
onDismiss: () => {
toast.dismiss(WASM_INIT_FAILED_TOAST_ID)
},
}),
{
id: WASM_INIT_FAILED_TOAST_ID,
duration: Number.POSITIVE_INFINITY,
icon: null,
}
)
}
}, [kclManager.wasmInitFailed])
// Only create the native file menus on desktop
useEffect(() => {
if (isDesktop()) {
window.electron
.createModelingPageMenu()
.then(() => {
setNativeFileMenuCreated(true)
})
.catch(reportRejection)
}
}, [])
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader
className="transition-opacity transition-duration-75"
project={{ project, file }}
enableMenu={true}
nativeFileMenuCreated={nativeFileMenuCreated}
>
<CommandBarOpenButton />
<ShareButton />
</AppHeader>
<ModalContainer />
<ModelingSidebar />
<EngineStream pool={pool} authToken={authToken} />
{/* <CamToggle /> */}
<LowerRightControls navigate={navigate}>
<UnitsMenu />
<Gizmo />
</LowerRightControls>
</div>
)
}