Fix following past 48 entries and make the e621 page size configurable

Follow lists were paginated at the API default of 48, but the SPA treats
them as complete sets: the tag/pool toggles read their state from page one
(so the 49th follow looked unfollowed and its spinner waited for a page that
could never contain it) and the Followed page rendered only 48 cards while
showing that as the count. Both follow endpoints are now unpaginated — they
are per-user sets and still restricted to the caller's rows — and the three
consumers take plain arrays.

Post visibility: the old J621-Django online view fetched limit=320 (e621's
maximum) while ours hard-coded 48, and fetchPostsByIds capped id batches at
100. The Online browser now has a 'Posts per page' setting (48/100/200/320)
in its sidebar, mirrored in Account -> Browsing preferences, stored per user
as e621_per_page and also used for pool loading; the id-batch cap is raised
to 320.

Tests: follow list shape/isolation (4) and preference validation/merge (3)
added; the full backend suite is 46 green. Live-checked the array response
shape and the preference bounds (200 accepted, 500 rejected).
This commit is contained in:
2026-09-18 19:06:18 -05:00
parent d16a77907a
commit 1adb761c8d
16 changed files with 237 additions and 25 deletions
@@ -9,6 +9,8 @@ import type { UserPreferences } from "@/lib/types";
import { useAuth } from "@/store/auth";
import { toast } from "@/store/toasts";
const E621_PER_PAGE_OPTIONS = [48, 100, 200, 320];
const LANDING_OPTIONS: { value: NonNullable<UserPreferences["landing_page"]>; label: string }[] = [
{ value: "library", label: "Library" },
{ value: "online", label: "Online" },
@@ -26,6 +28,7 @@ export function PreferencesCard() {
per_page: user?.preferences?.per_page ?? 48,
zoom: user?.preferences?.zoom ?? 190,
online_hot_default: user?.preferences?.online_hot_default ?? true,
e621_per_page: user?.preferences?.e621_per_page ?? 48,
});
const [saving, setSaving] = useState(false);
@@ -149,8 +152,32 @@ export function PreferencesCard() {
))}
</select>
</Field>
<Field label="e621 posts per page">
<select
className={inputClass}
value={values.e621_per_page ?? 48}
onChange={(event) =>
setValues((current) => ({
...current,
e621_per_page: Number(event.target.value),
}))
}
>
{E621_PER_PAGE_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</Field>
</div>
<p className="text-[11px] leading-relaxed text-ctp-overlay0">
e621 posts per page drives the Online browser and how many pool posts
load per request; e621 caps a request at 320.
</p>
<label className="flex flex-col gap-1.5">
<span className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
Thumbnail size