Mute users and tags + reduce API traffic to avoid 429s
Created by: jedmund
Summary
-
Mutes (server-backed): overflow menu next to the follow pill on profiles and next to the (now labeled) filter pill on tag feeds, with Yes/No confirmation. Settings grows a Content section with a Fully hide / Obscured behavior picker and drill-ins into server-backed Muted Users and Muted Tags lists (swipe to unmute, pull to refresh). Backed by Pixiv's
/v1/mute/list+/v1/mute/editso mutes sync with the official app and across devices; feeds respect the serveris_mutedflag plus a localMutesStorefallback. Free-account quota (1-item cap) surfaces a dedicated Premium-upsell alert. -
API traffic reduction: an Instruments capture showed 185 requests in the 60s before a 429, mostly detail-open fan-out and scroll-driven prefetch bursts. This PR cuts that down with (1) removal of the eager
bookmarkDetailcall (heart seeds fromillust.isBookmarked), (2) a 1.5s deferral onillustRelatedwith cancel-on-pop, (3) a 400ms floor between consecutivenextPagefetches, (4) aRateLimitGatethat short-circuits subsequentsend()s after a 429 usingRetry-After, and (5) per-request logging + signposts + a rolling per-endpoint counter so future throttles are trivial to diagnose. -
Related-posts UX: new
RelatedPostsSectionwraps the nested grid with a masonry skeleton during the deferred load, a "No Related Posts Yet" empty state (common for just-uploaded illusts Pixiv hasn't generated suggestions for), and a retry row on failure.
Test plan
Mutes
-
Open a pushed profile → (…)→ Mute → alert → Yes → back to Home → their posts gone. -
Mute a second user from another device / official app → pull-to-refresh the Muted Users list here → they appear (server is authoritative). -
Switch accounts in Settings → confirm the other account's mute list loads independently. -
On a free (non-Premium) account already at the 1-item cap, attempt a second mute → Premium-upsell alert fires, optimistic state rolls back. -
Tag feed → filter button reads "Popular"/"Recent" next to its glyph → overflow → Mute → confirm. -
Flip Settings → Mute behavior between Fully hide and Obscured; confirm feeds switch between dropping muted posts and rendering them blurred with a "Muted — tap to reveal" pill. Tap the pill → reveals in place. -
Swipe-to-unmute from Muted Users / Muted Tags → content returns on next load-more / refresh. -
Cold launch → mute lists load from the server, behavior preference persists.
API traffic
-
Scroll Home + open several detail views rapidly. In Console, stream log stream --predicate 'subsystem == "com.attractor.pixillate" && category == "api"'. Confirm you see→and←lines with per-endpoint counts; confirmbookmarkDetailis no longer fired on detail open. -
Force a 429 (rapid tag searches). Verify the ⚠︎ HTTP 429line lands, followed by⏸︎ rate-limit gate engaged, and subsequent requests hit✖ blocked by rate-limit gatewithout the network round-trip. -
Open a detail, swipe out before 1.5s. Telemetry should show no illustRelatedcall. Open one, stay past 1.5s → deferredillustRelatedfires. -
Fast-scroll the Home feed; pagination log lines include SKIP throttled elapsed=… minInterval=400msfor rapid bursts. -
Profile run (Cmd+I → os_signpost, subsystem com.attractor.pixillate) → each request appears as an interval on the timeline tagged with the endpoint name + status.
Related-posts UX
-
Open a detail, scroll to the related grid within ~1s → skeleton renders until fetch lands. -
Open a freshly-posted illust with no suggestions yet → ContentUnavailableViewwith "No Related Posts Yet" copy. -
Force a fetch failure (airplane mode after detail open) → error row with Try Again actually retries.