Browse Source

offline switch for testing

Viktoriia 1 year ago
parent
commit
6d0ac4469a

+ 8 - 2
src/database/index.ts

@@ -1,7 +1,8 @@
 import * as SQLite from 'expo-sqlite';
 import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
-import { storage } from 'src/storage';
+import { StoreType, getOnlineStatus, storage } from 'src/storage';
 import { fetchLimitedRanking, fetchLpi, fetchInHistory, fetchInMemoriam } from '@api/ranking';
+import { initTilesDownload } from './tilesService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
 
@@ -27,11 +28,16 @@ export const checkInternetConnection = async (): Promise<NetInfoState> => {
 };
 
 export const syncDataWithServer = async (): Promise<void> => {
+  const userId = storage.get('uid', StoreType.STRING) as string;
+
   const { isConnected, isInternetReachable } = await checkInternetConnection();
-  if (isConnected && isInternetReachable) {
+  const isOnline = getOnlineStatus();
+  if (isConnected && isInternetReachable && isOnline) {
+    console.log('Syncing data with server...');
     processSyncQueue();
     await updateAvatars();
     await updateMasterRanking();
+    await initTilesDownload(userId);
   }
 };
 

+ 75 - 0
src/database/tilesService/index.ts

@@ -0,0 +1,75 @@
+import * as FileSystem  from 'expo-file-system';
+
+const baseTilesDir = `${FileSystem.cacheDirectory}tiles/`;
+const MAP_HOST = 'https://maps.nomadmania.eu';
+
+interface TileType {
+  url: string;
+  type: string;
+  maxZoom: number;
+}
+
+async function ensureDirExists(dirPath: string): Promise<void> {
+  const dirInfo = await FileSystem.getInfoAsync(dirPath);
+
+  if (!dirInfo.exists) {
+    await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
+  }
+}
+
+async function downloadTile(z: number, x: number, y: number, tile: TileType): Promise<void> {
+  try {
+    const tileUrl = `${MAP_HOST}${tile.url}/${z}/${x}/${y}`;
+    const filePath = `${baseTilesDir}${tile.type}/${z}/${x}/${y}`;
+    await FileSystem.downloadAsync(tileUrl, filePath);
+
+  } catch (error) {
+    console.error(`Error downloading tile ${z}/${x}/${y} for ${tile.type}:`, error);
+  }
+}
+
+async function checkTilesExistence(tileType: TileType): Promise<boolean> {
+  const dirPath = `${baseTilesDir}${tileType.type}/`;
+  const MIN_SIZE_BYTES = 1024 * 512;
+  try {
+    const fileInfo = await FileSystem.getInfoAsync(dirPath, { size: true });
+
+    return fileInfo.exists && fileInfo.size < MIN_SIZE_BYTES;
+  } catch (error) {
+    console.error(`Error fetching directory size: ${dirPath}:`, error);
+    return true;
+  }
+}
+
+async function downloadTiles(tileType: TileType): Promise<void> {
+  await ensureDirExists(`${baseTilesDir}${tileType.type}/`);
+  const tilesExist = await checkTilesExistence(tileType);
+
+  if (tilesExist) {
+    for (let z = 2; z <= tileType.maxZoom; z++) {
+      const maxTile = Math.pow(2, z);
+
+      for (let x = 0; x < maxTile; x++) {
+        const dirPath = `${baseTilesDir}${tileType.type}/${z}/${x}`;
+        await ensureDirExists(dirPath);
+
+        for (let y = 0; y <= maxTile; y++) {
+          await downloadTile(z, x, y, tileType);
+        }
+      }
+    }
+  }
+}
+
+export async function initTilesDownload(userId: string): Promise<void> {
+  let tileTypes: TileType[] = [
+    {url: '/tiles_osm', type: 'background', maxZoom: 5},
+    {url: '/tiles_nm/grid', type: 'grid', maxZoom: 5},
+    {url: '/tiles_nm/regions_mqp', type: 'regions_mqp', maxZoom: 4},
+  ];
+  userId && tileTypes.push({url: `/tiles_nm/user_visited/${userId}`, type: 'user_visited', maxZoom: 3});
+
+  for (const type of tileTypes) {
+    await downloadTiles(type);
+  }
+}

+ 7 - 5
src/screens/InAppScreens/MapScreen/index.tsx

@@ -4,7 +4,7 @@ import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps';
 import * as turf from '@turf/turf';
 import * as FileSystem from 'expo-file-system';
 import * as Location from 'expo-location';
-import { storage, StoreType } from '../../../storage';
+import { getOnlineStatus, storage, StoreType } from '../../../storage';
 
 import MenuIcon from '../../../../assets/icons/menu.svg';
 import SearchIcon from '../../../../assets/icons/search.svg';
@@ -46,7 +46,7 @@ import {
 const MAP_HOST = 'https://maps.nomadmania.eu';
 
 const tilesBaseURL = `${MAP_HOST}/tiles_osm`;
-const localTileDir = `${FileSystem.cacheDirectory}tiles`;
+const localTileDir = `${FileSystem.cacheDirectory}tiles/background`;
 
 const gridUrl = `${MAP_HOST}/tiles_nm/grid`;
 const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
@@ -61,6 +61,7 @@ const AnimatedMarker = Animated.createAnimatedComponent(Marker);
 const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const userId = storage.get('uid', StoreType.STRING);
   const token = storage.get('token', StoreType.STRING);
+  const isOnline = getOnlineStatus();
 
   const { mutateAsync } = fetchSeriesData();
 
@@ -119,11 +120,11 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
   useEffect(() => {
     const unsubscribe = NetInfo.addEventListener((state) => {
-      setIsConnected(state.isConnected);
+      setIsConnected(isOnline as boolean);
     });
 
     return () => unsubscribe();
-  }, []);
+  }, [isOnline]);
 
   useEffect(() => {
     (async () => {
@@ -153,6 +154,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     latitudeDelta: any;
     longitudeDelta?: any;
   }) => {
+    if (!isOnline) return;
     const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta);
 
     if (cancelTokenRef.current) {
@@ -186,7 +188,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
       (region: { properties: { id: any } }) => region.properties.id
     );
 
-    await mutateAsync(
+    isOnline && await mutateAsync(
       { regions: JSON.stringify(regionIds), token: String(token) },
       {
         onSuccess: (data) => {

+ 24 - 1
src/screens/InAppScreens/ProfileScreen/Settings/index.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 
 import { Header, PageWrapper, MenuButton } from '../../../../components';
 import { NAVIGATION_PAGES } from '../../../../types';
@@ -15,6 +15,8 @@ import ExitIcon from '../../../../../assets/icons/exit.svg';
 import UserXMark from '../../../../../assets/icons/user-xmark.svg';
 
 import type { MenuButtonType } from '../../../../types/components';
+import { Switch, Text, View } from 'react-native';
+import { getOnlineStatus, storage } from 'src/storage';
 
 const buttons: MenuButtonType[] = [
   {
@@ -61,6 +63,16 @@ const buttons: MenuButtonType[] = [
 ];
 
 const Settings = () => {
+  const [isEnabled, setIsEnabled] = useState(Boolean(getOnlineStatus()));
+
+  useEffect(() => {
+    storage.set('online', isEnabled);
+  }, [isEnabled]);
+
+  const toggleSwitch = () => {
+    setIsEnabled(!isEnabled);
+  };
+
   return (
     <PageWrapper>
       <Header label={'Settings'} />
@@ -72,6 +84,17 @@ const Settings = () => {
           buttonFn={button.buttonFn}
         />
       ))}
+      <View style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 }}>
+        <Text style={{ color: Colors.DARK_BLUE, fontSize: 16, fontWeight: 'bold' }}>
+          Offline mode
+        </Text>
+        <Switch
+          trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.RED }}
+          thumbColor={Colors.WHITE}
+          onValueChange={toggleSwitch}
+          value={!isEnabled}
+        />
+      </View>
     </PageWrapper>
   );
 };

+ 4 - 4
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -139,7 +139,7 @@ const PersonalInfo: FC<PersonalInfoProps> = ({ data }) => {
   return (
     <View style={{ marginTop: 20, gap: 20 }}>
       <InfoItem inline={true} title={'RANKING'}>
-        {data.scores?.map((data) => {
+        {data?.scores?.map((data) => {
           if (!data.score) return null;
           return (
             <View
@@ -152,7 +152,7 @@ const PersonalInfo: FC<PersonalInfoProps> = ({ data }) => {
         })}
       </InfoItem>
       <InfoItem inline={true} title={'SERIES BADGES'}>
-        {data.series.map((data) => (
+        {data?.series?.map((data) => (
           <View style={{ display: 'flex', flexDirection: 'column', gap: 5, alignItems: 'center' }}>
             <Image source={{ uri: API_HOST + data.icon_png }} style={{ width: 28, height: 28 }} />
             <Text style={[styles.headerText, { flex: 0 }]}>{data.score}</Text>
@@ -177,14 +177,14 @@ const PersonalInfo: FC<PersonalInfoProps> = ({ data }) => {
         <Text style={[styles.titleText, { flex: 0 }]}>{data.bio}</Text>
       </InfoItem>
       <InfoItem title={'SOCIAL LINKS'}>
-        <View style={{ display: 'flex', flexDirection: 'row', gap: 15, alignItems: 'center' }}>
+        {/* <View style={{ display: 'flex', flexDirection: 'row', gap: 15, alignItems: 'center' }}>
           {data.links.f?.link ? <IconFacebook fill={Colors.DARK_BLUE} /> : null}
           {data.links.i?.link ? <IconInstagram fill={Colors.DARK_BLUE} /> : null}
           {data.links.t?.link ? <IconTwitter fill={Colors.DARK_BLUE} /> : null}
           {data.links.y?.link ? <IconYouTube fill={Colors.DARK_BLUE} /> : null}
           {data.links.www?.link ? <IconGlobe fill={Colors.DARK_BLUE} /> : null}
           {data.links.other?.link ? <IconLink fill={Colors.DARK_BLUE} /> : null}
-        </View>
+        </View> */}
       </InfoItem>
     </View>
   );

+ 5 - 3
src/screens/InAppScreens/TravellersScreen/Components/Profile.tsx

@@ -10,6 +10,7 @@ import { Colors } from '../../../../theme';
 import TickIcon from '../../../../../assets/icons/tick.svg';
 import UNIcon from '../../../../../assets/icons/un_icon.svg';
 import NMIcon from '../../../../../assets/icons/nm_icon.svg';
+import { getOnlineStatus } from 'src/storage';
 
 type Props = {
   avatar: string;
@@ -36,6 +37,7 @@ export const Profile: FC<Props> = ({
   tbt_score
 }) => {
   const scoreNames = ['NM1301', 'DARE', 'UN', 'UN+', 'TCC', 'YES', 'SLOW', 'WHS', 'KYE'];
+  const isOnline = getOnlineStatus();
 
   return (
     <>
@@ -46,7 +48,7 @@ export const Profile: FC<Props> = ({
         <View>
           <Image
             style={{ borderRadius: 24, width: 48, height: 48 }}
-            source={{ uri: API_HOST + '/img/avatars/' + avatar }}
+            source={{ uri: isOnline ? API_HOST + '/img/avatars/' + avatar : '' }}
           />
         </View>
         <View style={{ gap: 5, width: '75%' }}>
@@ -68,12 +70,12 @@ export const Profile: FC<Props> = ({
                 Age: {date_of_birth}
               </Text>
               <Image
-                source={{ uri: API_HOST + '/img/flags_new/' + homebase_flag }}
+                source={{ uri: isOnline ? API_HOST + '/img/flags_new/' + homebase_flag : '' }}
                 style={styles.countryFlag}
               />
               {homebase2_flag ? (
                 <Image
-                  source={{ uri: API_HOST + '/img/flags_new/' + homebase2_flag }}
+                  source={{ uri: isOnline ? API_HOST + '/img/flags_new/' + homebase2_flag : '' }}
                   style={[styles.countryFlag, { marginLeft: -15 }]}
                 />
               ) : null}

+ 8 - 6
src/screens/InAppScreens/TravellersScreen/MasterRankingScreen/index.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import { Header, PageWrapper } from '../../../../components';
-import { storage, StoreType } from '../../../../storage';
+import { getOnlineStatus, storage, StoreType } from '../../../../storage';
 
 import { Profile } from '../Components/Profile';
 
@@ -19,11 +19,13 @@ const MasterRankingScreen = () => {
   }, []);
 
   const getFullRanking = async () => {
-    await mutateAsync(undefined, {
-      onSuccess: (data) => {
-        setMasterRanking(data.data);
-      }
-    });
+    if (getOnlineStatus()) {
+      await mutateAsync(undefined, {
+        onSuccess: (data) => {
+          setMasterRanking(data.data);
+        }
+      });
+    }
   };
 
   return (

+ 4 - 0
src/storage/mmkv.ts

@@ -28,3 +28,7 @@ export enum StoreType {
   NUMBER = 'number',
   BOOLEAN = 'boolean'
 }
+
+export const getOnlineStatus = () => {
+  return storage.get('online', StoreType.BOOLEAN) ?? true;
+};