vojo/src/app/pages/auth/AuthMascot.tsx

82 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { Ref, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { launchSplash } from '../../plugins/launchSplash';
import {
attachMascotVideo,
detachMascotVideo,
mascotVideoReady,
} from './mascotSingleton';
import * as css from './styles.css';
type AuthMascotProps = {
mascotRef?: Ref<HTMLDivElement>;
// True for the splash-gate variant (cold start, sync wait): mascot is
// centered both axes to mirror the Android 12+ system splash icon, so the
// native → WebView → web-splash handoff reads as one continuous splash.
// False (default) keeps the auth-page positioning (top-anchored) used by
// login / register / reset-password.
centered?: boolean;
};
export function AuthMascot({ mascotRef, centered }: AuthMascotProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const didSignalLaunchReadyRef = useRef(false);
// Singleton <video> attach/detach. Preserves currentTime across the
// ConfigConfigLoading → SpecVersionsLoader → ClientRootLoading remounts
// so the mascot loop reads as one continuous animation instead of two
// restarts mid-boot.
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return undefined;
attachMascotVideo(container, css.AuthMascotVideo);
return () => {
detachMascotVideo(container);
};
}, []);
// Native splash hand-off: only the centered (splash-gate) variant signals
// the Android system splash to drop. Auth pages (login/register) don't
// signal — they're already past the cold-boot phase by the time they
// mount. requestAnimationFrame×2 ensures the browser has actually
// committed the paint before we ask the OS to release its splash.
const signalLaunchReady = useCallback(() => {
if (!centered || didSignalLaunchReadyRef.current) return;
didSignalLaunchReadyRef.current = true;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
launchSplash.ready();
});
});
}, [centered]);
useEffect(() => {
if (!centered) return undefined;
let cancelled = false;
mascotVideoReady().then(() => {
if (!cancelled) signalLaunchReady();
});
return () => {
cancelled = true;
};
}, [centered, signalLaunchReady]);
const setRefs = useCallback(
(node: HTMLDivElement | null) => {
containerRef.current = node;
if (typeof mascotRef === 'function') mascotRef(node);
else if (mascotRef && typeof mascotRef === 'object') {
(mascotRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
},
[mascotRef]
);
return (
<div
className={centered ? css.AuthMascotCentered : css.AuthMascot}
aria-hidden="true"
ref={setRefs}
/>
);
}