feat(ui): skeleton→content crossfade helper + app-wide adoption#229
Open
Kelbie wants to merge 12 commits into
Open
feat(ui): skeleton→content crossfade helper + app-wide adoption#229Kelbie wants to merge 12 commits into
Kelbie wants to merge 12 commits into
Conversation
Add SkeletonContentCrossfade, the canonical region-level skeleton→content transition. While loading, a sizeable region shows the shared SkeletonLoadingShimmer wave; on the loading→loaded edge the skeleton fades out over the real content (220ms) so images fade in independently via expo-image and never block the swap. Leaf primitives stay dumb; reduced motion does an instant swap; exit="none" keeps FlashList-recycled cells safe. Adopt at Add Mints (results region), Mint info (stats + rating), Mint reviews (footer skeletons), Receive (QR hub), payment QR, referenced-post preview, history-refresh row, and mention search. Fix MintIcon, which was missing its expo-image transition. Add a Design System demo (/(settings-flow)/design-system-skeleton-crossfade). Thread + profile keep using the shared SkeletonLoadingShimmer directly: thread items are FlashList-recycled (an exiting skeleton renders in the wrong recycled slot) and the profile stats grid has a tuned 3-branch layout.
Address device feedback on the skeleton→content transition: - Make the fade a clear two-step crossfade — skeleton fades OUT, then content fades IN (300ms total) — instead of dissolving the skeleton over already- visible content. One centralized behavior, so every adopter (and the Design System demo) animates identically. - Default the region wave's highlight to the theme `surface` tint (matching the thread shimmer); the previous background-tinted fallback was nearly invisible, which read as "no wave" on Add Mints.
The shimmer wave must use the theme background, not surface: a background- colored band sweeping over the skeleton momentarily erases it, giving the illusion of the skeleton disappearing and reappearing rather than a brighter stripe on top. My previous surface default broke this (visible in the Design System). Hard-code the highlight to `background` and drop the override so it can't drift again.
The wave must paint the color revealed when the skeleton is "erased" — i.e. whatever is *behind* it — so it reads as the skeleton disappearing/reappearing. That's the screen background for full-screen lists, but the card/surface color when the skeleton is on one. Add a `surfaceColor` prop (defaults to background) and set it where skeletons sit on a surface: the Design System cards and MintInfo stats (surface-secondary), and the referenced-post preview (surface). Fixes the wrong-colored wave in the Design System.
…first-frame buildNostrDataLayer() now memoises ONE facade (and one entity cache) per active profile + tier config, so the cache accumulates across every caller instead of being rebuilt empty per request. An identity switch clears the prior cache before serving the new profile (no cross-account bleed). useThread paints its first frame from the shared cache (layer.readThread) for deep links and reply-taps that carry no nav snapshot, ordered after the richer feed nav seed so existing behaviour is unchanged.
The for-you and notifications facade paths already ingest, but the nagg GraphQL fast-paths (thread, user feed, posts-by-pubkey, and the non-mapped feed fallback) returned app-shaped results without populating the cache — so the cache was not authoritative for those surfaces. Write their notes/profiles/ metrics back into the singleton cache, tagged 'nagg', so opening a reply as its own thread or revisiting a profile serves instantly from cache.
useThread now seeds from the shared cache first (readThread: ancestors + reply previews + profiles/metrics), falling back to the transient nav snapshot only for anything not yet ingested, then own-content for a just-posted note. The cache is authoritative; the nav seed is a safety net pending on-device proof before its removal.
PostCard now resolves the author's pfp/name from the authoritative singleton cache via useProfile (per-key subscription) instead of a transient prop Map. A row re-renders in place when its author's profile arrives — so a reply's parent fills the instant it loads, relay-mode threads show authors the feed already saw, and nothing flashes a stale fallback. A loading status drives a real skeleton vs the npub fallback. Also seeds the viewer's own pfp/name from profileStore into the cache at build/switch, so notifications (whose targets are the viewer's own posts) render own identity with zero fetch. Adds shared/lib/nostr/useEntityCache.ts (useProfile/useNote/useNoteStats).
Opening a thread, the cache-seeded reply previews were replaced wholesale by the server's ranked order on the initial fetch, causing a visible reshuffle. Preserve the already-painted order as a stable prefix and append the ranked delta instead (reusing the load-more merge); with no seed, the server order is used as-is.
Put the new ResolvedThread.parents into allEvents (not the reply list) so buildThreadStructure renders the parent chain in relay/primal mode, matching nagg. With the parents now ingested into the cache, tapping a reply serves its parent from cache instead of flashing it in from the network.
Add three deterministic analysis features to the log-doctor dev CLI, all
confined to codereview/log-doctor (no production logger changes). Pure logic
lives in a new analysis.ts module so it is unit-testable without loading the
CLI/WDA stack.
- errors: cluster near-identical entries into exemplar + count + first/last,
collapsing the biggest token consumer. --all/--no-cluster restores the full
per-entry listing with context.
- perf: complete the existing p95 with p50/p99 and a per-event latency
sparkline (over params.ms / _perf ops). span-end duration_ms remains a
separate, documented gap rather than being silently conflated.
- redaction: new read-side audit mode (+ a stats line, + budget row) that
counts {_kind} brands and <REDACTED:..> markers and heuristically flags raw
values that look un-redacted, tiered high/low signal. Reports event names,
never values; respects the deliberate non-reversible {_kind,len} design.
Also reuse a shared percentile() in stats gaps, fix the stale MODES header
list, and document the new mode/flag in OVERVIEW.md + README.md.
The redaction pipeline already branded whole 64-hex (hex32), nsec, jwt, pem
and cashu tokens, but several private-key encodings still reached the log in
the clear because maxStringLength is 120:
- WIF private keys (base58, 51-52 chars) were logged in full as long_string.
- Extended private keys (xprv/yprv/zprv + testnet) are ~111 chars (<=120) and
were logged in full (or previewed via the base64 long pattern).
- base64-encoded 32-byte secrets (44 chars) were shown in full.
- The hex / base64 long patterns previewed their first 32 chars — enough to
leak half a key — even for opaque blobs that could be key material.
Hardening (value-based, field-name rules unchanged):
- New secret patterns: wif, xprv-family, base64_key (32/64-byte base64).
- hex / base64 / base58 long patterns are now no-preview: emit {_kind,len}
only, never bytes — even when short enough to otherwise print in full.
- Embedded xprv inside larger strings is scrubbed to <REDACTED:xprv>.
Deliberately conservative on the privkey/pubkey hex ambiguity (already
handled by hex32) and does NOT touch npub (public identity stays readable).
Embedded raw 64-hex is left alone so public image-hash URLs remain debuggable.
Covered by __tests__/loggerRedaction.test.ts (8 cases, incl. over-redaction
regressions).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
SkeletonContentCrossfade(shared/ui/composed/SkeletonContentCrossfade.tsx) — the canonical region-level skeleton→content transition — and adopts it across the app's existing skeleton sites.Behavior:
SkeletonLoadingShimmerwave (one sweep per region; compact/inline swaps usewave="none"and keep the cheap pulse).expo-image'stransitionand never block (or double-fade with) the swap.Text loading,Skeleton,Avatar,MintIcon) — the fade lives at the region seam, which keeps recycling safe and primitives cheap.exit="none"for FlashList-recycled cells (skeleton unmounts; real item owns its entrance).Adoption
Add Mints (results region), Mint info (stats + rating), Mint reviews (footer skeletons), Receive (QR hub), payment QR, referenced-post preview, history-refresh row, mention search.
MintIcongained its missingexpo-imagetransition. New Design System demo at/(settings-flow)/design-system-skeleton-crossfade.Deliberately left on the shared wave
Thread (
PostCardSkeleton/ThreadView) and profile (UserProfileScreen) keep usingSkeletonLoadingShimmerdirectly. Thread items are FlashList-recycled — wrapping them reintroduces the wrong-recycled-slot exit glitch thatThreadView.tsx:400-403already guards against; the profile stats grid has a tuned 3-branch layout + content-shift telemetry not worth a regression-prone rewrite. Both already run the shared wave + a FlashList-safeFadeIn, so they're adopted at the wave level.Tests / gates
__tests__/skeletonContentCrossfade.test.tsx(8 tests): phase machine, wave gating, reduced-motion,exit="none", rapid-toggle (no stacked overlay), mount-already-loaded (no spurious fade).tscclean,eslintclean (pre-existing react-perf warnings only),knipno new unused exports.naggFeedClient*suite failures are unrelated to this change (fail identically on the base branch).Stacked on #228. Skill docs (
sovran-ui/references/thread-skeletons.md,ui-patterns.md) updated in the workspace.🤖 Generated with Claude Code