mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix: add better offline messages and checks (#1051)
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -65,7 +65,7 @@
|
||||
"react-native-ios-utilities": "5.1.8",
|
||||
"react-native-mmkv": "2.12.2",
|
||||
"react-native-pager-view": "^6.9.1",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-reanimated": "~3.19.1",
|
||||
"react-native-reanimated-carousel": "4.0.2",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
@@ -1637,7 +1637,7 @@
|
||||
|
||||
"react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
|
||||
|
||||
"react-native-reanimated": ["react-native-reanimated@3.17.5", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw=="],
|
||||
"react-native-reanimated": ["react-native-reanimated@3.19.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ=="],
|
||||
|
||||
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="],
|
||||
|
||||
|
||||
@@ -74,7 +74,12 @@ export const HomeIndex = () => {
|
||||
|
||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||
const prevIsConnected = useRef<boolean | null>(false);
|
||||
const { isConnected, loading: retryLoading, retryCheck } = useNetworkStatus();
|
||||
const {
|
||||
isConnected,
|
||||
serverConnected,
|
||||
loading: retryLoading,
|
||||
retryCheck,
|
||||
} = useNetworkStatus();
|
||||
const invalidateCache = useInvalidatePlaybackProgressCache();
|
||||
useEffect(() => {
|
||||
// Only invalidate cache when transitioning from offline to online
|
||||
@@ -358,13 +363,28 @@ export const HomeIndex = () => {
|
||||
|
||||
const sections = settings?.home?.sections ? customSections : defaultSections;
|
||||
|
||||
if (isConnected === false) {
|
||||
if (!isConnected || serverConnected !== true) {
|
||||
let title: string;
|
||||
let subtitle: string;
|
||||
|
||||
if (!isConnected) {
|
||||
// No network connection
|
||||
title = t("home.no_internet");
|
||||
subtitle = t("home.no_internet_message");
|
||||
} else if (serverConnected === null) {
|
||||
// Network is up, but server is being checked
|
||||
title = t("home.checking_server_connection");
|
||||
subtitle = t("home.checking_server_connection_message");
|
||||
} else if (!serverConnected) {
|
||||
// Network is up, but server is unreachable
|
||||
title = t("home.server_unreachable");
|
||||
subtitle = t("home.server_unreachable_message");
|
||||
}
|
||||
return (
|
||||
<View className='flex flex-col items-center justify-center h-full -mt-6 px-8'>
|
||||
<Text className='text-3xl font-bold mb-2'>{t("home.no_internet")}</Text>
|
||||
<Text className='text-center opacity-70'>
|
||||
{t("home.no_internet_message")}
|
||||
</Text>
|
||||
<Text className='text-3xl font-bold mb-2'>{title}</Text>
|
||||
<Text className='text-center opacity-70'>{subtitle}</Text>
|
||||
|
||||
<View className='mt-4'>
|
||||
{!Platform.isTV && (
|
||||
<Button
|
||||
@@ -378,6 +398,7 @@ export const HomeIndex = () => {
|
||||
{t("home.go_to_downloads")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color='black'
|
||||
onPress={retryCheck}
|
||||
@@ -390,9 +411,9 @@ export const HomeIndex = () => {
|
||||
}
|
||||
>
|
||||
{retryLoading ? (
|
||||
<ActivityIndicator size={"small"} color={"white"} />
|
||||
<ActivityIndicator size='small' color='white' />
|
||||
) : (
|
||||
"Retry"
|
||||
t("home.retry")
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -1,30 +1,58 @@
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
|
||||
async function checkApiReachable(basePath?: string): Promise<boolean> {
|
||||
if (!basePath) return false;
|
||||
try {
|
||||
const response = await fetch(basePath, { method: "HEAD" });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const validateConnection = useCallback(async () => {
|
||||
if (!api?.basePath) return false;
|
||||
const reachable = await checkApiReachable(api.basePath);
|
||||
setServerConnected(reachable);
|
||||
return reachable;
|
||||
}, [api?.basePath]);
|
||||
|
||||
// Manual check (optional)
|
||||
const retryCheck = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const state = await NetInfo.fetch();
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
await validateConnection();
|
||||
setLoading(false);
|
||||
}, []);
|
||||
}, [validateConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
const unsubscribe = NetInfo.addEventListener(async (state) => {
|
||||
setIsConnected(!!state.isConnected);
|
||||
if (state.isConnected) {
|
||||
await validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial state
|
||||
// Initial check: wait for NetInfo first
|
||||
NetInfo.fetch().then((state) => {
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
if (state.isConnected) {
|
||||
validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
}, [validateConnection]);
|
||||
|
||||
return { isConnected, loading, retryCheck };
|
||||
return { isConnected, serverConnected, loading, retryCheck };
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"react-native-ios-utilities": "5.1.8",
|
||||
"react-native-mmkv": "2.12.2",
|
||||
"react-native-pager-view": "^6.9.1",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-reanimated": "~3.19.1",
|
||||
"react-native-reanimated-carousel": "4.0.2",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
|
||||
@@ -374,7 +374,7 @@ function useProtectedRoute(user: UserDto | null, loaded = false) {
|
||||
useEffect(() => {
|
||||
if (loaded === false) return;
|
||||
|
||||
const inAuthGroup = segments[0] === "(auth)";
|
||||
const inAuthGroup = segments.length > 1 && segments[0] === "(auth)";
|
||||
|
||||
if (!user?.Id && inAuthGroup) {
|
||||
console.log("Redirected to login");
|
||||
|
||||
@@ -35,10 +35,15 @@
|
||||
"servers": "Servers"
|
||||
},
|
||||
"home": {
|
||||
"checking_server_connection": "Checking server connection...",
|
||||
"no_internet": "No Internet",
|
||||
"no_items": "No items",
|
||||
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
||||
"checking_server_connection_message": "Checking connection to server",
|
||||
"go_to_downloads": "Go to downloads",
|
||||
"retry": "Retry",
|
||||
"server_unreachable": "Server Unreachable",
|
||||
"server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
|
||||
"oops": "Oops!",
|
||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||
"continue_watching": "Continue Watching",
|
||||
|
||||
Reference in New Issue
Block a user