fix(native): show errors for Explore rows, failed pages and Register

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 22:06:42 -04:00
1 parent 669fc74653
commit ecf119c98e
20 files changed
+175 -65

No files matched your search

+10
View File
@@ -97,6 +97,16 @@ export default function RegisterScreen() {
}
};
if (publicInfo.isPending) {
return (
<AuthScreen title={t`Create account`}>
<View className="items-center py-12">
<Spinner />
</View>
</AuthScreen>
);
}
if (publicInfo.isError && !publicInfo.data) {
return (
<AuthScreen title={t`Can't reach the server`}>
@@ -80,6 +80,8 @@ export default function ExploreScreen() {
defaultUserStatuses={trendingStatuses}
defaultEpisodeProgress={trendingProgress}
isLoading={trending.isPending}
isError={trending.isError}
onRetry={() => void trending.refetch()}
/>
</Animated.View>
@@ -93,6 +95,8 @@ export default function ExploreScreen() {
defaultEpisodeProgress={popularMovies.data?.episodeProgress ?? {}}
genres={movieGenres.data?.genres}
isLoading={popularMovies.isPending}
isError={popularMovies.isError}
onRetry={() => void popularMovies.refetch()}
/>
</Animated.View>
@@ -106,6 +110,8 @@ export default function ExploreScreen() {
defaultEpisodeProgress={popularTv.data?.episodeProgress ?? {}}
genres={tvGenres.data?.genres}
isLoading={popularTv.isPending}
isError={popularTv.isError}
onRetry={() => void popularTv.refetch()}
/>
</Animated.View>
</View>
+32 -16
View File
@@ -9,6 +9,7 @@ import { useCSSVariable, useResolveClassNames } from "uniwind";
import { UpcomingRow } from "@/components/dashboard/upcoming-row";
import { EmptyState } from "@/components/ui/empty-state";
import { LoadMoreFooter } from "@/components/ui/load-more-footer";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
@@ -59,21 +60,29 @@ 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 } =
useInfiniteQuery(
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
cursor: pageParam,
direction: isRecent ? "recent" : undefined,
mediaType: !isRecent && mediaType !== "all" ? mediaType : undefined,
statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter] : undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
const {
data,
isPending,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isFetchNextPageError,
} = useInfiniteQuery(
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
cursor: pageParam,
direction: isRecent ? "recent" : undefined,
mediaType: !isRecent && mediaType !== "all" ? mediaType : undefined,
statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter] : undefined,
}),
);
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
);
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
const sections = useMemo(
@@ -93,10 +102,10 @@ export default function UpcomingScreen() {
const { refreshing, onRefresh } = usePullToRefresh(refreshUpcoming);
const onEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
if (hasNextPage && !isFetchingNextPage && !isFetchNextPageError) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
}, [hasNextPage, isFetchingNextPage, isFetchNextPageError, fetchNextPage]);
const filterChips = (
<ScrollView
@@ -186,6 +195,13 @@ export default function UpcomingScreen() {
contentInsetAdjustmentBehavior="automatic"
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={
<LoadMoreFooter
isFetchingNextPage={isFetchingNextPage}
isFetchNextPageError={isFetchNextPageError}
onRetry={() => fetchNextPage()}
/>
}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
ListEmptyComponent={
isPending ? null : isError && allItems.length === 0 ? (
@@ -9,6 +9,7 @@ import { useCSSVariable, useResolveClassNames } from "uniwind";
import { HistoryRow } from "@/components/history/history-row";
import { EmptyState } from "@/components/ui/empty-state";
import { LoadMoreFooter } from "@/components/ui/load-more-footer";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
@@ -57,18 +58,26 @@ export default function HistoryScreen() {
const [mediaType, setMediaType] = useState<"all" | "movie" | "tv">("all");
const { data, isPending, isError, refetch, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
orpc.tracking.history.infiniteOptions({
input: (pageParam: string | undefined) => ({
limit: 30,
cursor: pageParam,
type: mediaType !== "all" ? mediaType : undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
const {
data,
isPending,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isFetchNextPageError,
} = useInfiniteQuery(
orpc.tracking.history.infiniteOptions({
input: (pageParam: string | undefined) => ({
limit: 30,
cursor: pageParam,
type: mediaType !== "all" ? mediaType : undefined,
}),
);
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
);
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
@@ -96,10 +105,10 @@ export default function HistoryScreen() {
const { refreshing, onRefresh } = usePullToRefresh(refreshHistory);
const onEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
if (hasNextPage && !isFetchingNextPage && !isFetchNextPageError) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
}, [hasNextPage, isFetchingNextPage, isFetchNextPageError, fetchNextPage]);
const filterChips = (
<ScrollView
@@ -163,6 +172,13 @@ export default function HistoryScreen() {
contentInsetAdjustmentBehavior="automatic"
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={
<LoadMoreFooter
isFetchingNextPage={isFetchingNextPage}
isFetchNextPageError={isFetchNextPageError}
onRetry={() => fetchNextPage()}
/>
}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
ListEmptyComponent={
isPending ? null : isError && allItems.length === 0 ? (
+6 -12
View File
@@ -17,6 +17,7 @@ import { Pressable, RefreshControl, ScrollView, View, useWindowDimensions } from
import Animated, { FadeIn } from "react-native-reanimated";
import { EmptyState } from "@/components/ui/empty-state";
import { LoadMoreFooter } from "@/components/ui/load-more-footer";
import { PosterCard } from "@/components/ui/poster-card";
import { SelectModal } from "@/components/ui/select-modal";
import { Spinner } from "@/components/ui/spinner";
@@ -438,18 +439,11 @@ export default function LibraryScreen() {
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
libraryQuery.isFetchingNextPage ? (
<View className="items-center py-4">
<Spinner />
</View>
) : libraryQuery.isFetchNextPageError ? (
<View className="flex-row items-center justify-center gap-3 py-4">
<Text className="text-muted-foreground text-sm">{t`Couldn't load more`}</Text>
<Pressable onPress={() => libraryQuery.fetchNextPage()}>
<Text className="text-primary font-sans text-sm font-medium">{t`Retry`}</Text>
</Pressable>
</View>
) : null
<LoadMoreFooter
isFetchingNextPage={libraryQuery.isFetchingNextPage}
isFetchNextPageError={libraryQuery.isFetchNextPageError}
onRetry={() => libraryQuery.fetchNextPage()}
/>
}
/>
)}
+11 -6
View File
@@ -10,6 +10,7 @@ import Animated, { FadeIn } from "react-native-reanimated";
import { RecentlyViewedList } from "@/components/search/recently-viewed-list";
import { type SearchResultItem, SearchResultRow } from "@/components/search/search-result-row";
import { EmptyState } from "@/components/ui/empty-state";
import { LoadMoreFooter } from "@/components/ui/load-more-footer";
import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/text";
import { useDebounce } from "@/hooks/use-debounce";
@@ -117,17 +118,21 @@ export default function SearchScreen() {
keyboardShouldPersistTaps="handled"
contentInsetAdjustmentBehavior="automatic"
onEndReached={() => {
if (searchResults.hasNextPage && !searchResults.isFetchingNextPage) {
if (
searchResults.hasNextPage &&
!searchResults.isFetchingNextPage &&
!searchResults.isFetchNextPageError
) {
searchResults.fetchNextPage();
}
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
searchResults.isFetchingNextPage ? (
<View className="items-center py-4">
<Spinner />
</View>
) : null
<LoadMoreFooter
isFetchingNextPage={searchResults.isFetchingNextPage}
isFetchNextPageError={searchResults.isFetchNextPageError}
onRetry={() => searchResults.fetchNextPage()}
/>
}
/>
)}
@@ -16,6 +16,18 @@ import { orpc } from "@/lib/orpc";
type TitleStatus = "in_watchlist" | "watching" | "caught_up" | "completed";
const genreChipsContentStyle = { paddingHorizontal: 16 };
function LoadError({ onRetry }: { onRetry: () => void }) {
const { t } = useLingui();
return (
<View className="items-center gap-2 py-6">
<Text className="text-muted-foreground text-sm">{t`Couldn't load titles`}</Text>
<Pressable onPress={onRetry}>
<Text className="text-primary font-sans text-sm font-medium">{t`Retry`}</Text>
</Pressable>
</View>
);
}
export function FilterableTitleRow({
title,
icon,
@@ -25,6 +37,8 @@ export function FilterableTitleRow({
defaultEpisodeProgress,
genres,
isLoading,
isError,
onRetry,
}: {
title: string;
icon: Icon;
@@ -43,6 +57,10 @@ export function FilterableTitleRow({
defaultEpisodeProgress: Record<string, { watched: number; total: number }>;
genres?: Array<{ id: number; name: string }>;
isLoading?: boolean;
/** Error state of the default (no genre) list. */
isError?: boolean;
/** Retries the default (no genre) list. */
onRetry?: () => void;
}) {
const { t } = useLingui();
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
@@ -130,12 +148,9 @@ export function FilterableTitleRow({
)}
{!showLoading && items.length === 0 && selectedGenre !== null && discover.isError ? (
<View className="items-center gap-2 py-6">
<Text className="text-muted-foreground text-sm">{t`Couldn't load titles`}</Text>
<Pressable onPress={() => discover.refetch()}>
<Text className="text-primary font-sans text-sm font-medium">{t`Retry`}</Text>
</Pressable>
</View>
<LoadError onRetry={() => discover.refetch()} />
) : !showLoading && items.length === 0 && selectedGenre === null && isError ? (
<LoadError onRetry={() => onRetry?.()} />
) : !showLoading && items.length === 0 && selectedGenre !== null ? (
<View className="items-center py-6">
<Text className="text-muted-foreground text-sm">
@@ -0,0 +1,36 @@
import { useLingui } from "@lingui/react/macro";
import { Pressable, View } from "react-native";
import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/text";
export function LoadMoreFooter({
isFetchingNextPage,
isFetchNextPageError,
onRetry,
}: {
isFetchingNextPage: boolean;
isFetchNextPageError: boolean;
onRetry: () => void;
}) {
const { t } = useLingui();
if (isFetchingNextPage) {
return (
<View className="items-center py-4">
<Spinner />
</View>
);
}
if (isFetchNextPageError) {
return (
<View className="flex-row items-center justify-center gap-3 py-4">
<Text className="text-muted-foreground text-sm">{t`Couldn't load more`}</Text>
<Pressable onPress={onRetry}>
<Text className="text-primary font-sans text-sm font-medium">{t`Retry`}</Text>
</Pressable>
</View>
);
}
return null;
}
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "تعذّر تحميل مكتبتك"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "جارٍ استرداد سجل المشاهدة وقائمة المشا
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Bibliothek konnte nicht geladen werden"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Verlauf, Merkliste und Bewertungen werden abgerufen..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Could not load your library"
msgid "Could not reach the server. Check your connection and the URL."
msgstr "Could not reach the server. Check your connection and the URL."
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr "Couldn't load more"
@@ -2905,6 +2905,7 @@ msgstr ""
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "No se pudo cargar tu biblioteca"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Obteniendo tu historial de visionado, lista y valoraciones..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Impossible de charger votre bibliothèque"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Récupération de votre historique, liste de suivi et notes..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "לא ניתן לטעון את הספרייה שלך"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "מאחזר את היסטוריית הצפייה, רשימת הצפייה
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Impossibile caricare la tua libreria"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Recupero della cronologia, watchlist e valutazioni..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "ライブラリを読み込めませんでした"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "視聴履歴、ウォッチリスト、評価を取得中..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "라이브러리를 불러올 수 없습니다"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "시청 기록, 시청 목록, 평점을 가져오는 중..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Kon je bibliotheek niet laden"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Kijkgeschiedenis, watchlist en beoordelingen ophalen..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "Não foi possível carregar a sua biblioteca"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "Buscando seu histórico de exibição, lista de desejos e avaliações..
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx
+2 -1
View File
@@ -863,7 +863,7 @@ msgstr "无法加载您的媒体库"
msgid "Could not reach the server. Check your connection and the URL."
msgstr ""
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
msgid "Couldn't load more"
msgstr ""
@@ -2905,6 +2905,7 @@ msgstr "正在获取您的观看记录、观看列表和评分..."
#: apps/native/src/app/(tabs)/(search)/index.tsx
#: apps/native/src/components/explore/filterable-title-row.tsx
#: apps/native/src/components/settings/integrations-section.tsx
#: apps/native/src/components/ui/load-more-footer.tsx
#: apps/native/src/components/ui/offline-banner.ios.tsx
#: apps/native/src/components/ui/offline-banner.tsx
#: apps/native/src/components/ui/server-unreachable-banner.ios.tsx