Splash only appears when the server is slow: no flash of the welcome screen on a fast start

The splash used to stay a fixed 700ms plus a fade, so a fast response still showed it
briefly and jumped away. Now only the matching background shows for the first 350ms;
the splash content appears only if the response has not arrived by then, and once shown
it stays at least 700ms. Not run on a device.
This commit is contained in:
2026-10-11 01:19:34 +03:30
parent 8a61cb25b8
commit c7407f32b9
2 changed files with 32 additions and 9 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { AnimatePresence, MotionConfig, motion } from 'motion/react'; import { AnimatePresence, MotionConfig, motion } from 'motion/react';
import { api, ApiError, http, type Grade, type UserProfile } from '@/api'; import { api, ApiError, http, type Grade, type UserProfile } from '@/api';
import type { Prefetch } from '@/state/AppData'; import type { Prefetch } from '@/state/AppData';
@@ -23,7 +23,13 @@ type Flow =
| { kind: 'setup'; profile: UserProfile; grades: Grade[] } | { kind: 'setup'; profile: UserProfile; grades: Grade[] }
| { kind: 'main'; profile: UserProfile; grades: Grade[]; prefetch?: Prefetch }; | { kind: 'main'; profile: UserProfile; grades: Grade[]; prefetch?: Prefetch };
/** کمترین زمانِ نمایشِ اسپلش، تا صفحه چشمک نزند. */ /**
* اسپلش فقط وقتی نشان داده می‌شود که سرور دیرتر از SPLASH_DELAY_MS جواب بدهد؛ اگر زودتر رسید، کاربر
* اسپلش را نمی‌بیند و مستقیم به صفحه‌ی بعدی می‌رود (به‌جای چند صد میلی‌ثانیه چشمک).
* اگر یک بار نشان داده شد، دست‌کم SPLASH_MIN_MS می‌ماند تا خودش چشمک نزند.
* تا آن لحظه فقط پس‌زمینه‌ی هم‌رنگِ اسپلش دیده می‌شود، نه صفحه‌ی سفید.
*/
const SPLASH_DELAY_MS = 350;
const SPLASH_MIN_MS = 700; const SPLASH_MIN_MS = 700;
/** /**
@@ -32,6 +38,8 @@ const SPLASH_MIN_MS = 700;
*/ */
export function App() { export function App() {
const [flow, setFlow] = useState<Flow>({ kind: 'boot' }); const [flow, setFlow] = useState<Flow>({ kind: 'boot' });
const [splashVisible, setSplashVisible] = useState(false);
const splashShownAt = useRef<number | null>(null);
/** پروفایل و پایه‌ها را می‌گیرد و تصمیم می‌گیرد کاربر کجا برود. */ /** پروفایل و پایه‌ها را می‌گیرد و تصمیم می‌گیرد کاربر کجا برود. */
const enter = useCallback(async () => { const enter = useCallback(async () => {
@@ -45,7 +53,12 @@ export function App() {
const boot = useCallback(async () => { const boot = useCallback(async () => {
setFlow({ kind: 'boot' }); setFlow({ kind: 'boot' });
const started = Date.now(); splashShownAt.current = null;
setSplashVisible(false);
const timer = setTimeout(() => {
splashShownAt.current = Date.now();
setSplashVisible(true);
}, SPLASH_DELAY_MS);
let next: Flow | null = null; let next: Flow | null = null;
if (api.auth.isSignedIn()) { if (api.auth.isSignedIn()) {
try { try {
@@ -59,8 +72,13 @@ export function App() {
} else { } else {
next = store.get(KEYS.onboarded) ? { kind: 'login' } : { kind: 'onboarding' }; next = store.get(KEYS.onboarded) ? { kind: 'login' } : { kind: 'onboarding' };
} }
const wait = SPLASH_MIN_MS - (Date.now() - started); clearTimeout(timer);
// اسپلش دیده شده؟ فقط در آن صورت تا کمینه‌ی زمانش نگه دار.
const shownAt = splashShownAt.current;
if (shownAt !== null) {
const wait = SPLASH_MIN_MS - (Date.now() - shownAt);
if (wait > 0) await new Promise((r) => setTimeout(r, wait)); if (wait > 0) await new Promise((r) => setTimeout(r, wait));
}
if (next) setFlow(next); if (next) setFlow(next);
}, [enter]); }, [enter]);
@@ -99,9 +117,10 @@ export function App() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }} // اگر اسپلش هرگز دیده نشد، محو‌شدن هم لازم نیست (وگرنه همان چشمک از راهِ دیگر برمی‌گردد).
transition={{ duration: splashVisible || flow.kind !== 'boot' ? 0.2 : 0 }}
> >
{flow.kind === 'boot' && <SplashScreen />} {flow.kind === 'boot' && <SplashScreen showContent={splashVisible} />}
{flow.kind === 'bootError' && ( {flow.kind === 'bootError' && (
<div className="flex h-full flex-col justify-center"> <div className="flex h-full flex-col justify-center">
<ErrorState error={flow.error} onRetry={() => void boot()} /> <ErrorState error={flow.error} onRetry={() => void boot()} />

View File

@@ -8,8 +8,12 @@ const GLYPHS = [
{ c: '÷', x: 64, d: 3.8 }, { c: '∑', x: 80, d: 0.6 }, { c: '×', x: 90, d: 4.6 }, { c: '÷', x: 64, d: 3.8 }, { c: '∑', x: 80, d: 0.6 }, { c: '×', x: 90, d: 4.6 },
]; ];
/** تا وقتی نشستِ قبلی بررسی می‌شود نمایش داده می‌شود؛ عمداً تأخیرِ ساختگی ندارد. */ /**
export function SplashScreen() { * تا وقتی نشستِ قبلی بررسی می‌شود نمایش داده می‌شود. `showContent=false` فقط پس‌زمینه است؛ App
* محتوا را فقط وقتی روشن می‌کند که پاسخِ سرور دیر کرده باشد.
*/
export function SplashScreen({ showContent }: { showContent: boolean }) {
if (!showContent) return <div className="h-full bg-rose" />;
return ( return (
<div className="relative flex h-full flex-col items-center justify-center overflow-hidden bg-rose"> <div className="relative flex h-full flex-col items-center justify-center overflow-hidden bg-rose">
<div aria-hidden="true" className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2"> <div aria-hidden="true" className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2">