Search results can link to older posts that are not present in the first
/browse page. The previous deep-link flow kept paginating the all-assets
stream until the target id appeared, leaving users stuck on the waiting
indicator for very old posts.
Fetch /api/posts/:id directly for ?post= arrivals and inject the resolved
target post at the top of the stream when it is not already in loaded
items. The normal paginated feed still loads below for context. Keep the
explicit finding/not-found status messages as a fallback for slow or
missing direct fetches.
Verified with search result c5eeb17d-3bd0-4d32-9c92-5efa6e4a015c: target
post rendered within 100ms instead of waiting for pagination. Checks:
tsc, format:check, tests, build.
Banner / Home-card deep-links were starting the smooth scroll the
moment the target post entered the DOM, before the in-view Reveal
animations on the top bubbles had time to fade in. Users perceived the
page as 'scrolling past nothing' because most bubbles were still at
opacity 0 when the viewport moved.
Track the moment first non-skeleton content appears for the current
target via firstContentAtRef, then hold the smooth-scroll start until
~300ms after that — long enough for the initial Reveal staggers to
play. Elapsed time is subtracted so cached arrivals don't pay the full
wait twice, and the ref resets per target so each navigation re-times.
Verified in the browser: with cold cache, content arrives ~480ms after
click, smooth scroll starts ~800ms (300ms settle), reaches deep target
by ~1.3s. With warm cache same pattern; users now see content before
motion begins.
Banner / Home-card clicks landing on /browse?post=X now always start the
smooth-scroll positioning from the top of the stream, instead of from
whatever scrollY the user happened to leave the page at. Runs in
useLayoutEffect before paint so the user never briefly sees the previous
position before the jump, giving a clearly visible scroll journey to the
target post every time.
Verified in the browser: before banner click scrollY=2000, immediately
after =0, then smooth-scrolled to ~25k as pagination loaded the target
post deeper in the stream.
- Replace the bare '…' loading dots at the bottom of the post stream
with a skeleton bubble that matches the initial-load placeholders, so
pagination feels like content arriving instead of a frozen indicator.
- Localize the retry control via new 'retry' / 'loadMoreFailed' keys
across all 7 locales and surface a user-friendly error string instead
of the raw exception message.
- Retry button now picks reset() vs loadMore() based on item count so a
pagination failure only refetches the next page, not the whole stream.
- When a banner deep-link can't find its target post and pagination
errors, break out of the retry loop and release the scroll lock so the
user sees the inline retry instead of an endless freeze.
Verified in the browser: zh-CN renders '加载更多资料失败,请检查网络后重试。'
with a '重试' button; banner clicks with empty / '#' / 'javascript:' /
null linkUrls render no anchor and do not navigate.
- MessageStream: drop the mount-time scroll lock and the 80ms-delayed
custom rAF; engage the lock only while the smooth animation runs and
use native scrollTo({behavior:'smooth'}) so the page never feels frozen
during pagination and the easing is buttery.
- PublicLayout: fire the default /browse prefetch immediately on mount
(banner / Home tile destination) so a fast tap hits a warm cache;
popular / latest stay deferred to idle.
- FigmaBanner: prefetch the all-scope stream on mount and on pointerdown
as safety nets, and ignore empty / '#' / javascript: link URLs so a
contentless banner renders as a non-interactive image.
- usePostStream: dedupe in-flight prefetches by key so concurrent
callers (layout + banner) collapse into a single network request.
The banners API can return hundreds of records; show at most 10 so the carousel
and its dot indicator stay on one row within the phone width, regardless of how
many exist on the backend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Banner linkUrls come back from the API as unprefixed paths
(e.g. /browse?post=123). Navigating to them directly dropped non-English
viewers into the English version of the post. Localize both the rendered
href and the SPA navigate target via stripLangPrefix + localizePath.
Initialize the categories state from the cached response on first render so the
category icons stay visible when navigating back to the home page, instead of
flashing empty for a frame.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With many banners the pagination dots overflowed the phone width. Show at most
10 dots in a window that follows the active slide; each dot still maps to its
real slide index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measure the image's rendered bottom edge with refs + ResizeObserver and
position the long-press save hint relative to it instead of pinning to
screen center or stage bottom. Enlarges the toast for mobile legibility
and clamps the offset so tall portrait images don't push it offscreen.
The app ships its own 7-language i18n and serves localized content, but mobile
browsers (Google Translate) were auto-translating the Chinese UI into broken
English (brand, nav, language dropdown). Add translate="no" + the Google
notranslate meta, and keep the attribute set on language changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revert desktop primary back to the direct injected sign (no WalletConnect relay,
which could spin forever) — reliable for a BNB-chain extension. Add console
diagnostics ([wallet-login] ...) and provider enumeration so a stuck/no-popup
flow can be pinpointed. WalletConnect stays as the explicit mobile MetaMask/
imToken option.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raw window.ethereum is unreliable when several extensions contend for it, so
desktop now opens the RainbowKit connect modal (EIP-6963 wallet discovery +
WalletConnect QR) when a project id is configured, falling back to the injected
flow otherwise. In-wallet mobile browsers keep the direct injected sign.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clear tpRequest when a TokenPocket login expires or fails so the mobile UI
returns to the initial state instead of showing a stuck waiting spinner
alongside the error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Mobile TokenPocket now opens the tpoutside:// sign deep link and returns to
the original browser to finish login (no wallet in-app browser); desktop
keeps the QR. Fixes mobile login + logout being trapped in TP's browser.
- Desktop without an injected wallet shows a clear message instead of a dead
button; TokenPocket login card is always available as a working path.
- Raise toast z-index above the login modal so feedback is visible.
- Add native TokenPocket-login strings across 7 locales.
- Document that the live backend lacks favorites + TokenPocket routes (404),
the real blocker for those features in production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove forced BNB chain switch on injected login (signature is chain-agnostic)
- Refine isMobileDevice so touch Macs stay on desktop flow
- Wire RainbowKit/WalletConnect as a real MetaMask/imToken QR fallback,
gated on a valid VITE_WALLETCONNECT_PROJECT_ID
- Rebuild login modal: single desktop primary action, collapsible other
methods, mobile open-app fallback feedback, brand icons
- Add My Favorites entry points (header, mobile menu, wallet dropdown)
- Favorites page: error retry, mobile filter drawer
- Auto sign-out and re-login prompt on favorites 401
- Full native translations for all wallet strings across 7 locales
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>