diff --git a/src/components/Global/PapillonModernHeader.tsx b/src/components/Global/PapillonModernHeader.tsx index 7d21dee7..df29f5f4 100644 --- a/src/components/Global/PapillonModernHeader.tsx +++ b/src/components/Global/PapillonModernHeader.tsx @@ -1,27 +1,15 @@ -import React, { Children, useEffect, useRef, useState } from "react"; -import { Button, StyleSheet, View } from "react-native"; - -import { Screen } from "@/router/helpers/types"; -import { NativeText } from "@/components/Global/NativeComponents"; -import InfiniteDatePager from "@/components/Global/InfiniteDatePager"; -import { useCurrentAccount } from "@/stores/account"; -import { useTimetableStore } from "@/stores/timetable"; -import { AccountService } from "@/stores/account/types"; -import { updateTimetableForWeekInCache } from "@/services/timetable"; -import { set } from "lodash"; -import { dateToEpochWeekNumber } from "@/utils/epochWeekNumber"; - -import Reanimated, { FadeIn, FadeInDown, FadeInLeft, FadeOut, FadeOutLeft, FadeOutRight, FadeOutUp, LinearTransition, ZoomIn, ZoomOut } from "react-native-reanimated"; +import React, { useEffect, useState } from "react"; +import { StyleSheet, View } from "react-native"; +import Reanimated, { FadeIn, FadeOut, LinearTransition, ZoomIn, ZoomOut } from "react-native-reanimated"; import { animPapillon } from "@/utils/ui/animations"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import PapillonSpinner from "@/components/Global/PapillonSpinner"; import { PressableScale } from "react-native-pressable-scale"; import { useTheme } from "@react-navigation/native"; import { BlurView } from "expo-blur"; -import AnimatedNumber from "@/components/Global/AnimatedNumber"; import { LinearGradient } from "expo-linear-gradient"; -import { TouchableOpacity } from "react-native-gesture-handler"; -import { ArrowLeftToLine, ArrowUp, CalendarCheck, CalendarClock, CalendarPlus, CalendarSearch, History, ListRestart, Loader, Plus, Rewind } from "lucide-react-native"; +import NetInfo from "@react-native-community/netinfo"; +import { WifiOff } from "lucide-react-native"; interface ModernHeaderProps { children: React.ReactNode, @@ -33,9 +21,6 @@ interface ModernHeaderProps { }; export const PapillonModernHeader: React.FC = (props) => { - const theme = useTheme(); - const insets = useSafeAreaInsets(); - if (props.native) { return ( @@ -243,6 +228,13 @@ export const PapillonHeaderSelector: React.FC<{ loading = false, }) => { const theme = useTheme(); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); return ( {children} - {loading && + {isOnline && loading && , - loading?: boolean, + small?: boolean; + opened?: boolean; + modalOpen?: boolean; + translationY?: Reanimated.SharedValue; + loading?: boolean; }> = ({ small, opened, modalOpen, translationY, loading }) => { const theme = useTheme(); const { colors } = theme; - const account = useCurrentAccount(store => store.account!); + const account = useCurrentAccount((store) => store.account!); const shouldHideName = account.personalization.hideNameOnHomeScreen || false; - const shouldHidePicture = account.personalization.hideProfilePicOnHomeScreen || false; + const shouldHidePicture = + account.personalization.hideProfilePicOnHomeScreen || false; - const borderAnimatedStyle = useAnimatedStyle(() => ({ - borderWidth: 1, - borderRadius: 80, - borderColor: interpolateColor( - translationY?.value || 0, // Should think to pass a default value - [200, 251], - ["#ffffff50", colors.border], - ), - backgroundColor: interpolateColor( - translationY?.value || 0, // Should think to pass a default value - [200, 251], - ["#ffffff30", "transparent"], - ), - })); - - const textAnimatedStyle = useAnimatedStyle(() => ({ - color: interpolateColor( - translationY?.value || 0, // Should think to pass a default value - [200, 251], - ["#FFF", colors.text], - ), - fontSize: 16, - fontFamily: "semibold", - maxWidth: 140, - })); + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + return NetInfo.addEventListener((state) => { + setIsOnline(state.isConnected ?? false); + }); + }, []); const AnimatedChevronDown = Animated.createAnimatedComponent(ChevronDown); - const iconAnimatedStyle = useAnimatedStyle(() => ({ - color: interpolateColor( - translationY?.value || 0, // Should think to pass a default value - [200, 251], - ["#FFF", colors.text], - ), - marginLeft: -6, - })); return ( ) : ( - + )} - {account.studentName ? ( - account.studentName?.first + (shouldHideName ? "" : " " + account.studentName.last) - ) : "Mon compte"} + {account.studentName + ? account.studentName?.first + + (shouldHideName ? "" : " " + account.studentName.last) + : "Mon compte"} - {loading && ( + {isOnline && loading && ( )} - + > - navigation?: NativeStackNavigationProp + widget: React.ForwardRefExoticComponent>; + navigation?: NativeStackNavigationProp< + RouteParameters, + keyof RouteParameters + >; } export interface WidgetProps { @@ -28,13 +38,23 @@ export interface WidgetProps { hidden: boolean; } -const Widget: React.FC = ({ widget: DynamicWidget, navigation }) => { +const Widget: React.FC = ({ + widget: DynamicWidget, + navigation, +}) => { const theme = useTheme(); const { colors } = theme; const widgetRef = useRef | null>(null); const [loading, setLoading] = useState(true); const [hidden, setHidden] = useState(false); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener((state) => { + setIsOnline(state.isConnected ?? false); + }); + }, []); const handlePress = () => { const location = (widgetRef.current as any)?.handlePress(); @@ -47,30 +67,27 @@ const Widget: React.FC = ({ widget: DynamicWidget, navigat @@ -143,4 +159,4 @@ const styles = StyleSheet.create({ }, }); -export default Widget; \ No newline at end of file +export default Widget; diff --git a/src/views/account/Attendance/Attendance.tsx b/src/views/account/Attendance/Attendance.tsx index 0108845a..d876ad6f 100644 --- a/src/views/account/Attendance/Attendance.tsx +++ b/src/views/account/Attendance/Attendance.tsx @@ -6,10 +6,10 @@ import type { Screen } from "@/router/helpers/types"; import { useCurrentAccount } from "@/stores/account"; import { useAttendanceStore } from "@/stores/attendance"; import { updateAttendanceInCache, updateAttendancePeriodsInCache } from "@/services/attendance"; -import { NativeText } from "@/components/Global/NativeComponents"; -import Reanimated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; +import { NativeItem, NativeList, NativeText } from "@/components/Global/NativeComponents"; +import Reanimated, { FadeIn, FadeOut, FadeOutUp, FlipInXDown, LinearTransition } from "react-native-reanimated"; import PapillonPicker from "@/components/Global/PapillonPicker"; -import { ChevronDown, Eye, Scale, Timer, UserX } from "lucide-react-native"; +import { ChevronDown, Eye, Scale, Timer, UserX, WifiOff } from "lucide-react-native"; import PapillonHeader from "@/components/Global/PapillonHeader"; import { animPapillon } from "@/utils/ui/animations"; import AttendanceItem from "./Atoms/AttendanceItem"; @@ -20,6 +20,8 @@ import { protectScreenComponent } from "@/router/helpers/protected-screen"; import { Observation } from "@/services/shared/Observation"; import MissingItem from "@/components/Global/MissingItem"; import React from "react"; +import NetInfo from "@react-native-community/netinfo"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; const Attendance: Screen<"Attendance"> = ({ route, navigation }) => { const theme = useTheme(); @@ -29,14 +31,21 @@ const Attendance: Screen<"Attendance"> = ({ route, navigation }) => { const periods = useAttendanceStore(store => store.periods); const attendances = useAttendanceStore(store => store.attendances); - - + const errorTitle = useMemo(() => getErrorTitle(), []); const [isRefreshing] = useState(false); const [isLoading, setLoading] = useState(true); const [userSelectedPeriod, setUserSelectedPeriod] = useState(null); const selectedPeriod = useMemo(() => userSelectedPeriod ?? defaultPeriod, [userSelectedPeriod, defaultPeriod]); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + useEffect(() => { updateAttendancePeriodsInCache(account); }, [navigation, account.instance]); @@ -201,16 +210,20 @@ const Attendance: Screen<"Attendance"> = ({ route, navigation }) => { - {isLoading && !isRefreshing && + {isOnline && isLoading && !isRefreshing && ( - + - } + )} @@ -223,6 +236,24 @@ const Attendance: Screen<"Attendance"> = ({ route, navigation }) => { paddingTop: 0, }} > + {!isOnline && + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être obsolètes. + + + + + } {attendances[selectedPeriod] && attendances[selectedPeriod].absences.length === 0 && attendances[selectedPeriod].delays.length === 0 && attendances[selectedPeriod].punishments.length === 0 && Object.keys(attendances_observations_details).length === 0 &&( = ({ navigation, route }) => { const account = useCurrentAccount((state) => state.account!); const [chats, setChats] = useState(null); + const [isOnline, setIsOnline] = useState(true); + const errorTitle = useMemo(() => getErrorTitle(), []); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + useLayoutEffect(() => { navigation.setOptions({ ...TabAnimatedTitle({ route, navigation }), @@ -81,95 +96,114 @@ const Messages: Screen<"Messages"> = ({ navigation, route }) => { return ( - {!chats ? ( + {!isOnline ? - - - - Chargement des discussions... - - - - Vos conversations arrivent... - + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez + + + - ) : chats.length === 0 ? ( - - ) : ( - - {chats.map((chat) => ( - navigation.navigate("Chat", { handle: chat })} - leading={ - + : ( + !chats ? ( + - + + - {!chat.read && ( + Chargement des discussions... + + + + Vos conversations arrivent... + + + ) : chats.length === 0 ? ( + + ) : ( + + {chats.map((chat) => ( + navigation.navigate("Chat", { handle: chat })} + leading={ + + } + > - )} - {chat.recipient} - - {chat.subject || "Aucun sujet"} - - ))} - - )} + style={{ flexDirection: "row", alignItems: "center", gap: 5 }} + > + {!chat.read && ( + + )} + {chat.recipient} + + {chat.subject || "Aucun sujet"} + + ))} + + ) + )} ); }; diff --git a/src/views/account/Grades/Grades.tsx b/src/views/account/Grades/Grades.tsx index 564687c3..346e8cee 100644 --- a/src/views/account/Grades/Grades.tsx +++ b/src/views/account/Grades/Grades.tsx @@ -16,7 +16,7 @@ import { useGradesStore } from "@/stores/grades"; import { animPapillon } from "@/utils/ui/animations"; import BackgroundIUTLannion from "@/views/login/IdentityProvider/actions/BackgroundIUTLannion"; import { useTheme } from "@react-navigation/native"; -import { ChevronDown } from "lucide-react-native"; +import { ChevronDown, WifiOff } from "lucide-react-native"; import React from "react"; import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react"; import { @@ -30,9 +30,18 @@ import Reanimated, { FadeInUp, FadeOut, FadeOutDown, + FadeOutUp, + FlipInXDown, LinearTransition, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import NetInfo from "@react-native-community/netinfo"; +import { + NativeList, + NativeItem, + NativeText, +} from "@/components/Global/NativeComponents"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; const GradesAverageGraph = lazy(() => import("./Graph/GradesAverage")); const GradesLatestList = lazy(() => import("./Latest/LatestGrades")); @@ -63,8 +72,18 @@ const Grades: Screen<"Grades"> = ({ route, navigation }) => { ); const latestGradesRef = useRef([]); + const errorTitle = useMemo(() => getErrorTitle(), []); const [isRefreshing, setIsRefreshing] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + setTimeout(() => { + return NetInfo.addEventListener((state) => { + setIsOnline(state.isConnected ?? false); + }); + }, 100); // Par rapport au bon rendu du graphique + }, []); useEffect(() => { setTimeout(() => { @@ -176,15 +195,20 @@ const Grades: Screen<"Grades"> = ({ route, navigation }) => { - - {!isLoading && ( + {((isOnline && !isLoading) || !isOnline) && ( setIsRefreshing(true)} - colors={Platform.OS === "android" ? [theme.colors.primary] : void 0} + onRefresh={() => { + if (isOnline) { + setIsRefreshing(true); + } + }} + colors={ + Platform.OS === "android" ? [theme.colors.primary] : void 0 + } progressViewOffset={outsideNav ? 72 : insets.top + 56} /> } @@ -202,9 +226,37 @@ const Grades: Screen<"Grades"> = ({ route, navigation }) => { paddingBottom: 16 + insets.bottom, }} > - {(!grades[selectedPeriod] || grades[selectedPeriod].length === 0) && - !isLoading && - !isRefreshing && ( + {!isOnline && ( + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être + obsolètes. + + + + + )} + + {(!grades[selectedPeriod] || + grades[selectedPeriod].length === 0) && ( = ({ route, navigation }) => { /> )} - {!isLoading && - grades[selectedPeriod] && - grades[selectedPeriod].length > 1 && ( + {grades[selectedPeriod] && grades[selectedPeriod].length > 1 && ( = ({ navigation, refresh, endRefresh }) => { const { colors } = useTheme(); - const account = useCurrentAccount(store => store.account!); - const mutateProperty = useCurrentAccount(store => store.mutateProperty); + const account = useCurrentAccount((store) => store.account!); + const mutateProperty = useCurrentAccount((store) => store.mutateProperty); const [updatedRecently, setUpdatedRecently] = useState(false); - const defined = useFlagsStore(state => state.defined); + const defined = useFlagsStore((state) => state.defined); - const [isOnline, setIsOnline] = useState(false); + const [isOnline, setIsOnline] = useState(true); const errorTitle = useMemo(() => getErrorTitle(), []); const [elements, setElements] = useState([]); @@ -46,7 +50,7 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef useEffect(() => { setElements([]); Elements.forEach((Element) => { - setElements(prevElements => [ + setElements((prevElements) => [ ...prevElements, { id: Element.id, @@ -58,7 +62,7 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef }, []); function sortElementsByImportance () { - setElements(prevElements => { + setElements((prevElements) => { const sortedElements = [...prevElements]; sortedElements.sort((a, b) => { let aImportance = a.importance === undefined ? -1 : a.importance; @@ -70,9 +74,9 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef } const updateImportance = (id: string, value: number) => { - setElements(prevElements => { + setElements((prevElements) => { const updatedElements = [...prevElements]; - const index = updatedElements.findIndex(element => element.id === id); + const index = updatedElements.findIndex((element) => element.id === id); updatedElements[index].importance = value; return updatedElements; }); @@ -84,28 +88,27 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef }; function checkForUpdateRecently () { - AsyncStorage.getItem("changelog.lastUpdate") - .then((value) => { - const currentVersion = PackageJSON.version; - if (value == null || value !== currentVersion) - setUpdatedRecently(true); - }); + AsyncStorage.getItem("changelog.lastUpdate").then((value) => { + const currentVersion = PackageJSON.version; + if (value == null || value !== currentVersion) setUpdatedRecently(true); + }); } const checkForNewTabs = useCallback(() => { const storedTabs = account.personalization.tabs || []; - const newTabs = defaultTabs.filter(defaultTab => - !storedTabs.some(storedTab => storedTab.name === defaultTab.tab) + const newTabs = defaultTabs.filter( + (defaultTab) => + !storedTabs.some((storedTab) => storedTab.name === defaultTab.tab) ); if (newTabs.length > 0) { const updatedTabs = [ ...storedTabs, - ...newTabs.map(tab => ({ + ...newTabs.map((tab) => ({ name: tab.tab, enabled: false, - installed: true - })) + installed: true, + })), ]; mutateProperty("personalization", { @@ -123,8 +126,7 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef } useEffect(() => { - if (refresh) - RefreshData(); + if (refresh) RefreshData(); }, [refresh]); useEffect(() => { @@ -134,7 +136,7 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef }, []); useEffect(() => { - return NetInfo.addEventListener(state => { + return NetInfo.addEventListener((state) => { setIsOnline(state.isConnected ?? false); }); }, []); @@ -145,20 +147,37 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef minHeight: Dimensions.get("window").height - 131, }} > - {(defined("force_changelog") || updatedRecently) && ( + {!isOnline && ( + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être + obsolètes. + + + + + )} + + {(defined("force_changelog") || updatedRecently) && isOnline && ( - } + leading={} onPress={() => navigation.navigate("ChangelogScreen")} style={{ backgroundColor: colors.primary + "30", @@ -171,53 +190,34 @@ const ModalContent: React.FC = ({ navigation, refresh, endRef Papillon {PackageJSON.version} est arrivé ! - Découvrez les nouveautés de cette nouvelle version en appuyant ici. + Découvrez les nouveautés de cette nouvelle version en appuyant + ici. )} - {!isOnline && - - - } - > - - {errorTitle.label} {errorTitle.emoji} - - - Vous êtes hors ligne. Les données affichées peuvent être obsolètes. - - - - - } - - - {elements.map((Element, index) => (Element && - - handleImportanceChange(Element.id, value): - () => {} - } - /> - - ))} + + {elements.map( + (Element, index) => + Element && ( + + handleImportanceChange(Element.id, value) + : () => {} + } + /> + + ) + )} ); diff --git a/src/views/account/Homeworks/Atoms/Item.tsx b/src/views/account/Homeworks/Atoms/Item.tsx index e637a282..63003a11 100644 --- a/src/views/account/Homeworks/Atoms/Item.tsx +++ b/src/views/account/Homeworks/Atoms/Item.tsx @@ -10,11 +10,13 @@ import { animPapillon } from "@/utils/ui/animations"; import HTMLView from "react-native-htmlview"; import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { RouteParameters } from "@/router/helpers/types"; -import { StyleSheet, View } from "react-native"; +import { StyleSheet, Alert, Platform, View } from "react-native"; import { Homework, HomeworkReturnType } from "@/services/shared/Homework"; import detectCategory from "@/utils/magic/categorizeHomeworks"; import { LinearGradient } from "expo-linear-gradient"; import { useCurrentAccount } from "@/stores/account"; +import NetInfo from "@react-native-community/netinfo"; +import { useAlert } from "@/providers/AlertProvider"; interface HomeworkItemProps { key: number | string @@ -40,6 +42,15 @@ const HomeworkItem = ({ homework, navigation, onDonePressHandler, index, total } } }); + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + + const { showAlert } = useAlert(); + useEffect(() => { if (account.personalization?.MagicHomeworks) { const data = getSubjectData(homework.subject); @@ -54,9 +65,31 @@ const HomeworkItem = ({ homework, navigation, onDonePressHandler, index, total } const [isLoading, setIsLoading] = useState(false); const handlePress = useCallback(() => { - setIsLoading(true); - onDonePressHandler(); - }, [onDonePressHandler]); + if (isOnline) { + setIsLoading(true); + onDonePressHandler(); + } else { + if (Platform.OS === "ios") { + Alert.alert("Information", "Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez", [ + { + text: "OK", + }, + ]); + } else { + showAlert({ + title: "Information", + message: "Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez", + actions: [ + { + title: "OK", + onPress: () => {}, + backgroundColor: theme.colors.card, + }, + ], + }); + } + } + }, [onDonePressHandler, isOnline]); const [mainLoaded, setMainLoaded] = useState(false); diff --git a/src/views/account/Homeworks/Homeworks.tsx b/src/views/account/Homeworks/Homeworks.tsx index ef995f05..9798d51d 100644 --- a/src/views/account/Homeworks/Homeworks.tsx +++ b/src/views/account/Homeworks/Homeworks.tsx @@ -1,4 +1,4 @@ -import { NativeList, NativeListHeader } from "@/components/Global/NativeComponents"; +import { NativeItem, NativeList, NativeListHeader, NativeText } from "@/components/Global/NativeComponents"; import { useCurrentAccount } from "@/stores/account"; import { useHomeworkStore } from "@/stores/homework"; import { useTheme } from "@react-navigation/native"; @@ -25,11 +25,11 @@ import HomeworksNoHomeworksItem from "./Atoms/NoHomeworks"; import HomeworkItem from "./Atoms/Item"; import { PressableScale } from "react-native-pressable-scale"; import { TouchableOpacity } from "react-native-gesture-handler"; -import { Book, Check, CheckCircle, CheckCircle2, CheckSquare, ChevronLeft, ChevronRight, CircleDashed, CircleDotDashed, Search, X } from "lucide-react-native"; +import { Book, Check, CheckCircle, CheckCircle2, CheckSquare, ChevronLeft, ChevronRight, CircleDashed, CircleDotDashed, Search, WifiOff, X } from "lucide-react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BlurView } from "expo-blur"; -import Reanimated, { Easing, FadeIn, FadeInLeft, FadeInRight, FadeInUp, FadeOut, FadeOutDown, FadeOutLeft, FadeOutRight, FadeOutUp, LinearTransition, ZoomIn, ZoomOut } from "react-native-reanimated"; +import Reanimated, { Easing, FadeIn, FadeInLeft, FadeInRight, FadeInUp, FadeOut, FadeOutDown, FadeOutLeft, FadeOutRight, FadeOutUp, FlipInXDown, LinearTransition, ZoomIn, ZoomOut } from "react-native-reanimated"; import { animPapillon } from "@/utils/ui/animations"; import PapillonSpinner from "@/components/Global/PapillonSpinner"; import AnimatedNumber from "@/components/Global/AnimatedNumber"; @@ -44,6 +44,8 @@ import {NativeSyntheticEvent} from "react-native/Libraries/Types/CoreEventTypes" import {NativeScrollEvent, ScrollViewProps} from "react-native/Libraries/Components/ScrollView/ScrollView"; import {SearchBar} from "react-native-screens"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import NetInfo from "@react-native-community/netinfo"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; type HomeworksPageProps = { index: number; @@ -120,11 +122,19 @@ const WeekView: Screen<"Homeworks"> = ({ route, navigation }) => { return days[new Date(date).getDay()]; }; + const errorTitle = useMemo(() => getErrorTitle(), []); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [loadedWeeks, setLoadedWeeks] = useState([]); + const [isOnline, setIsOnline] = useState(true); + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + const updateHomeworks = useCallback(async (force = false, showRefreshing = true, showLoading = true) => { if(!account) return; @@ -260,6 +270,29 @@ const WeekView: Screen<"Homeworks"> = ({ route, navigation }) => { /> } > + + {!isOnline && + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être obsolètes. + + + + + } + {groupedHomework && Object.keys(groupedHomework).map((day, index) => ( = ({ route, navigation }) => { /> - {loading && + {isOnline && loading && ( = ({ route, navigation }) => { marginLeft: 5, }} /> - } + )} diff --git a/src/views/account/Lessons/Atoms/Page.tsx b/src/views/account/Lessons/Atoms/Page.tsx index 3db344b5..d13732c8 100644 --- a/src/views/account/Lessons/Atoms/Page.tsx +++ b/src/views/account/Lessons/Atoms/Page.tsx @@ -1,6 +1,6 @@ -import { NativeText } from "@/components/Global/NativeComponents"; +import { NativeItem, NativeList, NativeText } from "@/components/Global/NativeComponents"; import { useTheme } from "@react-navigation/native"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Image, Platform, RefreshControl as RNRefreshControl, ScrollView, Text, View } from "react-native"; import { TimetableItem } from "./Item"; import { createNativeWrapper } from "react-native-gesture-handler"; @@ -8,15 +8,18 @@ import { createNativeWrapper } from "react-native-gesture-handler"; import Reanimated, { FadeInDown, FadeOut, - FadeOutUp + FadeOutUp, + FlipInXDown, + LinearTransition } from "react-native-reanimated"; - -import { Activity, Sofa, Utensils } from "lucide-react-native"; +import NetInfo from "@react-native-community/netinfo"; +import { Activity, Sofa, Utensils, WifiOff } from "lucide-react-native"; import LessonsNoCourseItem from "./NoCourse"; import { Timetable, TimetableClass } from "@/services/shared/Timetable"; import { animPapillon } from "@/utils/ui/animations"; import LessonsLoading from "./Loading"; import MissingItem from "@/components/Global/MissingItem"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; const RefreshControl = createNativeWrapper(RNRefreshControl, { disallowInterruption: true, @@ -42,6 +45,14 @@ interface PageProps { } export const Page = ({ day, date, current, paddingTop, refreshAction, loading, weekExists }: PageProps) => { + const errorTitle = useMemo(() => getErrorTitle(), []); + const [isOnline, setIsOnline] = useState(true); + const theme = useTheme(); + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); return ( + {!isOnline && + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être obsolètes. + + + + + } {day && day.length > 0 && day[0].type !== "vacation" && day.map((item, i) => ( diff --git a/src/views/account/News/Document.tsx b/src/views/account/News/Document.tsx index 82ea86cb..d34ba3a0 100644 --- a/src/views/account/News/Document.tsx +++ b/src/views/account/News/Document.tsx @@ -17,7 +17,7 @@ import { MoreHorizontal, } from "lucide-react-native"; import React, { useEffect, useLayoutEffect, useState } from "react"; -import {View, Dimensions, Linking, TouchableOpacity, type GestureResponderEvent, Text, StyleSheet} from "react-native"; +import {View, Dimensions, Linking, TouchableOpacity, type GestureResponderEvent, Text, StyleSheet, Alert, Platform} from "react-native"; import { ScrollView } from "react-native-gesture-handler"; import HTMLView from "react-native-htmlview"; import { PapillonModernHeader } from "@/components/Global/PapillonModernHeader"; @@ -33,6 +33,9 @@ import parse_initials from "@/utils/format/format_pronote_initials"; import { selectColorSeed } from "@/utils/format/select_color_seed"; import { AccountService } from "@/stores/account/types"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useAlert } from "@/providers/AlertProvider"; +import NetInfo from "@react-native-community/netinfo"; + const NewsItem: Screen<"NewsItem"> = ({ route, navigation }) => { const [message, setMessage] = useState(JSON.parse(route.params.message) as Information); @@ -57,6 +60,15 @@ const NewsItem: Screen<"NewsItem"> = ({ route, navigation }) => { }, }); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + const { showAlert } = useAlert(); + useLayoutEffect(() => { navigation.setOptions({ headerTitle: message.title, @@ -103,32 +115,47 @@ const NewsItem: Screen<"NewsItem"> = ({ route, navigation }) => { {message.title === "" ? message.author : message.title} {message.title === "" ? formatDate(message.date.toString()) : message.author} - {!isED && ( - : , - label: message.read - ? "Marquer comme non lu" - : "Marquer comme lu", - onPress: () => { + {!isED && : , + label: message.read ? "Marquer comme non lu" : "Marquer comme lu", + onPress: () => { + if (isOnline) { setNewsRead(account, message, !message.read); - setMessage((prev) => ({ - ...prev, - read: !prev.read, - })); - }, - }, - ]} - > - - - - - )} + message.read = !message.read; + } else { + if (Platform.OS === "ios") { + Alert.alert("Information", "Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez", [ + { + text: "OK", + }, + ]); + } else { + showAlert({ + title: "Information", + message: "Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez", + actions: [ + { + title: "OK", + onPress: () => {}, + backgroundColor: theme.colors.card, + }, + ], + }); + } + } + } + } + ]} + > + + + + } {important && ( diff --git a/src/views/account/News/News.tsx b/src/views/account/News/News.tsx index bbe55936..58cf83ef 100644 --- a/src/views/account/News/News.tsx +++ b/src/views/account/News/News.tsx @@ -1,35 +1,77 @@ -import React, { useCallback, useEffect, useLayoutEffect, useState } from "react"; -import { Image, StyleSheet, FlatList, ListRenderItem, View } from "react-native"; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; +import { + Image, + StyleSheet, + FlatList, + ListRenderItem, + View, +} from "react-native"; import { Screen } from "@/router/helpers/types"; import { updateNewsInCache } from "@/services/news"; import { useNewsStore } from "@/stores/news"; import { useCurrentAccount } from "@/stores/account"; -import { NativeList, NativeListHeader } from "@/components/Global/NativeComponents"; +import { + NativeItem, + NativeList, + NativeListHeader, + NativeText, +} from "@/components/Global/NativeComponents"; import { RefreshControl } from "react-native-gesture-handler"; import { LinearGradient } from "expo-linear-gradient"; import BetaIndicator from "@/components/News/Beta"; import NewsListItem from "./Atoms/Item"; -import Reanimated, { FadeInUp, FadeOut, LinearTransition } from "react-native-reanimated"; +import Reanimated, { + FadeInUp, + FadeOut, + FadeOutUp, + FlipInXDown, + LinearTransition, +} from "react-native-reanimated"; import { useTheme } from "@react-navigation/native"; import { animPapillon } from "@/utils/ui/animations"; import { categorizeMessages } from "@/utils/magic/categorizeMessages"; import TabAnimatedTitle from "@/components/Global/TabAnimatedTitle"; import { protectScreenComponent } from "@/router/helpers/protected-screen"; import MissingItem from "@/components/Global/MissingItem"; -import {Information} from "@/services/shared/Information"; -import {AccountService} from "@/stores/account/types"; +import { Information } from "@/services/shared/Information"; +import { AccountService } from "@/stores/account/types"; +import NetInfo from "@react-native-community/netinfo"; +import { WifiOff } from "lucide-react-native"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; -type NewsItem = Omit & { date: string, important: boolean }; +type NewsItem = Omit & { + date: string; + important: boolean; +}; const NewsScreen: Screen<"News"> = ({ route, navigation }) => { const theme = useTheme(); const account = useCurrentAccount((store) => store.account!); const informations = useNewsStore((store) => store.informations); - + const errorTitle = useMemo(() => getErrorTitle(), []); const [isLoading, setIsLoading] = useState(false); const [importantMessages, setImportantMessages] = useState([]); const [sortedMessages, setSortedMessages] = useState([]); const [isED, setIsED] = useState(false); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + + useEffect(() => { + if (!isOnline && isLoading) { + setIsLoading(false); + } + }, [isOnline, isLoading]); useLayoutEffect(() => { navigation.setOptions({ @@ -37,11 +79,14 @@ const NewsScreen: Screen<"News"> = ({ route, navigation }) => { }); }, [navigation, route.params, theme.colors.text]); - const fetchData = useCallback(async (hidden: boolean = false) => { - if (!hidden) setIsLoading(true); - await updateNewsInCache(account); - setIsLoading(false); - }, [account]); + const fetchData = useCallback( + async (hidden: boolean = false) => { + if (!hidden) setIsLoading(true); + await updateNewsInCache(account); + setIsLoading(false); + }, + [account] + ); useEffect(() => { if (account.service === AccountService.EcoleDirecte) setIsED(true); @@ -91,16 +136,19 @@ const NewsScreen: Screen<"News"> = ({ route, navigation }) => { } }, [informations, account.personalization.MagicNews]); - const renderItem: ListRenderItem = useCallback(({ item, index }) => ( - - ), [navigation, sortedMessages]); + const renderItem: ListRenderItem = useCallback( + ({ item, index }) => ( + + ), + [navigation, sortedMessages] + ); const NoNewsMessage = () => ( = ({ route, navigation }) => { ); @@ -125,6 +175,25 @@ const NewsScreen: Screen<"News"> = ({ route, navigation }) => { } > + {!isOnline && + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Les données affichées peuvent être obsolètes. + + + + + } + {importantMessages.length > 0 && ( = ({ route, navigation }) => { @@ -192,7 +265,7 @@ const styles = StyleSheet.create({ magicIcon: { width: 26, height: 26, - marginRight: 4 + marginRight: 4, }, noNewsText: { textAlign: "center", @@ -202,3 +275,4 @@ const styles = StyleSheet.create({ }); export default protectScreenComponent(NewsScreen); + diff --git a/src/views/account/Restaurant/Menu.tsx b/src/views/account/Restaurant/Menu.tsx index a84670d6..45c73871 100644 --- a/src/views/account/Restaurant/Menu.tsx +++ b/src/views/account/Restaurant/Menu.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useState } from "react"; +import React, { useEffect, useLayoutEffect, useMemo, useState } from "react"; import { View, ScrollView, @@ -13,6 +13,7 @@ import { Clock2, QrCode, Utensils, + WifiOff, } from "lucide-react-native"; import type { Screen } from "@/router/helpers/types"; @@ -30,7 +31,7 @@ import { Balance } from "@/services/shared/Balance"; import { balanceFromExternal } from "@/services/balance"; import MissingItem from "@/components/Global/MissingItem"; import { animPapillon } from "@/utils/ui/animations"; -import Reanimated, { FadeIn, FadeInDown, FadeInUp, FadeOut, FadeOutDown, LinearTransition } from "react-native-reanimated"; +import Reanimated, { FadeIn, FadeInDown, FadeInUp, FadeOut, FadeOutDown, FadeOutUp, FlipInXDown, LinearTransition } from "react-native-reanimated"; import { reservationHistoryFromExternal } from "@/services/reservation-history"; import { qrcodeFromExternal } from "@/services/qrcode"; import { ReservationHistory } from "@/services/shared/ReservationHistory"; @@ -42,6 +43,8 @@ import { LessonsDateModal } from "../Lessons/LessonsHeader"; import { BookingTerminal, BookingDay } from "@/services/shared/Booking"; import { bookDayFromExternal, getBookingsAvailableFromExternal } from "@/services/booking"; import AccountButton from "@/components/Restaurant/AccountButton"; +import NetInfo from "@react-native-community/netinfo"; +import { getErrorTitle } from "@/utils/format/get_papillon_error_title"; const Menu: Screen<"Menu"> = ({ route, navigation }) => { const theme = useTheme(); @@ -62,6 +65,14 @@ const Menu: Screen<"Menu"> = ({ route, navigation }) => { const [isInitialised, setIsInitialised] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); + const [isOnline, setIsOnline] = useState(true); + const errorTitle = useMemo(() => getErrorTitle(), []); + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + const getWeekNumber = (date: Date) => { const firstDayOfYear = new Date(date.getFullYear(), 0, 1); const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000; @@ -194,7 +205,24 @@ const Menu: Screen<"Menu"> = ({ route, navigation }) => { return ( - {!isInitialised ? ( + {!isOnline ? ( + + + }> + + {errorTitle.label} {errorTitle.emoji} + + + Vous êtes hors ligne. Vérifiez votre connexion Internet et réessayez + + + + + ) : !isInitialised ? ( ) : ( <> diff --git a/src/views/login/ServiceSelector.tsx b/src/views/login/ServiceSelector.tsx index 7b0a7421..e56bd944 100644 --- a/src/views/login/ServiceSelector.tsx +++ b/src/views/login/ServiceSelector.tsx @@ -1,5 +1,5 @@ import React, { memo, useEffect, useState } from "react"; -import { Image, View, StyleSheet, Text } from "react-native"; +import { Image, View, StyleSheet, Text, Platform, Alert } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import Reanimated, { LinearTransition, FlipInXDown } from "react-native-reanimated"; @@ -17,6 +17,7 @@ import { Check, School, Undo2 } from "lucide-react-native"; import Constants from "expo-constants"; import { LinearGradient } from "expo-linear-gradient"; import { sr } from "date-fns/locale"; +import NetInfo from "@react-native-community/netinfo"; const ServiceSelector: Screen<"ServiceSelector"> = ({ navigation }) => { const theme = useTheme(); @@ -30,6 +31,14 @@ const ServiceSelector: Screen<"ServiceSelector"> = ({ navigation }) => { const [v6Data, setV6Data] = useState(null); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + return NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + }); + }, []); + useEffect(() => { setTimeout(async () => { const v6Data = await GetV6Data(); @@ -202,12 +211,41 @@ const ServiceSelector: Screen<"ServiceSelector"> = ({ navigation }) => { - srv.name === service)?.login} - /> + {isOnline ? ( + srv.name === service)?.login} + /> + ) : ( + { + if (Platform.OS === "ios") { + Alert.alert("Information", "Pour poursuivre la connexion, vous devez être connecté à Internet. Vérifiez votre connexion Internet et réessayez", [ + { + text: "OK", + }, + ]); + } else { + showAlert({ + title: "Information", + message: "Pour poursuivre la connexion, vous devez être connecté à Internet. Vérifiez votre connexion Internet et réessayez", + actions: [ + { + title: "OK", + onPress: () => {}, + backgroundColor: theme.colors.card, + }, + ], + }); + } + }} + /> + )} {v6Data && v6Data.restore && (