Skip to content

Mute users and tags + reduce API traffic to avoid 429s

jedmund requested to merge jedmund/mute-users-tags into main

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/edit so mutes sync with the official app and across devices; feeds respect the server is_muted flag plus a local MutesStore fallback. 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 bookmarkDetail call (heart seeds from illust.isBookmarked), (2) a 1.5s deferral on illustRelated with cancel-on-pop, (3) a 400ms floor between consecutive nextPage fetches, (4) a RateLimitGate that short-circuits subsequent send()s after a 429 using Retry-After, and (5) per-request logging + signposts + a rolling per-endpoint counter so future throttles are trivial to diagnose.
  • Related-posts UX: new RelatedPostsSection wraps 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; confirm bookmarkDetail is no longer fired on detail open.
  • Force a 429 (rapid tag searches). Verify the ⚠︎ HTTP 429 line lands, followed by ⏸︎ rate-limit gate engaged, and subsequent requests hit ✖ blocked by rate-limit gate without the network round-trip.
  • Open a detail, swipe out before 1.5s. Telemetry should show no illustRelated call. Open one, stay past 1.5s → deferred illustRelated fires.
  • Fast-scroll the Home feed; pagination log lines include SKIP throttled elapsed=… minInterval=400ms for 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 → ContentUnavailableView with "No Related Posts Yet" copy.
  • Force a fetch failure (airplane mode after detail open) → error row with Try Again actually retries.

🤖 Generated with Claude Code

Merge request reports

Loading