浏览代码

triumphs offline mode

Viktoriia 1 年之前
父节点
当前提交
2bdd28c44e

文件差异内容过多而无法显示
+ 275 - 272
package-lock.json


+ 2 - 0
src/database/index.ts

@@ -7,6 +7,7 @@ import { downloadFlags } from './flagsService';
 import { fetchAndSaveAllTypesAndMasters } from './unMastersService';
 import { updateAvatars } from './avatarsService';
 import { fetchAndSaveStatistics } from './statisticsService';
+import { saveTriumphsData } from './triumphsService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
 const lastUpdateDate = storage.get('lastUpdateDate', StoreType.STRING) as string || '1990-01-01';
@@ -86,6 +87,7 @@ const updateMasterRanking = async () => {
 
   await fetchAndSaveStatistics(token);
   await fetchAndSaveAllTypesAndMasters();
+  await saveTriumphsData();
 };
 
 export default setupDatabaseAndSync;

+ 23 - 0
src/database/triumphsService/index.ts

@@ -0,0 +1,23 @@
+import { triumphsApi } from '@api/triumphs';
+import { storage } from 'src/storage';
+
+const NAMESPACE = 'triumphs';
+
+function saveData<T>(key: string, data: T) {
+  const namespacedKey = `${NAMESPACE}:${key}`;
+  const jsonData = JSON.stringify(data);
+  storage.set(namespacedKey, jsonData);
+}
+
+export async function saveTriumphsData() {
+  const response = await triumphsApi.getDates();
+  const last30Dates = response.data.dates.slice(-30);
+  saveData('dates', last30Dates);
+
+  await Promise.all(
+    last30Dates.map(async (date) => {
+      const response = await triumphsApi.getData(date);
+      saveData(`data_${date}`, response.data.triumphs);
+    })
+  );
+}

+ 17 - 5
src/modules/api/triumphs/queries/use-post-get-data.tsx

@@ -1,15 +1,27 @@
 import { useQuery } from '@tanstack/react-query';
 
 import { triumphsQueryKeys } from '../triumphs-query-keys';
-import { triumphsApi, type PostGetDates } from '../triumphs-api';
+import { triumphsApi, type PostGetData, type TriumphsData } from '../triumphs-api';
 
 import type { BaseAxiosError } from '../../../../types';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import { loadData } from 'src/storage';
 
