mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
92 lines
4.2 KiB
TypeScript
92 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
// `/`, the route's client half: the web app for a signed-in reader, the landing
|
|
// for everyone else. `/dev` is the same door with its own two answers
|
|
// (app/dev/_door.tsx): the builder for a member, Dev's page for a stranger.
|
|
//
|
|
// The export holds the landing, because a crawler and a stranger are the ones
|
|
// who read the file. The app is drawn in the browser, and only once the browser
|
|
// has said who is here:
|
|
//
|
|
// before hydration the landing — what the file holds, so nothing mismatches.
|
|
// A live token already marked <html> with `APP` and took
|
|
// the landing off the page before it was parsed
|
|
// (session.ts `BOOT`, the landing's first child).
|
|
// IAM still settling a live token is signed in: the SDK's own fast path
|
|
// answers the same way, so the app is not held for it.
|
|
// IAM settled its answer. A token that refreshed is signed in; one that
|
|
// could not is signed out.
|
|
//
|
|
// SIGNED OUT, `/` IS THE LANDING, drawn at once: it never leaves for hanzo.id
|
|
// to ask whether anyone is signed in there. Doing so held every new visitor's
|
|
// first paint for a round trip to another site. A person signed in at hanzo.id
|
|
// is found by the sign-in they press (/login asks hanzo.id first). A place in
|
|
// the app is a path under /chat or /dev, whose own page signs a stranger in to
|
|
// it; `/?chat=<id>`, the address a conversation had before, moves to its path
|
|
// before anything is drawn (app/page.tsx `MOVED`), so `/` itself holds none.
|
|
// /dev's door names its own (`placed`), and a stranger at one of them is sent
|
|
// to the sign-in and brought back to it (destination.ts `enter`).
|
|
//
|
|
// The marks follow the answer before paint, both ways, and leave with the page.
|
|
|
|
import { useEffect, useLayoutEffect, useRef, useState, type ComponentRef, type ReactNode } from 'react'
|
|
import dynamic from 'next/dynamic'
|
|
import { useIam } from '@hanzo/iam/react'
|
|
import { View } from '@hanzo/ui'
|
|
import { useHydrated } from '@hanzo/app/lib/hydrated'
|
|
import { enter } from '@hanzo/app/lib/destination'
|
|
import { APP, live } from '@hanzo/app/lib/session'
|
|
|
|
/** The app, fetched only for a signed-in reader (./_web). Until it lands the
|
|
* page is the ground under the `APP` mark: no landing, and nothing else. */
|
|
const App = dynamic(() => import('./_web'), { ssr: false })
|
|
|
|
/** `/` is no place in the app: its old addresses move to their paths first (app/page.tsx `MOVED`). */
|
|
const nowhere = (): boolean => false
|
|
|
|
export function Apex({
|
|
children,
|
|
placed = nowhere,
|
|
app,
|
|
}: {
|
|
children: ReactNode
|
|
/** Whether this address is a place in the app, which a stranger signs in to reach. `/` names none. */
|
|
placed?: () => boolean
|
|
/** What a signed-in reader is drawn. `/`'s is the app in Chat. */
|
|
app?: ReactNode
|
|
}) {
|
|
const hydrated = useHydrated()
|
|
const { isLoading, isAuthenticated } = useIam()
|
|
const [away] = useState(placed)
|
|
const gone = useRef(false)
|
|
const landing = useRef<ComponentRef<typeof View>>(null)
|
|
const signed = hydrated && (isLoading ? live() : isAuthenticated)
|
|
const isDesktop = typeof window !== 'undefined' && Boolean((window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI__ || (window as unknown as { __TAURI__?: unknown; __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__)
|
|
const leaving = hydrated && !signed && (away || isDesktop)
|
|
|
|
useLayoutEffect(() => {
|
|
if (!hydrated) return
|
|
const html = document.documentElement
|
|
html.toggleAttribute(APP, signed || leaving)
|
|
// A landing still drawn is one the session says to show, whatever BOOT
|
|
// guessed before it was parsed: BOOT hides it inline, and clearing that
|
|
// hands the box back to its own `display="contents"`.
|
|
if (landing.current instanceof HTMLElement) landing.current.style.display = ''
|
|
return () => html.removeAttribute(APP)
|
|
}, [hydrated, signed, leaving])
|
|
|
|
useEffect(() => {
|
|
if (!leaving || isLoading || gone.current) return
|
|
gone.current = true
|
|
enter()
|
|
}, [leaving, isLoading])
|
|
|
|
if (signed) return app ?? <App />
|
|
if (leaving) return null
|
|
return (
|
|
<View ref={landing} data-landing="" display="contents">
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|