From df013731be7a4e1d04ca6dcf0371dc6aaf571495 Mon Sep 17 00:00:00 2001 From: qdust41 Date: Sun, 12 Apr 2026 20:00:59 -0400 Subject: [PATCH] feat: add user list pagination UserList now uses useInfiniteQuery with offset pagination (20 per page) and an IntersectionObserver scroll sentinel for infinite scroll. Users sorted by username. Follows same pattern as Feed/UserFeed. Co-Authored-By: Claude Sonnet 4.6 --- assets/js/components/users.tsx | 45 +++++++++++++++++++++++++++++----- assets/js/constants.ts | 1 + fix_plan.md | 3 +-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/assets/js/components/users.tsx b/assets/js/components/users.tsx index ea2228c..41f57c1 100644 --- a/assets/js/components/users.tsx +++ b/assets/js/components/users.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useContext } from "react"; import { useQuery, useInfiniteQuery } from "@tanstack/react-query"; import { readUser, readTweet, buildCSRFHeaders } from "../ash_rpc"; import { AuthCtx } from "../context"; -import { FEED_PAGE_SIZE } from "../constants"; +import { FEED_PAGE_SIZE, USERS_PAGE_SIZE } from "../constants"; import { userDisplayLabel } from "../utils"; import { useFollowUser } from "../hooks"; import { Spinner, ErrorBanner, Avatar, ContextMenu } from "./ui"; @@ -83,23 +83,54 @@ export function UserCard({ user }: { user: User }) { } export function UserList() { - const { data, isLoading, isError, error } = useQuery({ + const sentinelRef = useRef(null); + + const { + data, + isLoading, + isError, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ queryKey: ["users"], - queryFn: async () => { + queryFn: async ({ pageParam }: { pageParam: number }) => { const res = await readUser({ fields: ["id", "email", "username", "displayName", "avatarUrl", "followerCount", "followingCount", "amIFollowing"], + sort: "username", + page: { limit: USERS_PAGE_SIZE, offset: pageParam }, headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load users"); - const users = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? []; - return users as User[]; + const pageData = res.data as any; + const users: User[] = Array.isArray(pageData) ? pageData : (pageData?.results ?? []); + const hasMore: boolean = Array.isArray(pageData) ? false : (pageData?.hasMore ?? false); + return { users, hasMore, nextOffset: pageParam + USERS_PAGE_SIZE }; }, + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextOffset : undefined, }); + useEffect(() => { + const el = sentinelRef.current; + if (!el) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { threshold: 0.1 }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + if (isLoading) return ; if (isError) return ; - const users = data ?? []; + const users = data?.pages.flatMap((p) => p.users) ?? []; if (users.length === 0) { return ( @@ -116,6 +147,8 @@ export function UserList() { {users.map((u) => ( ))} +
+ {isFetchingNextPage && }
); } diff --git a/assets/js/constants.ts b/assets/js/constants.ts index f13a770..7b2c3fd 100644 --- a/assets/js/constants.ts +++ b/assets/js/constants.ts @@ -1,2 +1,3 @@ export const FEED_PAGE_SIZE = 10; export const COMMENTS_PAGE_SIZE = 10; +export const USERS_PAGE_SIZE = 20; diff --git a/fix_plan.md b/fix_plan.md index 1a7862b..954bccc 100644 --- a/fix_plan.md +++ b/fix_plan.md @@ -13,13 +13,12 @@ - [x] Self-follow validation used `get_attribute(:follower_id)` which is nil at validation time (relate_actor runs after) — fixed to use `context.actor.id` - [x] Follow/unfollow test coverage (9 tests) -- [ ] No pagination on user list (`/users`) +- [x] User list pagination — useInfiniteQuery + scroll sentinel, USERS_PAGE_SIZE=20, sorted by username - [ ] No CHECK constraint on `likes >= 0` at DB level (low priority, app logic prevents it) - [ ] `read :following_feed` — nil actor returns empty list (not a bug) - [ ] No search for users or tweets - [x] Tweet creation, update, delete, comment tests (13 tests) - [ ] Missing test coverage: auth flows -- [ ] No pagination on user list (`/users`) ## Notes