-export const useGetTriumphsDates = (enabled: boolean) => {
-  return useQuery<PostGetDates, BaseAxiosError>({
-    queryKey: triumphsQueryKeys.getDates(),
+export const useGetTriumphsData = (date: string, enabled: boolean) => {
+  const netInfo = useConnection();
+
+  return useQuery<PostGetData, BaseAxiosError>({
+    queryKey: triumphsQueryKeys.getData(date),
     queryFn: async () => {
-      const response = await triumphsApi.getDates();
+      if (!netInfo?.isInternetReachable) {
+        const localData = loadData<TriumphsData[]>('triumphs', `data_${date}`);
+        if (localData) {
+          return { triumphs: localData } as PostGetData;
+        }
+        return { triumphs: [], result: 'ERROR' } as PostGetData;
+      }
+
+      const response = await triumphsApi.getData(date);
       return response.data;
     },
     enabled

+ 17 - 5
src/modules/api/triumphs/queries/use-post-get-dates.tsx

@@ -1,15 +1,27 @@
 import { useQuery } from '@tanstack/react-query';
 
 import { triumphsQueryKeys } from '../triumphs-query-keys';
-import { triumphsApi, type PostGetData } from '../triumphs-api';
+import { triumphsApi, type PostGetDates } from '../triumphs-api';
 
 import type { BaseAxiosError } from '../../../../types';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import { loadData } from 'src/storage';
 
-export const useGetTriumphsData = (date: string, enabled: boolean) => {
-  return useQuery<PostGetData, BaseAxiosError>({
-    queryKey: triumphsQueryKeys.getData(date),
+export const useGetTriumphsDates = (enabled: boolean) => {
+  const netInfo = useConnection();
+
+  return useQuery<PostGetDates, BaseAxiosError>({
+    queryKey: triumphsQueryKeys.getDates(),
     queryFn: async () => {
-      const response = await triumphsApi.getData(date);
+      if (!netInfo?.isInternetReachable) {
+        const localData = loadData<string[] | null>('triumphs', 'dates');
+        if (localData) {
+          return { dates: localData, result: 'OK' } as PostGetDates;
+        }
+        return { dates: [], result: 'ERROR' } as PostGetDates;
+      }
+
+      const response = await triumphsApi.getDates();
       return response.data;
     },
     enabled

+ 12 - 10
src/modules/api/triumphs/triumphs-api.tsx

@@ -6,17 +6,19 @@ export interface PostGetDates extends ResponseType {
   dates: string[];
 }
 
+export interface TriumphsData {
+  user_id: number;
+  name: string;
+  type: string;
+  treshold: number;
+  avatar: string | null;
+  flag: string;
+  text: string;
+  icon: string;
+}
+
 export interface PostGetData extends ResponseType {
-  triumphs: {
-    user_id: number;
-    name: string;
-    type: string;
-    treshold: number;
-    avatar: string | null;
-    flag: string;
-    text: string;
-    icon: string;
-  }[];
+  triumphs: TriumphsData[];
 }
 
 export const triumphsApi = {

+ 14 - 3
src/screens/InAppScreens/TravellersScreen/Components/TriumphItem.tsx

@@ -1,16 +1,27 @@
 import React, { useCallback } from 'react';
 import { View, Image, TouchableOpacity, Text } from 'react-native';
+import * as FileSystem from 'expo-file-system';
 
 import { AvatarWithInitials } from 'src/components';
 import { API_HOST } from 'src/constants';
 import { TriumphsStyles } from './styles';
 import { TriumphsData } from '../utils/types';
+import { useConnection } from 'src/contexts/ConnectionContext';
 
 export const TriumphItem = React.memo(
   ({ item, onPress }: { item: TriumphsData; onPress: (userId: number) => void }) => {
+    const netInfo = useConnection();
     const [firstPart, secondPart] = item.text.split(' reached ');
     const [firstName, lastName] = item.name.split(' ');
 
+    const avatarBaseUri = netInfo?.isInternetReachable
+      ? API_HOST + item.avatar
+      : `${FileSystem.documentDirectory}avatars/${item.user_id}.webp`;
+
+    const flagBaseUri = netInfo?.isInternetReachable
+      ? API_HOST + item.flag
+      : `${FileSystem.documentDirectory}flags/${item.flag.split('/').pop()}`;
+
     const handlePress = useCallback(() => {
       onPress(item.user_id);
     }, [item.user_id, onPress]);
@@ -19,13 +30,13 @@ export const TriumphItem = React.memo(
       <TouchableOpacity onPress={handlePress} style={TriumphsStyles.itemContainer}>
         {item.avatar ? (
           <View>
-            <Image source={{ uri: API_HOST + item.avatar }} style={TriumphsStyles.avatar} />
-            <Image source={{ uri: API_HOST + item.flag }} style={TriumphsStyles.flag} />
+            <Image source={{ uri: avatarBaseUri }} style={TriumphsStyles.avatar} />
+            <Image source={{ uri: flagBaseUri }} style={TriumphsStyles.flag} />
           </View>
         ) : (
           <AvatarWithInitials
             text={`${firstName[0] ?? ''}${lastName[0] ?? ''}`}
-            flag={API_HOST + item.flag}
+            flag={flagBaseUri}
             size={48}
           />
         )}

+ 6 - 2
src/screens/InAppScreens/TravellersScreen/TriumphsScreen/index.tsx

@@ -4,6 +4,7 @@ import moment from 'moment';
 
 import { Header, PageWrapper, WarningModal } from 'src/components';
 import RangeCalendar from 'src/components/Calendars/RangeCalendar';
+import { TriumphItem } from '../Components/TriumphItem';
 
 import { useGetTriumphsDates, useGetTriumphsData } from '@api/triumphs';
 import { TriumphsData } from '../utils/types';
@@ -14,14 +15,13 @@ import { useConnection } from 'src/contexts/ConnectionContext';
 import { StoreType, storage } from 'src/storage';
 import { useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
-import { TriumphItem } from '../Components/TriumphItem';
 
 const TriumphsScreen = () => {
   const navigation = useNavigation();
   const netInfo = useConnection();
   const token = storage.get('token', StoreType.STRING);
   const [selectedDate, setSelectedDate] = useState<string>(moment().format('YYYY-MM-DD'));
-  const { data: dates } = useGetTriumphsDates(true);
+  const { data: dates, refetch } = useGetTriumphsDates(true);
   const { data } = useGetTriumphsData(selectedDate, true);
   const [isCalendarVisible, setIsCalendarVisible] = useState(false);
   const [modalType, setModalType] = useState<string | null>(null);
@@ -32,6 +32,10 @@ const TriumphsScreen = () => {
     }
   }, [dates]);
 
+  useEffect(() => {
+    refetch();
+  }, [netInfo?.isInternetReachable]);
+
   const handlePress = useCallback(
     (userId: number) => {
       if (!netInfo?.isInternetReachable) {

+ 7 - 0
src/storage/mmkv.ts

@@ -28,3 +28,10 @@ export enum StoreType {
   NUMBER = 'number',
   BOOLEAN = 'boolean'
 }
+
+export function loadData<T>(nameKey: string, key: string): T | null {
+  const namespacedKey = `${nameKey}:${key}`;
+  const jsonData = storage.get(namespacedKey, StoreType.STRING) as string;
+
+  return jsonData ? JSON.parse(jsonData) : null;
+}

部分文件因为文件数量过多而无法显示