mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix: refresh data on open page in background replace cached data
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -16,6 +15,7 @@ import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
|
||||
export default function page() {
|
||||
@@ -26,7 +26,7 @@ export default function page() {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const { settings, updateSettings, pluginSettings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const [value, setValue] = useState<string>(settings?.marlinServerUrl || "");
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -15,6 +14,7 @@ import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ListGroup } from "@/components/list/ListGroup";
|
||||
import { ListItem } from "@/components/list/ListItem";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
|
||||
export default function page() {
|
||||
@@ -28,7 +28,7 @@ export default function page() {
|
||||
pluginSettings,
|
||||
refreshStreamyfinPluginSettings,
|
||||
} = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
// Local state for all editable fields
|
||||
const [url, setUrl] = useState<string>(settings?.streamyStatsServerUrl || "");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "@/augmentations";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { onlineManager, QueryClient } from "@tanstack/react-query";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
import * as BackgroundTask from "expo-background-task";
|
||||
import * as Device from "expo-device";
|
||||
@@ -187,11 +188,29 @@ export default function RootLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
// Set up online manager for network-aware query behavior
|
||||
onlineManager.setEventListener((setOnline) => {
|
||||
return NetInfo.addEventListener((state) => {
|
||||
setOnline(!!state.isConnected);
|
||||
});
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30000, // 30 seconds - data is fresh
|
||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours - keep in cache for persistence
|
||||
staleTime: 0, // Always stale - triggers background refetch on mount
|
||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours - keep in cache for offline
|
||||
networkMode: "offlineFirst", // Return cache first, refetch if online
|
||||
refetchOnMount: true, // Refetch when component mounts
|
||||
refetchOnReconnect: true, // Refetch when network reconnects
|
||||
refetchOnWindowFocus: false, // Not needed for mobile
|
||||
retry: (failureCount) => {
|
||||
if (!onlineManager.isOnline()) return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
networkMode: "online", // Only run mutations when online
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useRouter } from "expo-router";
|
||||
import { t } from "i18next";
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
} from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { calculateSmoothedETA } from "@/providers/Downloads/hooks/useDownloadSpeedCalculator";
|
||||
import { JobStatus } from "@/providers/Downloads/types";
|
||||
@@ -37,7 +37,7 @@ interface DownloadCardProps extends TouchableOpacityProps {
|
||||
export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
const { cancelDownload } = useDownload();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
|
||||
@@ -4,9 +4,10 @@ import type {
|
||||
UserDto,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getLocalizationApi, getUserApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { createContext, type ReactNode, useContext, useEffect } from "react";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { type Settings, useSettings } from "@/utils/atoms/settings";
|
||||
|
||||
@@ -30,7 +31,7 @@ export const useMedia = () => {
|
||||
export const MediaProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const updateSetingsWrapper = (update: Partial<Settings>) => {
|
||||
const updateUserConfiguration = async (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
@@ -6,6 +6,7 @@ import { toast } from "sonner-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import {
|
||||
clearCache,
|
||||
clearPermanentDownloads,
|
||||
@@ -18,7 +19,7 @@ import { ListItem } from "../list/ListItem";
|
||||
export const StorageSettings = () => {
|
||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const successHapticFeedback = useHaptic("success");
|
||||
const errorHapticFeedback = useHaptic("error");
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
|
||||
export const useFavorite = (item: BaseItemDto) => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [isFavorite, setIsFavorite] = useState<boolean | undefined>(
|
||||
|
||||
@@ -10,10 +10,10 @@ import type {
|
||||
} from "@/utils/jellyseerr/server/models/Search";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import "@/augmentations";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { toast } from "sonner-native";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
||||
import {
|
||||
@@ -436,7 +436,7 @@ const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
|
||||
export const useJellyseerr = () => {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const jellyseerrApi = useMemo(() => {
|
||||
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useHaptic } from "./useHaptic";
|
||||
import { usePlaybackManager } from "./usePlaybackManager";
|
||||
import { useInvalidatePlaybackProgressCache } from "./useRevalidatePlaybackProgressCache";
|
||||
|
||||
export const useMarkAsPlayed = (items: BaseItemDto[]) => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const lightHapticFeedback = useHaptic("light");
|
||||
const { markItemPlayed, markItemUnplayed } = usePlaybackManager();
|
||||
const invalidatePlaybackProgressCache = useInvalidatePlaybackProgressCache();
|
||||
|
||||
47
hooks/useNetworkAwareQueryClient.ts
Normal file
47
hooks/useNetworkAwareQueryClient.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
InvalidateQueryFilters,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
} from "@tanstack/react-query";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { invalidateQueriesWhenOnline } from "@/utils/query/networkAwareInvalidate";
|
||||
|
||||
type NetworkAwareQueryClient = QueryClient & {
|
||||
forceInvalidateQueries: QueryClient["invalidateQueries"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a queryClient wrapper with network-aware invalidation.
|
||||
* Use this instead of useQueryClient when you need to invalidate queries.
|
||||
*
|
||||
* - invalidateQueries: Only invalidates when online (preserves offline cache)
|
||||
* - forceInvalidateQueries: Always invalidates (use sparingly)
|
||||
*/
|
||||
export function useNetworkAwareQueryClient(): NetworkAwareQueryClient {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const networkAwareInvalidate = useCallback(
|
||||
<TTaggedQueryKey extends QueryKey = QueryKey>(
|
||||
filters?: InvalidateQueryFilters<TTaggedQueryKey>,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<void> => {
|
||||
if (!filters) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return invalidateQueriesWhenOnline(queryClient, filters, options);
|
||||
},
|
||||
[queryClient],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
// Create a proxy-like object that inherits from queryClient
|
||||
// but overrides invalidateQueries
|
||||
const wrapped = Object.create(queryClient) as NetworkAwareQueryClient;
|
||||
wrapped.invalidateQueries = networkAwareInvalidate;
|
||||
wrapped.forceInvalidateQueries =
|
||||
queryClient.invalidateQueries.bind(queryClient);
|
||||
return wrapped;
|
||||
}, [queryClient, networkAwareInvalidate]);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getLibraryApi, getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner-native";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
export const useCreatePlaylist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -58,7 +59,7 @@ export const useCreatePlaylist = () => {
|
||||
export const useAddToPlaylist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -108,7 +109,7 @@ export const useAddToPlaylist = () => {
|
||||
*/
|
||||
export const useRemoveFromPlaylist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -160,7 +161,7 @@ export const useRemoveFromPlaylist = () => {
|
||||
*/
|
||||
export const useDeletePlaylist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mutation = useMutation({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { useTwoWaySync } from "./useTwoWaySync";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTwoWaySync } from "./useTwoWaySync";
|
||||
* useRevalidatePlaybackProgressCache invalidates queries related to playback progress.
|
||||
*/
|
||||
export function useInvalidatePlaybackProgressCache() {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
const { getDownloadedItems } = useDownload();
|
||||
const { syncPlaybackState } = useTwoWaySync();
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner-native";
|
||||
import { useNetworkAwareQueryClient } from "@/hooks/useNetworkAwareQueryClient";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||
@@ -17,7 +18,7 @@ import type {
|
||||
export const useCreateWatchlist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (
|
||||
@@ -58,7 +59,7 @@ export const useCreateWatchlist = () => {
|
||||
export const useUpdateWatchlist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
@@ -106,7 +107,7 @@ export const useUpdateWatchlist = () => {
|
||||
export const useDeleteWatchlist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (watchlistId: number): Promise<void> => {
|
||||
@@ -147,7 +148,7 @@ export const useDeleteWatchlist = () => {
|
||||
export const useAddToWatchlist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
@@ -205,7 +206,7 @@ export const useAddToWatchlist = () => {
|
||||
export const useRemoveFromWatchlist = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const { settings } = useSettings();
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useNetworkAwareQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
createContext,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
@@ -37,6 +39,8 @@ export function NetworkStatusProvider({ children }: { children: ReactNode }) {
|
||||
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const wasServerConnected = useRef<boolean | null>(null);
|
||||
|
||||
const validateConnection = useCallback(async () => {
|
||||
if (!api?.basePath) return false;
|
||||
@@ -73,6 +77,14 @@ export function NetworkStatusProvider({ children }: { children: ReactNode }) {
|
||||
return () => unsubscribe();
|
||||
}, [validateConnection]);
|
||||
|
||||
// Refetch active queries when server becomes reachable
|
||||
useEffect(() => {
|
||||
if (serverConnected && wasServerConnected.current === false) {
|
||||
queryClient.refetchQueries({ type: "active" });
|
||||
}
|
||||
wasServerConnected.current = serverConnected;
|
||||
}, [serverConnected, queryClient]);
|
||||
|
||||
return (
|
||||
<NetworkStatusContext.Provider
|
||||
value={{ isConnected, serverConnected, loading, retryCheck }}
|
||||
|
||||
21
utils/query/networkAwareInvalidate.ts
Normal file
21
utils/query/networkAwareInvalidate.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
type InvalidateOptions,
|
||||
type InvalidateQueryFilters,
|
||||
onlineManager,
|
||||
type QueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
/**
|
||||
* Invalidates queries only when online. When offline, the invalidation
|
||||
* is skipped to preserve cached data for offline use.
|
||||
*/
|
||||
export function invalidateQueriesWhenOnline(
|
||||
queryClient: QueryClient,
|
||||
filters: InvalidateQueryFilters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<void> {
|
||||
if (!onlineManager.isOnline()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return queryClient.invalidateQueries(filters, options);
|
||||
}
|
||||
Reference in New Issue
Block a user