# Splash & mascot — open notes
State of play after the connection-indicator (`SyncIndicator`) work landed.
Anything in this file is **deferred / future work**, not active. Treat it as
context for the next person to pick up the splash topic.
## Current visible chain (cold load, web/desktop)
1. **Initial blank** — HTML loaded, React mounts. ~50ms. `body` background
is `#0d0e11` (the `--vojo-safe-area-bg` var), so it's a dark frame, not
white.
2. **`ConfigConfigLoading` mascot splash** —
[`pages/ConfigConfig.tsx`](../../src/app/pages/ConfigConfig.tsx) renders
`` (mascot + footer) while `/config.json` is fetched.
~50–300ms on Caddy-served static.
3. **`SpecVersions` mascot splash** —
[`pages/client/SpecVersions.tsx`](../../src/app/pages/client/SpecVersions.tsx)
renders `` while `/_matrix/client/versions` is fetched
from the homeserver. **0.5–2s, network-bound, the longest of the chain.**
4. **`ClientRoot` mascot splash** —
[`pages/client/ClientRoot.tsx`](../../src/app/pages/client/ClientRoot.tsx)
`loading || !mx ? : ...`. Shown while `initClient`
runs (IndexedDB open + crypto WASM init, ~300–700ms) AND while waiting
for `SyncState.Prepared` (the `useSyncState` gate at line ~200). The
Prepared wait can be 1–30s depending on cache warmth and account size.
5. App tree mounts. `SyncIndicator` takes over for the residual sync
activity (green slide while sync is still settling, hidden once Syncing).
## Current visible chain (Android native)
1. **OS system splash** — Android 12+ enforces a system splash with the
launcher icon centered over `windowBackground`. Configured via
[`android/app/src/main/res/values/styles.xml`](../../android/app/src/main/res/values/styles.xml)
`AppTheme.NoActionBarLaunch` → `windowBackground=@android:color/black`.
Cannot be fully eliminated; ~300–500ms while Android starts the process
and Capacitor loads the WebView.
2. Then the web chain (steps 1–5 above).
So an Android cold-launch sees: black-icon (OS) → black blank (HTML) →
mascot (config + spec-versions + initClient + Prepared) → app.
## What we tried and reverted (during the SyncIndicator work)
In the 0.3.0 attempt that got reverted, we made several aggressive changes
to shorten the chain. They worked, but introduced subtle bugs that the user
chose to roll back to keep the diff clean. Specifically:
### A. SpecVersions made non-blocking
[Commit reverted in `ff01e6c`.] The change made `SpecVersions` render
children immediately with `versions: []` and hydrate the real value in the
background (with retry on `online` event). This eliminated the 0.5–2s
mascot in step 3.
**Why reverted**: it surfaced a latent bug in
[`components/message/content/ImageContent.tsx`](../../src/app/components/message/content/ImageContent.tsx)
and `VideoContent.tsx`. While `versions: []`, `useMediaAuthentication()`
returns false, autoPlay images load with unauth URLs, the server rejects,
the local `error=true` flag latches and is never cleared on the post-
hydration retry. The reviewer flagged this as a blocker.
**Fix attempted**: reset `error` and `load` state in the `loadSrc`-deps
useEffect AND in `handleLoad` — both `ImageContent.tsx` and
`VideoContent.tsx`. The fix was correct but the user judged it as out-of-
scope scope-creep for the connection-indicator feature and rolled the
whole change back.
**To redo cleanly**: ship SpecVersions-non-blocking and the
ImageContent/VideoContent fix together, in a separate dedicated commit
(scope = "make boot non-blocking", not bundled with the indicator).
### B. ClientRoot loading paths replaced with `null`
[`pages/ConfigConfig.tsx::ConfigConfigLoading`](../../src/app/pages/ConfigConfig.tsx)
returned `null` instead of ``. Same for the `!mx` branch
in `ClientRoot`. Eliminated the mascot during steps 2 and 4-init-only.
Reverted as part of the same revert.
**To redo**: low risk on its own. Change those two callsites to render
`null` (the dark body background shows through). Skip the Prepared gate
removal (that's the deeper change).
### C. ClientRoot Prepared gate removed
The ambitious move: render `MatrixClientProvider` as soon as `mx` is created
(after `initClient`), not after `Prepared`. Eliminates the 1–30s wait for
first sync — app shell renders against the IndexedDB cache while sync
populates new events in background.
**Why reverted**: needed an extra fix in
[`pages/client/direct/RoomProvider.tsx`](../../src/app/pages/client/direct/RoomProvider.tsx)
to add `useAtomValue(allRoomsAtom)` so cold-start deep-links (push tap)
re-render when the room arrives. Plus arguably exposes a brief empty-UI
flash on first-time logins.
**To redo**: do it after a careful audit of every consumer of
`mx.getRoom()` / `mx.getRooms()` to make sure they all tolerate empty
stores. The deep-link re-render fix in `direct/RoomProvider.tsx` is the
mechanical trigger; there may be others.
## What's open
In rough priority order:
1. **SpecVersions non-blocking + media-auth race fix** (B above).
Biggest win for cold-start time-to-interactive (~1.5s saved). Bundle
with the ImageContent/VideoContent error reset in one commit.
2. **`null` instead of mascot during ConfigConfig + `!mx` loading**
(B above, simpler half). ~400ms shaved.
3. **Drop the Prepared gate** (C above). Most invasive; needs full audit.
Could wait until `dm_1x1_redesign.md` or another major-mode change is
ready to absorb the risk.
4. **OS system splash on Android** — currently iconic-on-black via
`AppTheme.NoActionBarLaunch`. Can't eliminate on Android 12+ but can
be tuned (different background color, different icon) via
`android/app/src/main/res/values/styles.xml`. Not user-reported as a
problem, just noting the lever exists.
## Don't break
- **`body { background: var(--vojo-safe-area-bg, #0d0e11) }`** in
`src/index.css` — the dark frame is what makes a `null` loading path
look intentional. Without it the user sees a white flash.
- **Mascot on auth pages** (`/login`, `/register`) is INTENDED — those
pages design the mascot in deliberately. Don't gate-remove it there.
Only the loading-state mascot is the candidate for removal.
- **The error-retry mascot in `ClientRoot`** (the
`{retry-dialog}` branch in
`ClientRoot.tsx`) is the rare case where the mascot is fine to keep —
it's a hard-error context that needs anchoring for the dialog.