Partner logos as bare tappable images, and loading skeletons for Docs/Videos - #6
Open
simonhamp wants to merge 9 commits into
Open
Partner logos as bare tappable images, and loading skeletons for Docs/Videos#6simonhamp wants to merge 9 commits into
simonhamp wants to merge 9 commits into
Conversation
The partner cards laid out badly on iOS and two of the three had no logo at all, falling back to a lettered monogram, because the native <image> renderer can't draw SVG and only Synergi published a raster. Rasterise all three from the official SVGs (headless Chrome — ImageMagick's SVG delegate is unreliable here) into transparent-background PNGs with a light and dark variant each, bundled under public/img/partners. Nexcalia and Web Mavens come from the inline SVGs on nativephp.com/consulting; Synergi is regenerated from the SVGs already in the repo, since the previous PNGs had a baked-in white background. Logos are picked server-side via isDark(), so an appearance flip needs a re-render — hence the AppearanceChanged listener. Local absolute paths work on both platforms (UIImage(contentsOfFile:) on iOS, Coil on Android), which is what let the bundled rasters replace the remote-only URLs. Drop the cards, taglines and chrome entirely: at logo size the text was noise, so the section is now a centered column of bare tappable logos, each sized to its own aspect ratio. That also removes the iOS layout issue along with the layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both screens block in mount() on a network fetch, so a fresh push showed nothing until it returned. Mark them #[Lazy] and override placeholder() with a skeleton that mirrors the real layout (Videos: featured thumb, section header, list rows; Docs: the TOC with its first section expanded), so the frame doesn't shift when content lands. Bones are theme tokens, so they work in both appearances; they're static because the renderer has no pulse/shimmer support. Only show a skeleton when there's actually something to wait for: - Videos uses Cache::remember, so a warm cache returns within the frame — publishPlaceholder() bails out on Cache::has() and paints the real feed. - DocsIndex::sections() is deliberately network-FIRST, so a warm cache did NOT make mount fast and skipping the skeleton on "cache exists" alone would have left the previous screen on display. DocsIndex::fresh() now returns the cached corpus only inside a short window, tracked by a separate marker key so the corpus itself still survives its full 24h as the offline fallback. mount() and publishPlaceholder() branch on the same condition, so they can't disagree. The window is deliberately short: past it sections() refetches (and the skeleton covers it), so a docs update lands without waiting out the 24h fallback TTL. Pull-to-refresh still forces a fetch anytime. Also removes the $loading prop and its branches from both screens — mount() always ran to completion before the first render, so those "Loading…" states were dead code that #[Lazy] now genuinely fills. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up the merged stale-tab-render work (c53cae8 => 1b61319). The #[Lazy] / placeholder() API the skeletons rely on is unchanged in main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Video thumbnails used fit="1" (.fit / ContentScale.Fit), which letterboxes: YouTube's hqdefault.jpg is 4:3, the containers are ~16:9, so each image scaled to fit the height and left a gap down the trailing edge. fit="2" (.fill / Crop) fills the frame instead, and cropping a 4:3 hqdefault to 16:9 removes exactly the black bars YouTube bakes into it. Both the featured card and the list rows were affected. Home's root column carried no background — only the scroll-view inside it did. The tree reaches the physical screen edge, but the scroll-view's content is inset by the top safe area, so the status-bar strip fell through to the host's Color(.systemBackground) (white) and showed as a seam against our off-white theme background. Every other screen already paints its root; Home was the outlier. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exiting a remote app cleared the shell's tree outright, so the client UI blinked out a frame or more before Jump's home republished — a hard cut with a flash of empty background in between. The publish path already animates a swap when `navigationPending` is set: it reclassifies the previous tree as the outgoing layer (iOS) or bumps the screenKey that drives AnimatedContent (Android). The exit couldn't use any of that because it destroyed the very frame the animation needs — iOS only stages an outgoing screen `if wasActive, let oldTree = bridge.currentTree`. So endSession() gains `keepingLastFrame`: it still suppresses the session and stops routing events remotely, but leaves the last frame on screen. exitToJump then stages `slide_from_left` before waking home, and home's republish is consumed as a navigation — the remote app slides out to the trailing edge as home enters from the leading one, matching the 3-finger swipe-RIGHT that triggered it. Both platforms accept the same transition names (the "slide_forward / slide_back" list in Android's NativeUIBridge is stale). A remote app whose root sentinel matches home's (tabs → tabs) counts as a native-chrome continuation and skips the swap, degrading to today's instant repaint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Font registration is process-wide on both platforms, so fonts a remote app pushed for its session would otherwise keep answering to tokens Jump itself uses — its own Inter/Audiowide could resolve to the dev app's files after the swipe back. Clear on exit; __jumpResume re-pushes Jump's theme right after, so the bundled faces resolve again on the next lookup. Needs the matching mobile-ui change (runtime font registry). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The relay called NativeUIFontResolver.clearRuntimeFonts() directly, which
hard-couples this plugin to a native-ui version that has a runtime font
store. Jump builds against whatever native-ui it bundles — currently one
without it — so the iOS build failed outright:
type 'NativeUIFontResolver' has no member 'clearRuntimeFonts'
Look the function up by name in the bridge registry instead. Present → the
session's pushed fonts are cleared; absent → no runtime font store exists,
so nothing was ever pushed and there's nothing to clean up. Compiles against
either version, on both platforms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Between tapping a server and the remote app's first frame, Jump's own UI sat there for a second or two — and not quietly. The remote app pushes its theme as it boots, and its font aliases name files Jump doesn't bundle, so home visibly re-rendered in fallback fonts before being replaced. It read as Jump breaking, not as an app loading. A full-screen <modal> now covers that window (iOS presents it as a fullScreenCover). Every colour in it is a literal and it uses no `font=` tokens, so the incoming theme can't shift the cover itself — the point is lost if the loading screen flickers too. Raised before the Discovery::connect() bridge call, which returns immediately, so the cover renders first and the remote tree replaces it whenever it's ready. Cleared on __jumpResume, which covers both a clean 3-finger exit and a connection that died or never produced a tree (the relay routes an abandoned reconnect through the same path). The layout's overlay guard now accounts for it too: the server can drop out of the store mid-connect, which would empty the store and take the cover down with the overlay — exposing Jump's UI for exactly the window it exists to hide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each Jump session stacked another "servers nearby" pill, and the dismissed bottom sheet appeared to reopen. Both come from Element.Init calling registerPluginRenderers() on every session. That registration is only half idempotent: element renderers land in a dictionary keyed by type, but plugin ROOT HOSTS — the floating overlay, the drawer — append to NativeRootHostRegistry, and its wrap() folds EVERY registered entry around the content. So after one Jump the overlay was wrapped twice, after two Jumps three times: N pills, and N copies of the bottom sheets living inside that overlay. Dismissing "the" sheet dismissed the top copy and left an identical one presented behind it, which read as close → reopen → close. Guard the call so it runs once per process. Nothing clears either registry, so re-registering was never buying anything. The real defect is upstream: NativeRootHostRegistry.register() takes a `name` that identifies the host and then appends without deduping on it. Worth fixing there too, so any host calling this more than once is safe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two related polish passes on the Home, Docs and Videos tabs.
Agency partners: bare tappable logos
The partner cards laid out badly on iOS, and two of the three had no logo at all — they fell back to a lettered monogram because the native
<image>renderer can't draw SVG and only Synergi published a raster.public/img/partners. Nexcalia and Web Mavens come from the inline SVGs on nativephp.com/consulting; Synergi is regenerated from the SVGs already in the repo, whose previous PNGs had a baked-in white background.isDark(), so an appearance flip needs a re-render — hence the newAppearanceChangedlistener onHome.Bundled local paths work on both platforms (
UIImage(contentsOfFile:)on iOS, Coil on Android), which is what let the rasters replace the remote-only URLs.Docs / Videos: loading skeletons, only when needed
Both screens block in
mount()on a network fetch, so a fresh push showed nothing until it returned. Both are now#[Lazy]with aplaceholder()skeleton mirroring the real layout — Videos: featured thumb, section header, list rows; Docs: the TOC with its first section expanded — so the frame doesn't shift when content lands. Bones use theme tokens (works in both appearances) and are static, since the renderer has no pulse/shimmer support.A skeleton only appears when there's actually something to wait for:
Cache::remember, so a warm cache returns within the frame —publishPlaceholder()bails onCache::has()and paints the real feed.DocsIndex::sections()is deliberately network-first, so a warm cache did not make mount fast, and skipping the skeleton on "cache exists" alone would have left the previous screen on display.DocsIndex::fresh()now returns the cached corpus only inside a short window, tracked by a separate marker key so the corpus itself still survives its full 24h as the offline fallback.mount()andpublishPlaceholder()branch on the same condition, so they can't disagree.The window is deliberately short: past it,
sections()refetches (and the skeleton covers it), so a docs update lands without waiting out the 24h fallback TTL. Pull-to-refresh still forces a fetch anytime.FRESH_MINUTESis the single knob if you'd rather trade freshness for never seeing the skeleton.Also removes the
$loadingprop and its branches from both screens —mount()always ran to completion before the first render, so those "Loading…" states were dead code that#[Lazy]now genuinely fills.Also
Bumps
nativephp/mobileto latestdev-main(c53cae8→1b61319) to pick up the merged stale-tab-render work. The#[Lazy]/placeholder()API the skeletons rely on is unchanged there.Testing
php artisan view:cachecompiles all views, including the two new skeletons.php artisan test: 24 passed, 161 assertions. The one failure (ExampleTest, "No application encryption key") is pre-existing — this checkout has an empty.env— and unrelated.app/Providers/NativeServiceProvider.phpandapp/NativeComponents/Concerns/InteractsWithDiscovery.phpfail Pint, but both already do so onmain.)Worth a visual check on both platforms that the logo display sizes feel balanced, and that the skeletons line up with the loaded content.
🤖 Generated with Claude Code