fix(native): block duplicate watch taps, stop invalidating title details, and show refresh spinner only on pull

- invalidateTitleQueries no longer invalidates titles.get and returns a promise for tracking/library refetches; mutations return it from onSuccess so isPending covers the refetch
- per-episode pending guard (disabled + dimmed row) and disabled season "Mark All Watched" while pending
- add usePullToRefresh so RefreshControl only reflects user-initiated pulls (Home, Explore, Library, Upcoming)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 03:07:24 -04:00
1 parent bc3ea712f9
commit 67766298d8
9 files changed
+132 -73

No files matched your search

+20 -25
View File
@@ -11,6 +11,7 @@ import { UpcomingRow } from "@/components/dashboard/upcoming-row";
import { EmptyState } from "@/components/ui/empty-state";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { orpc } from "@/lib/orpc";
import { queryClient } from "@/lib/query-client";
import * as Haptics from "@/utils/haptics";
@@ -56,28 +57,20 @@ export default function UpcomingScreen() {
const [mediaType, setMediaType] = useState<"all" | "movie" | "tv">("all");
const [statusFilter, setStatusFilter] = useState<"all" | "watching" | "watchlist">("all");
const {
data,
isPending,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isRefetching,
} = useInfiniteQuery(
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
cursor: pageParam,
mediaType: mediaType !== "all" ? mediaType : undefined,
statusFilter: statusFilter !== "all" ? [statusFilter] : undefined,
const { data, isPending, isError, refetch, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
cursor: pageParam,
mediaType: mediaType !== "all" ? mediaType : undefined,
statusFilter: statusFilter !== "all" ? [statusFilter] : undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
);
);
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
const sections = useMemo(
@@ -90,9 +83,11 @@ export default function UpcomingScreen() {
[allItems],
);
const onRefresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: orpc.library.upcoming.key() });
}, []);
const refreshUpcoming = useCallback(
() => queryClient.invalidateQueries({ queryKey: orpc.library.upcoming.key() }),
[],
);
const { refreshing, onRefresh } = usePullToRefresh(refreshUpcoming);
const onEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
@@ -171,7 +166,7 @@ export default function UpcomingScreen() {
contentInsetAdjustmentBehavior="automatic"
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={onRefresh} />}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
ListEmptyComponent={
isPending ? null : isError && allItems.length === 0 ? (
<EmptyState