Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/api/video-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const useSearchVideos = createInfiniteQuery<
export const useVideoDetail = createQuery<VideoDetail, { id: string }, Error>({
queryKey: ['video'],
fetcher: (variables) => fetchVideoDetail(variables.id),
// The fetcher already times out after 15s; with the default 2 retries a
// dead network keeps the watch screen spinning for ~48s before the error
// screen appears, which reads as a freeze. Fail once, let the user retry.
retry: 0,
});

/** Single page of a user's uploads (the site's user page has no pagination). */
Expand Down
9 changes: 7 additions & 2 deletions src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function BrowseScreen() {

// Default browse = genre only (no query, no sort), matching the site. Query is
// added only when typing; sort/tags/broad only when set in the filter sheet.
const { data, fetchNextPage, isFetchingNextPage, isLoading, refetch, isFetching } =
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch, isFetching } =
useSearchVideos({
variables: {
genre,
Expand Down Expand Up @@ -117,7 +117,12 @@ export default function BrowseScreen() {

<VideoGrid
items={items}
onEndReached={() => fetchNextPage()}
onEndReached={() => {
// Short lists fire onEndReached repeatedly (especially with one
// row of results); without the guard each fire queues another
// page fetch and walks the whole site on a struggling network.
if (hasNextPage && !isFetchingNextPage) void fetchNextPage();
}}
onRefresh={() => refetch()}
isRefreshing={isFetching && !isLoading}
isFetchingNextPage={isFetchingNextPage}
Expand Down
6 changes: 4 additions & 2 deletions src/app/tag/[name].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function TagScreen() {
// text — the `tags[]` filter knows nothing about them.
const searchTags = params.type === 'query' ? undefined : [name];
const query = params.type === 'query' ? name : undefined;
const { data, fetchNextPage, isFetchingNextPage, isLoading, refetch, isFetching } =
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch, isFetching } =
useSearchVideos({ variables: { tags: searchTags, query } });

const items = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
Expand All @@ -29,7 +29,9 @@ export default function TagScreen() {
</View>
<VideoGrid
items={items}
onEndReached={() => fetchNextPage()}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) void fetchNextPage();
}}
onRefresh={() => refetch()}
isRefreshing={isFetching && !isLoading}
isFetchingNextPage={isFetchingNextPage}
Expand Down
13 changes: 11 additions & 2 deletions src/app/watch/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { useEvent } from 'expo';
import { Link, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useVideoPlayer } from 'expo-video';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, Pressable, ScrollView, Share, Text, View } from 'react-native';

export { ScreenErrorBoundary as ErrorBoundary };
Expand Down Expand Up @@ -81,13 +81,20 @@ export default function WatchScreen() {

const { autoplay } = usePlaybackSettings();

// Focus guard for the slow-network case: the detail query can resolve long
// after the user has backed out of this screen (it stays mounted in the
// stack). Firing play() then would start a player that was never attached
// to a view — or play audio underneath the previous screen — so autoplay
// must only run while this screen actually has focus.
const isFocusedRef = useRef(true);

// Start playback automatically when the screen is entered or the source
// changes (new video via the stack, or a resolution switch) unless the
// user opted out in Settings. Coming back from a deeper screen (author /
// tag) stays paused — the focus cleanup below paused it on leave.
// biome-ignore lint/correctness/useExhaustiveDependencies: videoSource is an intentional trigger — replay when the source lands or switches.
useEffect(() => {
if (!autoplay) return;
if (!autoplay || !isFocusedRef.current) return;
try {
player.play();
} catch {
Expand All @@ -102,7 +109,9 @@ export default function WatchScreen() {
// "shared object already released", which is safe to ignore.
useFocusEffect(
useCallback(() => {
isFocusedRef.current = true;
return () => {
isFocusedRef.current = false;
try {
player.pause();
} catch {
Expand Down
Loading