Преглед на файлове

Merge branch 'new-maps' of Viktoriia/nomadmania-app into dev

Viktoriia преди 7 месеца
родител
ревизия
358d152b99
променени са 59 файла, в които са добавени 2254 реда и са изтрити 630 реда
  1. 6 1
      Route.tsx
  2. 5 3
      app.config.ts
  3. 0 0
      assets/geojson/mqp.json
  4. 3 0
      assets/icons/location-sharing.svg
  5. 1 0
      package.json
  6. 1 1
      src/components/ErrorModal/index.tsx
  7. 21 3
      src/components/MenuDrawer/index.tsx
  8. 1 1
      src/components/MenuDrawer/styles.tsx
  9. 1 1
      src/components/TabBarButton/style.tsx
  10. 1 0
      src/constants/secrets.ts
  11. 0 69
      src/database/geojsonService/index.ts
  12. 2 5
      src/database/index.ts
  13. 48 58
      src/database/tilesService/index.ts
  14. 11 1
      src/modules/api/countries/countries-api.tsx
  15. 2 1
      src/modules/api/countries/countries-query-keys.tsx
  16. 1 0
      src/modules/api/countries/queries/index.ts
  17. 17 0
      src/modules/api/countries/queries/use-get-list-countries.tsx
  18. 3 0
      src/modules/api/location/index.ts
  19. 29 0
      src/modules/api/location/location-api.ts
  20. 7 0
      src/modules/api/location/location-query-keys.tsx
  21. 5 0
      src/modules/api/location/queries/index.ts
  22. 17 0
      src/modules/api/location/queries/use-post-get-settings.tsx
  23. 17 0
      src/modules/api/location/queries/use-post-get-users-location.tsx
  24. 17 0
      src/modules/api/location/queries/use-post-is-feature-active.tsx
  25. 19 0
      src/modules/api/location/queries/use-post-set-settings.tsx
  26. 26 0
      src/modules/api/location/queries/use-post-update-location.tsx
  27. 3 0
      src/modules/api/maps/index.ts
  28. 18 0
      src/modules/api/maps/maps-api.ts
  29. 18 0
      src/modules/api/maps/maps-query-keys.tsx
  30. 4 0
      src/modules/api/maps/queries/index.ts
  31. 23 0
      src/modules/api/maps/queries/use-post-get-visited-countries-ids.tsx
  32. 17 0
      src/modules/api/maps/queries/use-post-get-visited-dare-ids.tsx
  33. 23 0
      src/modules/api/maps/queries/use-post-get-visited-regions-ids.tsx
  34. 17 0
      src/modules/api/maps/queries/use-post-get-visited-series-ids.tsx
  35. 11 1
      src/modules/api/myDARE/dare-api.tsx
  36. 2 1
      src/modules/api/myDARE/dare-query-keys.tsx
  37. 1 0
      src/modules/api/myDARE/queries/index.ts
  38. 17 0
      src/modules/api/myDARE/queries/use-get-list-dare.tsx
  39. 1 0
      src/modules/api/regions/queries/index.ts
  40. 17 0
      src/modules/api/regions/queries/use-get-list-regions.tsx
  41. 13 1
      src/modules/api/regions/regions-api.tsx
  42. 2 1
      src/modules/api/regions/regions-query-keys.tsx
  43. 1 0
      src/modules/api/series/queries/index.ts
  44. 17 0
      src/modules/api/series/queries/use-get-icons.tsx
  45. 10 1
      src/modules/api/series/series-api.tsx
  46. 7 3
      src/modules/api/series/series-query-keys.tsx
  47. 247 44
      src/screens/InAppScreens/MapScreen/FilterModal/index.tsx
  48. 32 1
      src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx
  49. 72 39
      src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx
  50. 17 3
      src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx
  51. 6 6
      src/screens/InAppScreens/MapScreen/UniversalSearch/index.tsx
  52. 130 0
      src/screens/InAppScreens/MapScreen/UserItem/index.tsx
  53. 645 254
      src/screens/InAppScreens/MapScreen/index.tsx
  54. 105 52
      src/screens/InAppScreens/MapScreen/style.tsx
  55. 1 1
      src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx
  56. 256 74
      src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx
  57. 226 0
      src/screens/LocationSharingScreen/index.tsx
  58. 32 3
      src/types/api.ts
  59. 2 1
      src/types/navigation.ts

+ 6 - 1
Route.tsx

@@ -94,6 +94,7 @@ import MessagesScreen from 'src/screens/InAppScreens/MessagesScreen';
 import ChatScreen from 'src/screens/InAppScreens/MessagesScreen/ChatScreen';
 import { Splash } from 'src/components/SplashSpinner';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import LocationSharingScreen from 'src/screens/LocationSharingScreen';
 
 enableScreens();
 
@@ -143,7 +144,7 @@ const Route = () => {
     storage.remove('token');
     storage.remove('uid');
     storage.remove('currentUserData');
-    storage.remove('visitedTilesUrl');
+    storage.remove('showNomads');
     storage.remove('filterSettings');
     updateNotificationStatus();
     updateUnreadMessagesCount();
@@ -458,6 +459,10 @@ const Route = () => {
               name={NAVIGATION_PAGES.SYSTEM_NOTIFICATIONS}
               component={SystemNotificationsScreen}
             />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.LOCATION_SHARING}
+              component={LocationSharingScreen}
+            />
           </ScreenStack.Navigator>
         )}
       </BottomTab.Screen>

+ 5 - 3
app.config.ts

@@ -24,7 +24,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
   owner: 'nomadmaniaou',
   scheme: 'nm',
   // Should be updated after every production release (deploy to AppStore/PlayMarket)
-  version: '2.0.23',
+  version: '2.0.24',
   // Should be updated after every dependency change
   runtimeVersion: '1.5',
   orientation: 'portrait',
@@ -36,6 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     MAP_HOST: MAP_HOST,
     GOOGLE_MAP_PLACES_APIKEY: GOOGLE_MAP_PLACES_APIKEY,
     WEBSOCKET_URL: WEBSOCKET_URL,
+    VECTOR_MAP_HOST: env.VECTOR_MAP_HOST,
     eas: {
       projectId: env.EAS_PROJECT_ID
     }
@@ -113,7 +114,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'CAMERA',
       'MODIFY_AUDIO_SETTINGS'
     ],
-    versionCode: 76 // next version submitted to Google Play needs to be higher than that 2.0.23
+    versionCode: 77 // next version submitted to Google Play needs to be higher than that 2.0.24
   },
   plugins: [
     [
@@ -157,6 +158,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       {
         microphonePermission: 'Allow Nomadmania to access your microphone.'
       }
-    ]
+    ],
+    ['@maplibre/maplibre-react-native']
   ]
 });

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
assets/geojson/mqp.json


+ 3 - 0
assets/icons/location-sharing.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4834 5.78294C10.0947 8.23438 7.22303 12.0464 5.91335 13.6854C5.57615 14.1049 4.95109 14.1049 4.61389 13.6854C3.20752 11.9254 0 7.65966 0 5.26362C0 2.35766 2.35766 0 5.26362 0C7.91374 0 10.1079 1.96081 10.4736 4.5101H7.63642C7.31435 4.5101 6.84638 4.51065 6.84613 4.5101C6.56212 3.92025 5.95713 3.50908 5.26362 3.50908C4.30112 3.50908 3.50908 4.30112 3.50908 5.26362C3.50908 6.22612 4.30112 7.01816 5.26362 7.01816C6.04568 7.01816 6.71517 6.49528 6.93874 5.78294H7.63642H10.4834ZM10.4736 4.5101H13.8268L12.367 3.05035C12.1184 2.80175 12.1184 2.39802 12.367 2.14942C12.6156 1.90082 13.0193 1.90082 13.2679 2.14942L15.8136 4.69507C16.0622 4.94366 16.0622 5.34737 15.8136 5.596V5.59797L13.2679 8.14362C13.0193 8.39221 12.6156 8.39221 12.367 8.14362C12.1184 7.89502 12.1184 7.49128 12.367 7.24269L13.8268 5.78294H10.4834C10.5121 5.60194 10.5272 5.42835 10.5272 5.26362C10.5272 5.00779 10.509 4.7562 10.4736 4.5101Z" fill="#0F3F4F"/>
+</svg>

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@expo/config-plugins": "^8.0.8",
+    "@maplibre/maplibre-react-native": "^10.0.0-alpha.22",
     "@react-native-clipboard/clipboard": "^1.14.2",
     "@react-native-community/datetimepicker": "8.0.1",
     "@react-native-community/netinfo": "11.3.1",

+ 1 - 1
src/components/ErrorModal/index.tsx

@@ -27,7 +27,7 @@ export const ErrorModal = () => {
       storage.remove('token');
       storage.remove('uid');
       storage.remove('currentUserData');
-      storage.remove('visitedTilesUrl');
+      storage.remove('showNomads');
       storage.remove('filterSettings');
       updateNotificationStatus();
       updateUnreadMessagesCount();

+ 21 - 3
src/components/MenuDrawer/index.tsx

@@ -16,14 +16,18 @@ import ExitIcon from '../../../assets/icons/exit.svg';
 import UserXMark from '../../../assets/icons/user-xmark.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import BellIcon from 'assets/icons/notifications/bell-solid.svg';
+import SharingIcon from 'assets/icons/location-sharing.svg';
 
 import { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { usePostIsFeatureActiveQuery } from '@api/location';
 
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
   const token = storage.get('token', StoreType.STRING) as string;
+  const { data: isFeatureActive } = usePostIsFeatureActiveQuery(token, !!token);
   const navigation = useNavigation();
   const [modalInfo, setModalInfo] = useState({
     visible: false,
@@ -51,7 +55,7 @@ export const MenuDrawer = (props: any) => {
     storage.remove('token');
     storage.remove('uid');
     storage.remove('currentUserData');
-    storage.remove('visitedTilesUrl');
+    storage.remove('showNomads');
     storage.remove('filterSettings');
     updateNotificationStatus();
     updateUnreadMessagesCount();
@@ -69,7 +73,7 @@ export const MenuDrawer = (props: any) => {
 
   return (
     <>
-      <View style={styles.container}>
+      <SafeAreaView style={styles.container}>
         <View style={{ flex: 1 }}>
           <View style={styles.logoContainer}>
             <Image source={require('../../../assets/logo-ua.png')} style={styles.logo} />
@@ -99,12 +103,26 @@ export const MenuDrawer = (props: any) => {
               red={false}
               buttonFn={() =>
                 // todo: add types
+                // @ts-ignore
                 navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
                   screen: NAVIGATION_PAGES.NOTIFICATIONS
                 })
               }
             />
           )}
+          {isFeatureActive && isFeatureActive.active && (
+            <MenuButton
+              label="Location sharing"
+              icon={<SharingIcon fill={Colors.DARK_BLUE} width={20} height={20} />}
+              red={false}
+              buttonFn={() =>
+                // @ts-ignore
+                navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
+                  screen: NAVIGATION_PAGES.LOCATION_SHARING
+                })
+              }
+            />
+          )}
         </View>
 
         <View style={styles.bottomMenu}>
@@ -141,7 +159,7 @@ export const MenuDrawer = (props: any) => {
             </Text>
           </View>
         </View>
-      </View>
+      </SafeAreaView>
 
       <WarningModal
         isVisible={modalInfo.visible}

+ 1 - 1
src/components/MenuDrawer/styles.tsx

@@ -4,7 +4,7 @@ import { Colors } from 'src/theme';
 export const styles = StyleSheet.create({
   container: {
     flex: 1,
-    margin: '10%'
+    marginHorizontal: '10%'
   },
   logoContainer: {
     flex: 1,

+ 1 - 1
src/components/TabBarButton/style.tsx

@@ -6,7 +6,7 @@ export const styles = StyleSheet.create({
     alignItems: 'center',
     justifyContent: 'center',
     overflow: 'hidden',
-    paddingTop: Platform.OS === 'ios' ? 4 : 0,
+    paddingTop: 0,
   },
   labelStyle: {
     marginTop: 4,

+ 1 - 0
src/constants/secrets.ts

@@ -4,6 +4,7 @@ const { extra } = Constants.manifest2 || Constants.manifest;
 
 export const API_HOST = extra?.API_HOST || Constants?.expoConfig?.extra?.API_HOST;
 export const MAP_HOST = extra?.MAP_HOST || Constants?.expoConfig?.extra?.MAP_HOST;
+export const VECTOR_MAP_HOST = extra?.VECTOR_MAP_HOST || Constants?.expoConfig?.extra?.VECTOR_MAP_HOST;
 
 export const API_URL = `${API_HOST}/webapi`;
 

+ 0 - 69
src/database/geojsonService/index.ts

@@ -1,69 +0,0 @@
-import * as FileSystem from 'expo-file-system';
-import { Asset } from 'expo-asset';
-import { API_HOST } from 'src/constants';
-import axios from 'axios';
-import NetInfo from '@react-native-community/netinfo';
-import staticMQP from '../../../assets/geojson/mqp.json';
-
-const jsonFileName = 'mqp.json';
-const jsonFileUri = `${FileSystem.documentDirectory}${jsonFileName}`;
-
-let jsonData: any = null;
-
-const fetchLocalJsonData = async () => {
-  try {
-    const fileInfo = await FileSystem.getInfoAsync(jsonFileUri);
-    if (fileInfo.exists) {
-      const json = await FileSystem.readAsStringAsync(jsonFileUri);
-      return JSON.parse(json);
-    } else {
-      return staticMQP;
-    }
-  } catch (error) {
-    console.error('Failed to load local JSON data:', error);
-    return null;
-  }
-};
-
-const saveJsonDataToLocal = async (data: any) => {
-  try {
-    await FileSystem.writeAsStringAsync(jsonFileUri, JSON.stringify(data));
-  } catch (error) {
-    console.error('Failed to save JSON data to local storage:', error);
-  }
-};
-
-export const fetchJsonData = async () => {
-  const state = await NetInfo.fetch();
-  if (jsonData) {
-    return jsonData;
-  } else if (state.type === 'cellular') {
-    jsonData = await fetchLocalJsonData();
-    return jsonData;
-  }
-
-  try {
-    const response = await axios.get(`${API_HOST}/static/json/mqp.geojson`);
-    if (response.status !== 200) {
-      console.error('Network response error');
-    }
-    jsonData = response.data;
-    await saveJsonDataToLocal(jsonData);
-  } catch (error) {
-    console.error('Failed to fetch JSON data from server:', error);
-    jsonData = await fetchLocalJsonData();
-  }
-
-  return jsonData;
-};
-
-export const refreshJsonData = async () => {
-  jsonData = null;
-  return await fetchJsonData();
-};
-
-(async () => {
-  jsonData = await fetchJsonData();
-})();
-
-export default jsonData;

+ 2 - 5
src/database/index.ts

@@ -2,7 +2,7 @@ import * as SQLite from 'expo-sqlite/legacy';
 import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
 import { StoreType, storage } from 'src/storage';
 import { fetchLimitedRanking, fetchLpi, fetchInHistory, fetchInMemoriam } from '@api/ranking';
-import { initTilesDownload } from './tilesService';
+import { initOfflineSetup } from './tilesService';
 import { downloadFlags } from './flagsService';
 import { fetchAndSaveAllTypesAndMasters } from './unMastersService';
 import { updateAvatars } from './avatarsService';
@@ -10,7 +10,6 @@ import { fetchAndSaveStatistics } from './statisticsService';
 import { saveTriumphsData } from './triumphsService';
 import { saveSeriesRankingData } from './seriesRankingService';
 import { updateDarePlacesDb, updateNmRegionsDb } from 'src/db';
-import { refreshJsonData } from './geojsonService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
 const lastUpdateDate = storage.get('lastUpdateDate', StoreType.STRING) as string || '1990-01-01';
@@ -39,19 +38,17 @@ export const checkInternetConnection = async (): Promise<NetInfoState> => {
 };
 
 export const syncDataWithServer = async (): Promise<void> => {
-  const userId = storage.get('uid', StoreType.STRING) as string;
   const { isConnected } = await checkInternetConnection();
 
   if (isConnected) {
     console.log('Syncing data with server...');
     processSyncQueue();
-    await refreshJsonData();
     await updateMasterRanking();
     await downloadFlags();
     await updateAvatars(lastUpdateDate);
     await updateNmRegionsDb(lastUpdateNmRegions);
     await updateDarePlacesDb(lastUpdateDarePlaces);
-    await initTilesDownload(userId);
+    await initOfflineSetup();
   }
 };
 

+ 48 - 58
src/database/tilesService/index.ts

@@ -1,73 +1,63 @@
-import * as FileSystem  from 'expo-file-system';
-import { FASTEST_MAP_HOST } from 'src/constants';
+import * as FileSystem from 'expo-file-system';
+import MapLibreGL from '@maplibre/maplibre-react-native';
 
-const baseTilesDir = `${FileSystem.cacheDirectory}tiles/`;
-
-interface TileType {
-  url: string;
-  type: string;
-  maxZoom: number;
-}
+import { VECTOR_MAP_HOST } from 'src/constants';
 
-async function ensureDirExists(dirPath: string): Promise<void> {
-  const dirInfo = await FileSystem.getInfoAsync(dirPath);
-
-  if (!dirInfo.exists) {
-    await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
-  }
-}
+const baseTilesDir = `${FileSystem.cacheDirectory}tiles/`;
+const STYLE_URL = `${VECTOR_MAP_HOST}/nomadmania-maps.json`;
+const PACK_NAME = 'vector-map-pack';
 
-async function downloadTile(z: number, x: number, y: number, tile: TileType): Promise<void> {
+async function deleteCachedTilesIfExist(): Promise<void> {
   try {
-    const tileUrl = `${FASTEST_MAP_HOST}${tile.url}/${z}/${x}/${y}`;
-    const filePath = `${baseTilesDir}${tile.type}/${z}/${x}/${y}`;
-    await FileSystem.downloadAsync(tileUrl, filePath);
-
+    const dirInfo = await FileSystem.getInfoAsync(baseTilesDir, { size: true });
+    if (dirInfo.exists) {
+      await FileSystem.deleteAsync(baseTilesDir, { idempotent: true });
+    }
   } catch (error) {
-    console.error(`Error downloading tile ${z}/${x}/${y} for ${tile.type}:`, error);
+    console.error('Error deleting cached tiles:', error);
   }
 }
 
-async function checkTilesExistence(tileType: TileType): Promise<boolean> {
-  const dirPath = `${baseTilesDir}${tileType.type}/`;
-  const MIN_SIZE_BYTES = 1024 * 512;
+async function setupOfflineRegion(): Promise<void> {
   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);
-        }
+    const bounds: [GeoJSON.Position, GeoJSON.Position] = [
+      [-180, -85],
+      [180, 85]
+    ];
+    const minZoom = 0;
+    const maxZoom = 6;
+
+    const existingPacks = await MapLibreGL.offlineManager.getPacks();
+    const pack = existingPacks.find((pack) => pack.name === PACK_NAME);
+
+    if (pack) {
+      const status = await pack.status();
+      if (status.percentage < 100) {
+        pack.resume();
       }
+    } else {
+      await MapLibreGL.offlineManager.createPack(
+        {
+          name: PACK_NAME,
+          bounds,
+          minZoom,
+          maxZoom,
+          styleURL: STYLE_URL
+        },
+        (offlineRegion, status) => {},
+        (error) => {
+          if (error) {
+            console.error('Error creating offline pack:', error);
+          }
+        }
+      );
     }
+  } catch (error) {
+    console.error('Error setting up offline pack:', error);
   }
 }
 
-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: 4}
-  ];
-
-  for (const type of tileTypes) {
-    await downloadTiles(type);
-  }
+export async function initOfflineSetup(): Promise<void> {
+  await deleteCachedTilesIfExist();
+  await setupOfflineRegion();
 }

+ 11 - 1
src/modules/api/countries/countries-api.tsx

@@ -99,6 +99,15 @@ export interface PostGetUserDataReturn extends ResponseType {
   };
 }
 
+export interface PostGetListCountriesReturn extends ResponseType {
+  data: {
+    id: number;
+    name: string;
+    bbox: any;
+    flag: string;
+  }[];
+}
+
 export const countriesApi = {
   getSlow: (token: string) => request.postForm<PostGetSlowReturn>(API.GET_SLOW, { token }),
   setSlow: (data: PostSetSlow) => request.postForm<ResponseType>(API.SET_SLOW, data),
@@ -126,5 +135,6 @@ export const countriesApi = {
       country
     }),
   getCountryUserData: (id: number, token: string) =>
-    request.postForm<PostGetUserDataReturn>(API.GET_COUNTRY_USER_DATA, { id, token })
+    request.postForm<PostGetUserDataReturn>(API.GET_COUNTRY_USER_DATA, { id, token }),
+  getListCountries: () => request.postForm<PostGetListCountriesReturn>(API.GET_LIST_COUNTRIES)
 };

+ 2 - 1
src/modules/api/countries/countries-query-keys.tsx

@@ -4,5 +4,6 @@ export const countriesQueryKeys = {
   getCountryData: (id: number, token?: string) => ['getCountryData', id, token] as const,
   getUsersFromCountry: () => ['getUsersFromCountry'] as const,
   getUsersWhoVisitedCountry: () => ['getUsersWhoVisitedCountry'] as const,
-  getCountryUserData: () => ['getCountryUserData'] as const
+  getCountryUserData: () => ['getCountryUserData'] as const,
+  getListCountries: () => ['getListCountries'] as const
 };

+ 1 - 0
src/modules/api/countries/queries/index.ts

@@ -4,3 +4,4 @@ export * from './use-post-get-country-screen-data';
 export * from './use-post-get-users-from-country';
 export * from './use-post-get-users-who-visited-country';
 export * from './use-post-get-user-data-country-app';
+export * from './use-get-list-countries';

+ 17 - 0
src/modules/api/countries/queries/use-get-list-countries.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { countriesQueryKeys } from '../countries-query-keys';
+import { countriesApi, type PostGetListCountriesReturn } from '../countries-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetListCountriesQuery = (enabled: boolean) => {
+  return useQuery<PostGetListCountriesReturn, BaseAxiosError>({
+    queryKey: countriesQueryKeys.getListCountries(),
+    queryFn: async () => {
+      const response = await countriesApi.getListCountries();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 3 - 0
src/modules/api/location/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './location-api';
+export * from './location-query-keys';

+ 29 - 0
src/modules/api/location/location-api.ts

@@ -0,0 +1,29 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+import { FeatureCollection } from '@turf/turf';
+
+export interface PostGetSettingsReturn extends ResponseType {
+  sharing: 0 | 1;
+}
+
+export interface PostGetUsersLocationReturn extends ResponseType {
+  geojson: GeoJSON.FeatureCollection;
+}
+
+export interface PostIsFeatureActiveReturn extends ResponseType {
+  active: boolean;
+}
+
+export const locationApi = {
+  getSettings: (token: string) =>
+    request.postForm<PostGetSettingsReturn>(API.GET_LOCATION_SETTINGS, { token }),
+  setSettings: (token: string, sharing: 0 | 1) =>
+    request.postForm<ResponseType>(API.SET_LOCATION_SETTINGS, { token, sharing }),
+  updateLocation: (token: string, lat: number, lng: number) =>
+    request.postForm<ResponseType>(API.UPDATE_LOCATION, { token, lat, lng }),
+  getUsersLocation: (token: string) =>
+    request.postForm<PostGetUsersLocationReturn>(API.GET_USERS_LOCATION, { token }),
+  isFeatureActive: (token: string) =>
+    request.postForm<PostIsFeatureActiveReturn>(API.IS_FEATURE_ACTIVE, { token })
+};

+ 7 - 0
src/modules/api/location/location-query-keys.tsx

@@ -0,0 +1,7 @@
+export const locationQueryKeys = {
+  getSettings: (token: string) => ['location', 'getSettings', token],
+  setSettings: () => ['location', 'setSettings'],
+  updateLocation: () => ['location', 'updateLocation'],
+  getUsersLocation: (token: string) => ['location', 'getUsersLocation', token],
+  isFeatureActive: (token: string) => ['location', 'isFeatureActive', token]
+};

+ 5 - 0
src/modules/api/location/queries/index.ts

@@ -0,0 +1,5 @@
+export * from './use-post-get-settings';
+export * from './use-post-set-settings';
+export * from './use-post-update-location';
+export * from './use-post-get-users-location';
+export * from './use-post-is-feature-active';

+ 17 - 0
src/modules/api/location/queries/use-post-get-settings.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi, type PostGetSettingsReturn } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetSettingsQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetSettingsReturn, BaseAxiosError>({
+    queryKey: locationQueryKeys.getSettings(token),
+    queryFn: async () => {
+      const response = await locationApi.getSettings(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/location/queries/use-post-get-users-location.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi, type PostGetUsersLocationReturn } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetUsersLocationQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetUsersLocationReturn, BaseAxiosError>({
+    queryKey: locationQueryKeys.getUsersLocation(token),
+    queryFn: async () => {
+      const response = await locationApi.getUsersLocation(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/location/queries/use-post-is-feature-active.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi, type PostIsFeatureActiveReturn } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostIsFeatureActiveQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostIsFeatureActiveReturn, BaseAxiosError>({
+    queryKey: locationQueryKeys.isFeatureActive(token),
+    queryFn: async () => {
+      const response = await locationApi.isFeatureActive(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 19 - 0
src/modules/api/location/queries/use-post-set-settings.tsx

@@ -0,0 +1,19 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostSetSettingsMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, { token: string; sharing: 0 | 1 }, ResponseType>(
+    {
+      mutationKey: locationQueryKeys.setSettings(),
+      mutationFn: async (variables) => {
+        const response = await locationApi.setSettings(variables.token, variables.sharing);
+        return response.data;
+      }
+    }
+  );
+};

+ 26 - 0
src/modules/api/location/queries/use-post-update-location.tsx

@@ -0,0 +1,26 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostUpdateLocationMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; lat: number; lng: number },
+    ResponseType
+  >({
+    mutationKey: locationQueryKeys.updateLocation(),
+    mutationFn: async (variables) => {
+      const response = await locationApi.updateLocation(
+        variables.token,
+        variables.lat,
+        variables.lng
+      );
+      return response.data;
+    }
+  });
+};

+ 3 - 0
src/modules/api/maps/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './maps-api';
+export * from './maps-query-keys';

+ 18 - 0
src/modules/api/maps/maps-api.ts

@@ -0,0 +1,18 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostGetVisitedIds extends ResponseType {
+  ids: number[];
+}
+
+export const mapsApi = {
+  getVisitedRegionsIds: (token: string, type: 'in' | 'by', year: number, uid: number) =>
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_REGIONS_IDS, { token, type, year, uid }),
+  getVisitedCountriesIds: (token: string, type: 'in' | 'by', year: number, uid: number) =>
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_COUNTRIES_IDS, { token, type, year, uid }),
+  getVisitedDareIds: (token: string, uid: number) =>
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_DARE_IDS, { token, uid }),
+  getVisitedSeriesIds: (token: string) =>
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_SERIES_IDS, { token })
+};

+ 18 - 0
src/modules/api/maps/maps-query-keys.tsx

@@ -0,0 +1,18 @@
+export const mapsQueryKeys = {
+  getVisitedRegionsIds: (token: string, type: 'in' | 'by', year: number, uid: number) => [
+    'getVisitedRegionsIds',
+    token,
+    type,
+    year,
+    uid
+  ],
+  getVisitedCountriesIds: (token: string, type: 'in' | 'by', year: number, uid: number) => [
+    'getVisitedCountriesIds',
+    token,
+    type,
+    year,
+    uid
+  ],
+  getVisitedDareIds: (token: string, uid: number) => ['getVisitedDareIds', token, uid],
+  getVisitedSeriesIds: (token: string) => ['getVisitedSeriesIds', token]
+};

+ 4 - 0
src/modules/api/maps/queries/index.ts

@@ -0,0 +1,4 @@
+export * from './use-post-get-visited-regions-ids';
+export * from './use-post-get-visited-countries-ids';
+export * from './use-post-get-visited-dare-ids';
+export * from './use-post-get-visited-series-ids';

+ 23 - 0
src/modules/api/maps/queries/use-post-get-visited-countries-ids.tsx

@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { mapsQueryKeys } from '../maps-query-keys';
+import { mapsApi, type PostGetVisitedIds } from '../maps-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetVisitedCountriesIdsQuery = (
+  token: string,
+  type: 'in' | 'by',
+  year: number,
+  uid: number,
+  enabled: boolean
+) => {
+  return useQuery<PostGetVisitedIds, BaseAxiosError>({
+    queryKey: mapsQueryKeys.getVisitedCountriesIds(token, type, year, uid),
+    queryFn: async () => {
+      const response = await mapsApi.getVisitedCountriesIds(token, type, year, uid);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/maps/queries/use-post-get-visited-dare-ids.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { mapsQueryKeys } from '../maps-query-keys';
+import { mapsApi, type PostGetVisitedIds } from '../maps-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetVisitedDareIdsQuery = (token: string, uid: number, enabled: boolean) => {
+  return useQuery<PostGetVisitedIds, BaseAxiosError>({
+    queryKey: mapsQueryKeys.getVisitedDareIds(token, uid),
+    queryFn: async () => {
+      const response = await mapsApi.getVisitedDareIds(token, uid);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 23 - 0
src/modules/api/maps/queries/use-post-get-visited-regions-ids.tsx

@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { mapsQueryKeys } from '../maps-query-keys';
+import { mapsApi, type PostGetVisitedIds } from '../maps-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetVisitedRegionsIdsQuery = (
+  token: string,
+  type: 'in' | 'by',
+  year: number,
+  uid: number,
+  enabled: boolean
+) => {
+  return useQuery<PostGetVisitedIds, BaseAxiosError>({
+    queryKey: mapsQueryKeys.getVisitedRegionsIds(token, type, year, uid),
+    queryFn: async () => {
+      const response = await mapsApi.getVisitedRegionsIds(token, type, year, uid);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/maps/queries/use-post-get-visited-series-ids.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { mapsQueryKeys } from '../maps-query-keys';
+import { mapsApi, type PostGetVisitedIds } from '../maps-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetVisitedSeriesIdsQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetVisitedIds, BaseAxiosError>({
+    queryKey: mapsQueryKeys.getVisitedSeriesIds(token),
+    queryFn: async () => {
+      const response = await mapsApi.getVisitedSeriesIds(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 11 - 1
src/modules/api/myDARE/dare-api.tsx

@@ -28,11 +28,21 @@ export interface PostSetDARERegion {
   visits: 0 | 1;
 }
 
+export interface PostGetListDareReturn extends ResponseType {
+  data: {
+    id: number;
+    name: string;
+    bbox: any;
+    flag: string;
+  }[];
+}
+
 export const dareApi = {
   getMegaregionsDare: (token: string) =>
     request.postForm<PostGetMegaReturn>(API.GET_MEGAREGIONS_DARE, { token }),
   getRegionsDare: (megaregion: number, token: string) =>
     request.postForm<PostGetRegionsDAREReturn>(API.GET_REGIONS_DARE, { megaregion, token }),
   setDARERegion: (data: PostSetDARERegion) =>
-    request.postForm<ResponseType>(API.SET_DARE_REGION, data)
+    request.postForm<ResponseType>(API.SET_DARE_REGION, data),
+  getListDare: () => request.postForm<PostGetListDareReturn>(API.GET_LIST_DARE)
 };

+ 2 - 1
src/modules/api/myDARE/dare-query-keys.tsx

@@ -2,5 +2,6 @@ export const dareQueryKeys = {
   getMegaregionsDare: (token: string) => ['getMegaregionsDare', { token }] as const,
   getRegionsDare: (megaregion: number, token: string) =>
     ['getRegionsDare', { megaregion, token }] as const,
-  setDARERegion: () => ['setDARERegion'] as const
+  setDARERegion: () => ['setDARERegion'] as const,
+  getListDare: () => ['getListDare'] as const
 };

+ 1 - 0
src/modules/api/myDARE/queries/index.ts

@@ -1,3 +1,4 @@
 export * from './use-post-get-megaregions-dare';
 export * from './use-post-get-regions-dare';
 export * from './use-post-set-update-dare';
+export * from './use-get-list-dare';

+ 17 - 0
src/modules/api/myDARE/queries/use-get-list-dare.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { dareQueryKeys } from '../dare-query-keys';
+import { dareApi, type PostGetListDareReturn } from '../dare-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetListDareQuery = (enabled: boolean) => {
+  return useQuery<PostGetListDareReturn, BaseAxiosError>({
+    queryKey: dareQueryKeys.getListDare(),
+    queryFn: async () => {
+      const response = await dareApi.getListDare();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 1 - 0
src/modules/api/regions/queries/index.ts

@@ -6,3 +6,4 @@ export * from './use-post-get-dare-region-data';
 export * from './use-post-get-users-from-region';
 export * from './use-post-get-users-who-visited-region';
 export * from './use-post-get-users-who-visited-dare';
+export * from './use-get-list-regions';

+ 17 - 0
src/modules/api/regions/queries/use-get-list-regions.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { regionQueryKeys } from '../regions-query-keys';
+import { regionsApi, type PostGetListRegionsReturn } from '../regions-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetListRegionsQuery = (enabled: boolean) => {
+  return useQuery<PostGetListRegionsReturn, BaseAxiosError>({
+    queryKey: regionQueryKeys.getListRegions(),
+    queryFn: async () => {
+      const response = await regionsApi.getListRegions();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 13 - 1
src/modules/api/regions/regions-api.tsx

@@ -11,11 +11,13 @@ export interface PostGetUserDataReturn extends ResponseType {
   last_visit_year: boolean;
   no_of_visits: number;
   best_visit_quality: number;
+  bbox: any;
 }
 
 export interface PostGetUserDataDareReturn extends ResponseType {
   visited: boolean;
   first_visit_year: boolean;
+  bbox: any;
 }
 
 export interface PostGetRegionDataReturn extends ResponseType {
@@ -127,6 +129,15 @@ export interface PostGetUsersWhoVisitedDataReturn extends ResponseType {
   };
 }
 
+export interface PostGetListRegionsReturn extends ResponseType {
+  data: {
+    id: number;
+    name: string;
+    bbox: any;
+    flag: string;
+  }[];
+}
+
 export const regionsApi = {
   getRegionsWithFlag: () => request.postForm<PostGetRegionsReturn>(API.GET_REGIONS_WITH_FLAGS),
   getUserData: (region_id: number, token: string) =>
@@ -171,5 +182,6 @@ export const regionsApi = {
       sort,
       age,
       country
-    })
+    }),
+  getListRegions: () => request.postForm<PostGetListRegionsReturn>(API.GET_LIST_REGIONS)
 };

+ 2 - 1
src/modules/api/regions/regions-query-keys.tsx

@@ -6,5 +6,6 @@ export const regionQueryKeys = {
   getDareRegionData: (id: number, token?: string) => ['getNmRegionData', id, token] as const,
   getUsersFromRegion: () => ['getUsersFromRegion'] as const,
   getUsersWhoVisitedRegion: () => ['getUsersWhoVisitedRegion'] as const,
-  getUsersWhoVisitedDare: () => ['getUsersWhoVisitedDare'] as const
+  getUsersWhoVisitedDare: () => ['getUsersWhoVisitedDare'] as const,
+  getListRegions: () => ['getListRegions'] as const
 };

+ 1 - 0
src/modules/api/series/queries/index.ts

@@ -9,3 +9,4 @@ export * from './use-post-get-data-from-point';
 export * from './use-post-get-suggestion-data';
 export * from './use-post-submit-suggestion';
 export * from './use-post-get-list';
+export * from './use-get-icons';

+ 17 - 0
src/modules/api/series/queries/use-get-icons.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSeriesIcons } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetIconsQuery = (enabled: boolean) => {
+  return useQuery<PostGetSeriesIcons, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getIcons(),
+    queryFn: async () => {
+      const response = await seriesApi.getIcons();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 10 - 1
src/modules/api/series/series-api.tsx

@@ -150,6 +150,14 @@ export type SubmitSuggestionTypes = {
   item: number;
 };
 
+export interface PostGetSeriesIcons extends ResponseType {
+  data: {
+    id: number;
+    new_icon_png: string;
+    new_icon_visited_png: string;
+  }[];
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
@@ -185,5 +193,6 @@ export const seriesApi = {
   getSuggestionData: () => request.postForm<PostGetSuggestionData>(API.GET_SUGGESTION_DATA),
   submitSuggestion: (data: SubmitSuggestionTypes) =>
     request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data),
-  getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST)
+  getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST),
+  getIcons: () => request.postForm<PostGetSeriesIcons>(API.GET_ICONS)
 };

+ 7 - 3
src/modules/api/series/series-query-keys.tsx

@@ -2,12 +2,16 @@ export const seriesQueryKeys = {
   fetchSeriesData: () => ['fetchSeriesData'] as const,
   getSeriesGroups: () => ['getSeriesGroups'] as const,
   getSeriesWithGroup: () => ['getSeriesWithGroup'] as const,
-  getItemsForSeries: (token: string, series_id: string) => ['getItemsForSeries', {token, series_id}] as const,
+  getItemsForSeries: (token: string, series_id: string) =>
+    ['getItemsForSeries', { token, series_id }] as const,
   setToggleItem: () => ['setToggleItem'] as const,
   getSeriesGroupsRanking: () => ['getSeriesGroupsRanking'] as const,
-  getSeriesRanking: (id: number, page: number, page_size: number) => ['getSeriesRanking', {id, page, page_size}] as const,
-  getDataFromPoint: (token: string, lat: number, lng: number) => ['getDataFromPoint', {token, lat, lng}] as const,
+  getSeriesRanking: (id: number, page: number, page_size: number) =>
+    ['getSeriesRanking', { id, page, page_size }] as const,
+  getDataFromPoint: (token: string, lat: number, lng: number) =>
+    ['getDataFromPoint', { token, lat, lng }] as const,
   getSuggestionData: () => ['getSuggestionData'] as const,
   submitSuggestion: () => ['submitSuggestion'] as const,
   getList: () => ['getList'] as const,
+  getIcons: () => ['getIcons'] as const
 };

+ 247 - 44
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -1,19 +1,40 @@
 import React, { useEffect, useState } from 'react';
-import { View, Text, TouchableOpacity, Image, Switch, Dimensions } from 'react-native';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Image,
+  Switch,
+  Dimensions,
+  Platform,
+  Linking
+} from 'react-native';
 import ReactModal from 'react-native-modal';
 import { Colors } from 'src/theme';
 import { ModalStyles } from '../../TravellersScreen/Components/styles';
 import { Dropdown, MultiSelect } from 'react-native-searchable-dropdown-kj';
-import { Button } from 'src/components';
+import { Button, WarningModal } from 'src/components';
 import { ButtonVariants } from 'src/types/components';
 import { styles } from './styles';
 import { TabBar, TabView } from 'react-native-tab-view';
 import { usePostGetMapYearsQuery } from '@api/user';
-import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
+import { API_HOST } from 'src/constants';
 import CheckSvg from 'assets/icons/mark.svg';
 import { useGetListQuery } from '@api/series';
 import { RadioButton } from 'react-native-paper';
 import { storage, StoreType } from 'src/storage';
+import moment from 'moment';
+import {
+  usePostGetSettingsQuery,
+  usePostIsFeatureActiveQuery,
+  usePostSetSettingsMutation,
+  usePostUpdateLocationMutation
+} from '@api/location';
+import * as Location from 'expo-location';
+
+import SharingIcon from 'assets/icons/location-sharing.svg';
+import UsersIcon from 'assets/icons/bottom-navigation/travellers.svg';
+import LocationIcon from 'assets/icons/location.svg';
 
 const FilterModal = ({
   isFilterVisible,
@@ -21,28 +42,33 @@ const FilterModal = ({
   tilesTypes,
   tilesType,
   setTilesType,
-  type,
   setType,
   userId,
-  setVisitedTiles,
+  setRegionsFilter,
   setSeriesFilter,
   isPublicView,
-  isLogged = true
+  isLogged = true,
+  showNomads,
+  setShowNomads
 }: {
   isFilterVisible: boolean;
   setIsFilterVisible: (isVisible: boolean) => void;
   tilesTypes: any[];
   tilesType: any;
   setTilesType: (item: any) => void;
-  type: number;
   setType: (type: any) => void;
   userId: number;
-  setVisitedTiles: (tiles: string) => void;
+  setRegionsFilter: (data: { visitedLabel: 'in' | 'by'; year: number }) => void;
   setSeriesFilter?: (filter: any) => void;
   isPublicView: boolean;
   isLogged: boolean;
+  showNomads?: boolean;
+  setShowNomads?: (showNomads: boolean) => void;
 }) => {
   const token = storage.get('token', StoreType.STRING) as string;
+  const { data: locationSettings } = usePostGetSettingsQuery(token, isLogged && !isPublicView);
+  const { mutateAsync: setSettings } = usePostSetSettingsMutation();
+  const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
   const [index, setIndex] = useState(0);
   const [selectedYear, setSelectedYear] = useState<{ label: string; value: number } | null>(null);
   const [allYears, setAllYears] = useState<{ label: string; value: number }[]>([]);
@@ -51,7 +77,8 @@ const FilterModal = ({
     { label: 'visited by', value: 0 },
     { label: 'visited in', value: 1 }
   ];
-  const [routes] = useState([
+  const { data: isFeatureActive } = usePostIsFeatureActiveQuery(token, !!token);
+  const [routes, setRoutes] = useState([
     { key: 'regions', title: 'Travels' },
     { key: 'series', title: 'Series' }
   ]);
@@ -65,6 +92,31 @@ const FilterModal = ({
     ? (storage.get('filterSettings', StoreType.STRING) as string)
     : null;
 
+  const [isSharing, setIsSharing] = useState(false);
+  const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
+  const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
+
+  useEffect(() => {
+    if (isFeatureActive && isFeatureActive.active) {
+      setRoutes([
+        { key: 'regions', title: 'Travels' },
+        { key: 'series', title: 'Series' },
+        { key: 'nomads', title: 'Nomads' }
+      ]);
+    }
+  }, [isFeatureActive]);
+
+  useEffect(() => {
+    const syncSettings = async () => {
+      if (locationSettings) {
+        let { status } = await Location.getForegroundPermissionsAsync();
+        setIsSharing(locationSettings.sharing !== 0 && status === 'granted');
+      }
+    };
+
+    syncSettings();
+  }, [locationSettings]);
+
   useEffect(() => {
     const loadFilterSettings = () => {
       try {
@@ -91,7 +143,7 @@ const FilterModal = ({
     if (isLogged && !isPublicView) {
       try {
         const filterSettings = {
-          type: tilesType.value,
+          type: tilesType.value === 2 ? 'dare' : tilesType.value === 1 ? 'countries' : 'regions',
           tilesType,
           selectedYear,
           selectedVisible,
@@ -136,42 +188,35 @@ const FilterModal = ({
   if (!data && isLogged) return;
 
   const handleApplyFilter = () => {
-    let tileUrl = `${FASTEST_MAP_HOST}/tiles_nm/`;
     if (!isLogged) {
       return;
     }
 
-    if (!selectedYear) {
-      if (tilesType.value === 0) {
-        tileUrl += 'user_visited/' + userId;
-      } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un/' + userId;
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
-      }
-      !isPublicView && storage.set('visitedTilesUrl', tileUrl);
-      setVisitedTiles(tileUrl);
-      return;
-    }
     if (selectedVisible.value === 0) {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_year/' + userId + '/' + selectedYear.value;
+        setRegionsFilter({
+          visitedLabel: 'by',
+          year: selectedYear ? selectedYear.value : moment().year()
+        });
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_year/' + userId + '/' + selectedYear.value;
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
+        setRegionsFilter({
+          visitedLabel: 'by',
+          year: selectedYear ? selectedYear.value : moment().year()
+        });
       }
     } else {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_in_year/' + userId + '/' + selectedYear.value;
+        setRegionsFilter({
+          visitedLabel: 'in',
+          year: selectedYear ? selectedYear.value : moment().year()
+        });
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_in_year/' + userId + '/' + selectedYear.value;
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
+        setRegionsFilter({
+          visitedLabel: 'in',
+          year: selectedYear ? selectedYear.value : moment().year()
+        });
       }
     }
-    !isPublicView && storage.set('visitedTilesUrl', tileUrl);
-    setVisitedTiles(tileUrl);
   };
 
   const handleCloseFilter = () => {
@@ -179,8 +224,8 @@ const FilterModal = ({
     setIsFilterVisible(false);
   };
 
-  const renderScene = ({ route }: { route: any }) => {
-    return route.key === 'regions' ? (
+  const renderRegions = () => {
+    return (
       <View style={styles.sceneContainer}>
         <View style={styles.optionsContainer}>
           <View style={styles.rowWrapper}>
@@ -246,7 +291,9 @@ const FilterModal = ({
             onPress={() => {
               saveFilterSettings();
               handleApplyFilter();
-              setType(tilesType.value);
+              setType(
+                tilesType.value === 2 ? 'dare' : tilesType.value === 1 ? 'countries' : 'regions'
+              );
               handleCloseFilter();
             }}
           />
@@ -256,13 +303,13 @@ const FilterModal = ({
               setTilesType({ label: 'NM regions', value: 0 });
               setSelectedYear(allYears[0]);
               setSelectedVisible({ label: 'visited by', value: 0 });
-              setVisitedTiles(`${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`);
-              setType(0);
+              setRegionsFilter({ visitedLabel: 'by', year: moment().year() });
+              setType('regions');
               if (!isPublicView && isLogged) {
                 storage.set(
                   'filterSettings',
                   JSON.stringify({
-                    type: 0,
+                    type: 'regions',
                     tilesType: { label: 'NM regions', value: 0 },
                     selectedYear: allYears[0],
                     selectedVisible: { label: 'visited by', value: 0 },
@@ -274,10 +321,6 @@ const FilterModal = ({
                     }
                   })
                 );
-                storage.set(
-                  'visitedTilesUrl',
-                  `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`
-                );
               }
             }}
             variant={ButtonVariants.OPACITY}
@@ -286,7 +329,11 @@ const FilterModal = ({
           />
         </View>
       </View>
-    ) : (
+    );
+  };
+
+  const renderSeries = () => {
+    return (
       <View style={styles.sceneContainer}>
         <View style={styles.optionsContainer}>
           <View style={[styles.row, { gap: 8 }]}>
@@ -435,6 +482,162 @@ const FilterModal = ({
     );
   };
 
+  const toggleSettingsSwitch = async () => {
+    if (!isSharing) {
+      handleGetLocation();
+    } else {
+      setSettings({ token, sharing: 0 });
+      setShowNomads && setShowNomads(false);
+      storage.set('showNomads', false);
+      setIsSharing(false);
+    }
+  };
+
+  const handleGetLocation = async () => {
+    let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    } else {
+      setAskLocationVisible(true);
+    }
+  };
+
+  const getLocation = async () => {
+    let currentLocation = await Location.getCurrentPositionAsync({
+      accuracy: Location.Accuracy.Balanced
+    });
+    setSettings({ token, sharing: 1 });
+    setIsSharing(true);
+    updateLocation({
+      token,
+      lat: currentLocation.coords.latitude,
+      lng: currentLocation.coords.longitude
+    });
+  };
+
+  const handleAcceptPermission = async () => {
+    setAskLocationVisible(false);
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    }
+  };
+
+  const toggleNomadsSwitch = () => {
+    setShowNomads && setShowNomads(!showNomads);
+    storage.set('showNomads', !showNomads);
+  };
+
+  const renderNomads = () => {
+    return (
+      <View style={[styles.sceneContainer, { flex: 0 }]}>
+        <View style={styles.textContainer}>
+          <Text style={styles.textWithIcon}>
+            Your location is shared each time you press the{'  '}
+            <View style={styles.icon}>
+              <LocationIcon width={12} height={12} />
+            </View>
+            {'  '}
+            button.
+          </Text>
+          <Text style={styles.text}>Your location is shared with ~250m radius precision.</Text>
+        </View>
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleNomadsSwitch}
+          disabled={!isSharing}
+        >
+          <View style={styles.alignStyle}>
+            <UsersIcon
+              fill={isSharing ? Colors.DARK_BLUE : Colors.LIGHT_GRAY}
+              width={20}
+              height={20}
+            />
+            <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
+              Show nomads
+            </Text>
+          </View>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleNomadsSwitch}
+              value={showNomads}
+              style={{ transform: 'scale(0.8)' }}
+              disabled={!isSharing}
+            />
+          </View>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleSettingsSwitch}
+        >
+          <View style={styles.alignStyle}>
+            <SharingIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+            <Text style={styles.buttonLabel}>Share location</Text>
+          </View>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleSettingsSwitch}
+              value={isSharing}
+              style={{ transform: 'scale(0.8)' }}
+            />
+          </View>
+        </TouchableOpacity>
+        <WarningModal
+          type={'success'}
+          isVisible={askLocationVisible}
+          onClose={() => setAskLocationVisible(false)}
+          action={handleAcceptPermission}
+          message="To use this feature we need your permission to access your location. If you press OK your system will ask you to approve location sharing with NomadMania app."
+        />
+        <WarningModal
+          type={'success'}
+          isVisible={openSettingsVisible}
+          onClose={() => setOpenSettingsVisible(false)}
+          action={() =>
+            Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+          }
+          message="NomadMania app needs location permissions to function properly. Open settings?"
+        />
+      </View>
+    );
+  };
+
+  const renderScene = ({ route }: { route: any }) => {
+    switch (route.key) {
+      case 'regions':
+        return renderRegions();
+      case 'series':
+        return renderSeries();
+      case 'nomads':
+        return renderNomads();
+      default:
+        return null;
+    }
+  };
+
   const isSmallScreen = Dimensions.get('window').width < 383;
 
   return (

+ 32 - 1
src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx

@@ -66,5 +66,36 @@ export const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center'
   },
-  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 }
+  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 },
+  alignStyle: {
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  buttonWrapper: {
+    width: '100%',
+    height: 48
+  },
+  buttonLabel: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(12),
+    fontWeight: '700',
+    marginLeft: 15
+  },
+  textContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', marginBottom: 12 },
+  textWithIcon: { lineHeight: 26, fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
+  text: { fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
+  icon: {
+    backgroundColor: Colors.WHITE,
+    width: 26,
+    height: 26,
+    borderRadius: 13,
+    alignItems: 'center',
+    justifyContent: 'center',
+    shadowColor: 'rgba(0, 0, 0, 0.2)',
+    shadowOffset: { width: 0, height: 2 },
+    shadowRadius: 4,
+    shadowOpacity: 1,
+    elevation: 8
+  }
 });

+ 72 - 39
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -1,58 +1,98 @@
 import { useEffect, useRef } from 'react';
 import { View, Image, Text, TouchableOpacity, Platform } from 'react-native';
-import { Marker, Callout, CalloutSubview, MapMarker } from 'react-native-maps';
-import CustomCallout from '../CustomCallout';
 
 import { styles } from './styles';
-import { ItemSeries } from '../../../../types/map';
 import { Colors } from 'src/theme';
 
 import CheckSvg from 'assets/icons/mark.svg';
+import MapLibreGL, { PointAnnotationRef } from '@maplibre/maplibre-react-native';
 
 const MarkerItem = ({
   marker,
-  iconUrl,
-  coordinate,
-  seriesName,
   toggleSeries,
   token
 }: {
-  marker: ItemSeries;
-  iconUrl: string;
-  coordinate: { latitude: number; longitude: number };
-  seriesName: string;
+  marker: any;
   toggleSeries: (item: any) => void;
   token: string;
 }) => {
-  let markerRef = useRef<MapMarker>(null);
+  const calloutRef = useRef<PointAnnotationRef>(null);
   useEffect(() => {
-    if (markerRef.current && Platform.OS !== 'ios') {
-      markerRef.current?.showCallout();
+    if (Platform.OS === 'android') {
+      calloutRef.current?.refresh();
     }
-  }, [marker.visited]);
-
+  }, [marker]);
   return (
     <>
-      <Marker coordinate={coordinate} tracksViewChanges={false} ref={markerRef}>
-        <View
-          style={[
-            styles.markerContainer,
-            (marker.visited === 1 && token && { backgroundColor: Colors.ORANGE }) || {}
-          ]}
+      {Platform.OS === 'ios' ? (
+        <MapLibreGL.PointAnnotation
+          id="selected_marker_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 1 }}
+        >
+          <View style={styles.customView}>
+            <View style={styles.calloutContainer}>
+              <View style={styles.calloutImageContainer}>
+                <Image
+                  source={{ uri: marker.icon.uri }}
+                  style={styles.calloutImage}
+                  resizeMode="contain"
+                />
+              </View>
+              <View style={styles.calloutTextContainer}>
+                <Text style={styles.seriesName}>{marker.series_name}</Text>
+                <Text style={styles.markerName}>{marker.name}</Text>
+              </View>
+              <TouchableOpacity
+                style={[
+                  styles.calloutButton,
+                  (marker.visited === 1 &&
+                    token && {
+                      backgroundColor: Colors.WHITE,
+                      borderWidth: 1,
+                      borderColor: Colors.BORDER_LIGHT
+                    }) ||
+                    {}
+                ]}
+                onPress={() => toggleSeries(marker)}
+              >
+                {marker?.visited === 1 && token ? (
+                  <View style={styles.completedContainer}>
+                    <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                    <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                      Completed
+                    </Text>
+                  </View>
+                ) : (
+                  <Text style={styles.calloutButtonText}>Mark Completed</Text>
+                )}
+              </TouchableOpacity>
+            </View>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      ) : (
+        <MapLibreGL.PointAnnotation
+          id="selected_marker_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 0.9 }}
+          onSelected={() => toggleSeries(marker)}
+          selected={true}
+          ref={calloutRef}
         >
-          <Image source={{ uri: iconUrl }} style={styles.icon} resizeMode="contain" />
-        </View>
-        {Platform.OS === 'ios' ? (
-          <Callout tooltip style={styles.customView}>
+          <View style={styles.customView}>
             <View style={styles.calloutContainer}>
               <View style={styles.calloutImageContainer}>
-                <Image source={{ uri: iconUrl }} style={styles.calloutImage} resizeMode="contain" />
+                <Image
+                  source={{ uri: marker.icon.uri }}
+                  style={styles.calloutImage}
+                  resizeMode="contain"
+                />
               </View>
               <View style={styles.calloutTextContainer}>
-                <Text style={styles.seriesName}>{seriesName}</Text>
+                <Text style={styles.seriesName}>{marker.series_name}</Text>
                 <Text style={styles.markerName}>{marker.name}</Text>
               </View>
-              <CalloutSubview
+              <TouchableOpacity
                 style={[
                   styles.calloutButton,
                   (marker.visited === 1 &&
@@ -75,18 +115,11 @@ const MarkerItem = ({
                 ) : (
                   <Text style={styles.calloutButtonText}>Mark Completed</Text>
                 )}
-              </CalloutSubview>
+              </TouchableOpacity>
             </View>
-          </Callout>
-        ) : (
-          <CustomCallout
-            marker={marker}
-            toggleSeries={toggleSeries}
-            seriesName={seriesName}
-            token={token}
-          />
-        )}
-      </Marker>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      )}
     </>
   );
 };

+ 17 - 3
src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx

@@ -40,11 +40,25 @@ export const styles = StyleSheet.create({
     borderRadius: 19,
     borderWidth: 2,
     borderColor: Colors.TEXT_GRAY,
-    marginTop: -34
+    marginTop: Platform.OS === 'ios' ? -34 : -4
   },
   calloutImage: {
-    width: 28,
-    height: 28
+    width: 32,
+    height: 32
+  },
+  userImage: {
+    width: 38,
+    height: 38,
+    borderRadius: 19,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  },
+  flag: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
   },
   calloutTextContainer: {
     flex: 1,

+ 6 - 6
src/screens/InAppScreens/MapScreen/UniversalSearch/index.tsx

@@ -23,7 +23,7 @@ const SearchModal = ({
 }: {
   searchVisible: boolean;
   handleCloseModal: () => void;
-  handleFindRegion: (id: number, type: string) => void;
+  handleFindRegion: (id: number, type: 'regions' | 'countries' | 'places') => void;
   index: number;
   searchData: any;
   setIndex: (index: number) => void;
@@ -35,7 +35,10 @@ const SearchModal = ({
     { key: 'regions', title: 'NM regions' },
     { key: 'dare', title: 'DARE places' }
   ]);
-  const [shouldOpenModal, setShouldOpenModal] = useState<{ id: number; type: string } | null>(null);
+  const [shouldOpenModal, setShouldOpenModal] = useState<{
+    id: number;
+    type: 'regions' | 'countries' | 'places';
+  } | null>(null);
   const [warningVisible, setWarningVisible] = useState(false);
 
   const renderItem = ({ item }: { item: any }) => {
@@ -51,10 +54,7 @@ const SearchModal = ({
           } else {
             handleCloseModal();
             navigation.navigate(
-              ...([
-                NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
-                { userId: item.id }
-              ] as never)
+              ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.id }] as never)
             );
           }
         }}

+ 130 - 0
src/screens/InAppScreens/MapScreen/UserItem/index.tsx

@@ -0,0 +1,130 @@
+import { useEffect, useRef } from 'react';
+import { View, Image, Text, TouchableOpacity, Platform } from 'react-native';
+
+import { Colors } from 'src/theme';
+
+import MapLibreGL, { PointAnnotationRef } from '@maplibre/maplibre-react-native';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+import moment from 'moment';
+import { styles } from '../MarkerItem/styles';
+
+const UserItem = ({ marker }: { marker: any }) => {
+  const calloutUserRef = useRef<PointAnnotationRef>(null);
+  const navigation = useNavigation();
+
+  useEffect(() => {
+    if (Platform.OS === 'android') {
+      calloutUserRef.current?.refresh();
+    }
+  }, [marker]);
+
+  const formatDateToLocalTime = (utcDate: string) => {
+    const date = moment.utc(utcDate).local();
+    const now = moment();
+
+    if (now.diff(date, 'days') === 1) {
+      return 'yesterday';
+    }
+
+    if (now.diff(date, 'weeks') === 1) {
+      return 'last week';
+    }
+
+    return date.fromNow();
+  };
+
+  return (
+    <>
+      {Platform.OS === 'ios' ? (
+        <MapLibreGL.PointAnnotation
+          id="selected_user_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 1 }}
+        >
+          <View style={styles.customView}>
+            <View style={styles.calloutContainer}>
+              <View style={[styles.calloutImageContainer, { borderColor: Colors.WHITE }]}>
+                <Image
+                  source={{ uri: marker.avatar.uri }}
+                  style={styles.userImage}
+                  resizeMode="contain"
+                />
+              </View>
+              <View style={styles.calloutTextContainer}>
+                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                  <Text style={styles.seriesName}>
+                    {marker.first_name + ' ' + marker.last_name}
+                  </Text>
+                  <Image source={{ uri: marker.flag.uri }} style={styles.flag} resizeMode="cover" />
+                </View>
+                <Text style={styles.markerName}>
+                  Last seen: {formatDateToLocalTime(marker.last_seen)}
+                </Text>
+              </View>
+              <TouchableOpacity
+                style={[styles.calloutButton]}
+                onPress={() =>
+                  navigation.navigate(
+                    ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: marker.id }] as never)
+                  )
+                }
+              >
+                <Text style={styles.calloutButtonText}>Go to profile</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      ) : (
+        <MapLibreGL.PointAnnotation
+          id="selected_user_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 1.1 }}
+          onSelected={() =>
+            navigation.navigate(
+              ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: marker.id }] as never)
+            )
+          }
+          selected={true}
+          ref={calloutUserRef}
+        >
+          <View style={styles.customView}>
+            <View style={styles.calloutContainer}>
+              <View style={styles.calloutImageContainer}>
+                <Image
+                  source={{ uri: marker.avatar.uri }}
+                  style={styles.userImage}
+                  resizeMode="contain"
+                />
+              </View>
+              <View style={styles.calloutTextContainer}>
+                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                  <Text style={styles.seriesName}>
+                    {marker.first_name + ' ' + marker.last_name}
+                  </Text>
+                  <Image source={{ uri: marker.flag.uri }} style={styles.flag} resizeMode="cover" />
+                </View>
+
+                <Text style={styles.markerName}>
+                  Last seen: {formatDateToLocalTime(marker.last_seen)}
+                </Text>
+              </View>
+              <TouchableOpacity
+                style={[styles.calloutButton]}
+                onPress={() =>
+                  navigation.navigate(
+                    ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: marker.id }] as never)
+                  )
+                }
+              >
+                <Text style={styles.calloutButtonText}>Go to profile</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      )}
+    </>
+  );
+};
+
+export default UserItem;

Файловите разлики са ограничени, защото са твърде много
+ 645 - 254
src/screens/InAppScreens/MapScreen/index.tsx


+ 105 - 52
src/screens/InAppScreens/MapScreen/style.tsx

@@ -1,16 +1,39 @@
-import { StyleSheet, Platform, Dimensions } from 'react-native';
-import { Colors } from '../../../theme';
-import { getFontSize } from 'src/utils';
+import { StyleSheet, Platform } from 'react-native';
+import { Colors } from 'src/theme';
 
 export const styles = StyleSheet.create({
   container: {
-    ...StyleSheet.absoluteFillObject,
-    alignItems: 'center',
-    justifyContent: 'flex-end',
-    paddingTop: 0
+    flex: 1
+  },
+  wrapper: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    alignItems: 'center'
   },
   map: {
-    ...StyleSheet.absoluteFillObject,
+    ...StyleSheet.absoluteFillObject
+  },
+  topRightButton: {
+    top: 52,
+    right: 16
+  },
+  bottomButton: {
+    bottom: Platform.OS == 'android' ? 80 : 100,
+    width: 42,
+    height: 42,
+    borderRadius: 21
+  },
+  bottomLeftButton: {
+    left: 16
+  },
+  bottomRightButton: {
+    right: 16
+  },
+  textClose: {
+    fontSize: 12,
+    color: 'white',
+    fontWeight: '500',
+    lineHeight: 14
   },
   cornerButton: {
     position: 'absolute',
@@ -24,11 +47,11 @@ export const styles = StyleSheet.create({
     shadowColor: 'rgba(33, 37, 41, 0.12)',
     shadowOffset: { width: 0, height: 4 },
     shadowRadius: 8,
-    elevation: 5,
+    elevation: 5
   },
   topLeftButton: {
     top: 52,
-    left: 16,
+    left: 16
   },
   closeLeftButton: {
     backgroundColor: 'rgba(33, 37, 41, 0.78)',
@@ -38,44 +61,14 @@ export const styles = StyleSheet.create({
     height: 36,
     borderRadius: 18,
     flexDirection: 'row',
-    gap: 6,
+    gap: 6
   },
-  topRightButton: {
-    top: 52,
-    right: 16,
-  },
-  bottomButton: {
-    bottom: Platform.OS == 'android' ? 80 : 100,
-    width: 42,
-    height: 42,
-    borderRadius: 21,
-  },
-  bottomLeftButton: {
-    left: 16,
-  },
-  bottomRightButton: {
-    right: 16,
-  },
-  textClose: {
-    fontSize: 12,
-    color: 'white',
-    fontWeight: '500',
-    lineHeight: 14,
-  },
-  location: {
-    width: 18,
-    height: 18,
-    borderRadius: 9,
-    borderColor: 'white',
-    backgroundColor: '#ED9334',
-    shadowColor: "#212529",
-    shadowOffset: {
-      width: 0,
-      height: 4,
-    },
-    shadowOpacity: 0.12,
-    shadowRadius: 8,
-    elevation: 5,
+  avatar: {
+    borderRadius: 48 / 2,
+    width: 48,
+    height: 48,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
   },
   searchContainer: {
     flexDirection: 'row',
@@ -91,11 +84,71 @@ export const styles = StyleSheet.create({
     color: Colors.DARK_BLUE,
     fontWeight: '600'
   },
-  avatar: {
-    borderRadius: 48 / 2,
-    width: 48,
-    height: 48,
+  customView: {
+    width: 200,
+    backgroundColor: 'white',
+    borderRadius: 8,
+    shadowColor: '#212529',
+    shadowOffset: { width: 0, height: 4 },
+    shadowOpacity: 0.12,
+    shadowRadius: 8,
+    elevation: 5
+  },
+  calloutContainer: {
+    alignItems: 'center',
+    paddingVertical: 15,
+    paddingHorizontal: 10
+  },
+  calloutImageContainer: {
+    width: 38,
+    height: 38,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: Colors.WHITE,
+    borderRadius: 19,
     borderWidth: 2,
-    borderColor: Colors.WHITE
+    borderColor: Colors.TEXT_GRAY,
+    marginTop: -34
   },
+  calloutTextContainer: {
+    flex: 1,
+    gap: 4,
+    alignItems: 'center',
+    marginVertical: 10
+  },
+  seriesName: {
+    fontSize: 13,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE,
+    textAlign: 'center'
+  },
+  markerName: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    textAlign: 'center'
+  },
+  calloutButton: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 6,
+    height: 30,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  calloutButtonText: {
+    color: 'white',
+    fontSize: 12,
+    fontWeight: 'bold'
+  },
+  completedContainer: { flexDirection: 'row', alignItems: 'center', gap: 6 },
+  customCallout: {
+    width: 200,
+    backgroundColor: Colors.WHITE,
+    shadowColor: '#212529',
+    shadowOffset: { width: 0, height: 4 },
+    shadowOpacity: 0.12,
+    shadowRadius: 8,
+    elevation: 5
+  }
 });

+ 1 - 1
src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx

@@ -108,7 +108,7 @@ export const EditPersonalInfo = () => {
     storage.remove('token');
     storage.remove('uid');
     storage.remove('currentUserData');
-    storage.remove('visitedTilesUrl');
+    storage.remove('showNomads');
     storage.remove('filterSettings');
     updateNotificationStatus();
     updateUnreadMessagesCount();

+ 256 - 74
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -1,15 +1,13 @@
 import {
   Platform,
   TouchableOpacity,
-  View,
   Image,
-  Animated as Animation,
   Linking,
   TextInput,
-  Dimensions
+  Dimensions,
+  StatusBar
 } from 'react-native';
 import React, { FC, useEffect, useRef, useState } from 'react';
-import MapView, { UrlTile, Marker } from 'react-native-maps';
 import * as Location from 'expo-location';
 import Animated, {
   Easing,
@@ -19,7 +17,7 @@ import Animated, {
 } from 'react-native-reanimated';
 
 import { styles } from './styles';
-import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
+import { API_HOST, VECTOR_MAP_HOST } from 'src/constants';
 import { CommonActions, NavigationProp } from '@react-navigation/native';
 import { AvatarWithInitials, LocationPopup } from 'src/components';
 import { Colors } from 'src/theme';
@@ -34,8 +32,101 @@ import SearchModal from '../../MapScreen/UniversalSearch';
 import { useGetUniversalSearch } from '@api/search';
 import { storage, StoreType } from 'src/storage';
 import { NAVIGATION_PAGES } from 'src/types';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import MapLibreGL, { CameraRef, MapViewRef } from '@maplibre/maplibre-react-native';
+import {
+  usePostGetVisitedCountriesIdsQuery,
+  usePostGetVisitedDareIdsQuery,
+  usePostGetVisitedRegionsIdsQuery
+} from '@api/maps';
+import moment from 'moment';
+
+MapLibreGL.setAccessToken(null);
+
+const generateFilter = (ids: number[]) => {
+  return ids.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
+};
+
+let regions_visited = {
+  id: 'regions_visited',
+  type: 'fill',
+  source: 'regions',
+  'source-layer': 'regions',
+  style: {
+    fillColor: 'rgba(255, 126, 0, 1)',
+    fillOpacity: 0.5,
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
+  },
+  filter: generateFilter([]),
+  maxzoom: 12
+};
+
+let countries_visited = {
+  id: 'countries_visited',
+  type: 'fill',
+  source: 'countries',
+  'source-layer': 'countries',
+  style: {
+    fillColor: 'rgba(255, 126, 0, 1)',
+    fillOpacity: 0.5,
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
+  },
+  filter: generateFilter([]),
+  maxzoom: 12
+};
+
+let dare_visited = {
+  id: 'dare_visited',
+  type: 'fill',
+  source: 'dare',
+  'source-layer': 'dare',
+  style: {
+    fillColor: 'rgba(255, 126, 0, 1)',
+    fillOpacity: 0.5,
+    fillOutlineColor: 'rgba(255, 126, 0, 1)'
+  },
+  filter: generateFilter([]),
+  maxzoom: 12
+};
+
+let regions = {
+  id: 'regions',
+  type: 'fill',
+  source: 'regions',
+  'source-layer': 'regions',
+  style: {
+    fillColor: 'rgba(15, 63, 79, 0)',
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
+  },
+  filter: ['all'],
+  maxzoom: 16
+};
+
+let countries = {
+  id: 'countries',
+  type: 'fill',
+  source: 'countries',
+  'source-layer': 'countries',
+  style: {
+    fillColor: 'rgba(15, 63, 79, 0)',
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
+  },
+  filter: ['all'],
+  maxzoom: 16
+};
 
-const AnimatedMarker = Animation.createAnimatedComponent(Marker);
+let dare = {
+  id: 'dare',
+  type: 'fill',
+  source: 'dare',
+  'source-layer': 'dare',
+  style: {
+    fillColor: 'rgba(14, 80, 109, 0.6)',
+    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+  },
+  filter: ['all'],
+  maxzoom: 16
+};
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -47,12 +138,17 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
   const userId = route.params?.userId;
   const data = route.params?.data;
 
-  const tilesBaseURL = `${FASTEST_MAP_HOST}/tiles_osm`;
-  const gridUrl = `${FASTEST_MAP_HOST}/tiles_nm/grid`;
-  const visitedDefaultTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
+  const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
+  const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
+  const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([]));
+
+  const [regionsFilter, setRegionsFilter] = useState<any>({
+    visitedLabel: 'by',
+    year: moment().year()
+  });
 
-  const mapRef = useRef<MapView>(null);
-  const strokeWidthAnim = useRef(new Animation.Value(2)).current;
+  const mapRef = useRef<MapViewRef>(null);
+  const cameraRef = useRef<CameraRef>(null);
   const [isFilterVisible, setIsFilterVisible] = useState(false);
   const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
   const tilesTypes = [
@@ -60,8 +156,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     { label: 'UN countries', value: 1 },
     { label: 'DARE places', value: 2 }
   ];
-  const [type, setType] = useState(0);
-  const [visitedTiles, setVisitedTiles] = useState(visitedDefaultTiles);
+  const [type, setType] = useState('regions');
   const [location, setLocation] = useState<Location.LocationObjectCoords | null>(null);
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
@@ -74,22 +169,43 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
   const [searchInput, setSearchInput] = useState('');
   const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
 
+  const { data: visitedRegionIds } = usePostGetVisitedRegionsIdsQuery(
+    token,
+    regionsFilter.visitedLabel,
+    regionsFilter.year,
+    +userId,
+    type === 'regions' && !!userId
+  );
+  const { data: visitedCountryIds } = usePostGetVisitedCountriesIdsQuery(
+    token,
+    regionsFilter.visitedLabel,
+    regionsFilter.year,
+    +userId,
+    type === 'countries' && !!userId
+  );
+  const { data: visitedDareIds } = usePostGetVisitedDareIdsQuery(
+    token,
+    +userId,
+    type === 'dare' && !!userId
+  );
+
+  useEffect(() => {
+    if (visitedRegionIds) {
+      setRegionsVisitedFilter(generateFilter(visitedRegionIds.ids));
+    }
+  }, [visitedRegionIds]);
+
   useEffect(() => {
-    Animation.loop(
-      Animation.sequence([
-        Animation.timing(strokeWidthAnim, {
-          toValue: 3,
-          duration: 700,
-          useNativeDriver: false
-        }),
-        Animation.timing(strokeWidthAnim, {
-          toValue: 2,
-          duration: 700,
-          useNativeDriver: false
-        })
-      ])
-    ).start();
-  }, [strokeWidthAnim]);
+    if (visitedCountryIds) {
+      setCountriesVisitedFilter(generateFilter(visitedCountryIds.ids));
+    }
+  }, [visitedCountryIds]);
+
+  useEffect(() => {
+    if (visitedDareIds) {
+      setDareVisitedFilter(generateFilter(visitedDareIds.ids));
+    }
+  }, [visitedDareIds]);
 
   const handleGetLocation = async () => {
     let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
@@ -109,15 +225,12 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     });
     setLocation(currentLocation.coords);
 
-    mapRef.current?.animateToRegion(
-      {
-        latitude: currentLocation.coords.latitude,
-        longitude: currentLocation.coords.longitude,
-        latitudeDelta: 5,
-        longitudeDelta: 5
-      },
-      800
-    );
+    if (currentLocation.coords) {
+      cameraRef.current?.flyTo(
+        [currentLocation.coords.longitude, currentLocation.coords.latitude],
+        1000
+      );
+    }
   };
 
   const handleAcceptPermission = async () => {
@@ -154,19 +267,6 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     setSearchVisible(true);
   };
 
-  const renderMapTiles = (url: string, zIndex: number, opacity = 1) => (
-    <UrlTile
-      key={url}
-      urlTemplate={`${url}/{z}/{x}/{y}`}
-      maximumZ={15}
-      maximumNativeZ={13}
-      shouldReplaceMapContent
-      minimumZ={0}
-      opacity={opacity}
-      zIndex={zIndex}
-    />
-  );
-
   const handleGoBack = () => {
     navigation.goBack();
   };
@@ -199,32 +299,115 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
   };
 
   return (
-    <View style={styles.container}>
-      <MapView
-        initialRegion={{
-          latitude: 0,
-          longitude: 0,
-          latitudeDelta: 180,
-          longitudeDelta: 180
-        }}
+    <SafeAreaView style={{ height: '100%' }}>
+      <StatusBar translucent backgroundColor="transparent" />
+
+      <MapLibreGL.MapView
         ref={mapRef}
-        showsMyLocationButton={false}
-        showsCompass={false}
-        zoomControlEnabled={false}
         style={styles.map}
-        mapType={Platform.OS == 'android' ? 'none' : 'standard'}
-        maxZoomLevel={15}
-        minZoomLevel={0}
+        styleJSON={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
+        rotateEnabled={false}
+        attributionEnabled={false}
       >
-        {renderMapTiles(tilesBaseURL, 1)}
-        {type === 0 && renderMapTiles(gridUrl, 2)}
-        {userId && renderMapTiles(visitedTiles, 2, 0.5)}
+        {type === 'regions' && (
+          <>
+            <MapLibreGL.LineLayer
+              id="nm-regions-line-layer"
+              sourceID={regions.source}
+              sourceLayerID={regions['source-layer']}
+              filter={regions.filter as any}
+              maxZoomLevel={regions.maxzoom}
+              style={{
+                lineColor: 'rgba(14, 80, 109, 1)',
+                lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
+                lineWidthTransition: { duration: 300, delay: 0 }
+              }}
+            />
+            <MapLibreGL.FillLayer
+              id={regions.id}
+              sourceID={regions.source}
+              sourceLayerID={regions['source-layer']}
+              filter={regions.filter as any}
+              style={regions.style}
+              maxZoomLevel={regions.maxzoom}
+              belowLayerID={regions_visited.id}
+            />
+            <MapLibreGL.FillLayer
+              id={regions_visited.id}
+              sourceID={regions_visited.source}
+              sourceLayerID={regions_visited['source-layer']}
+              filter={regionsVisitedFilter as any}
+              style={regions_visited.style}
+              maxZoomLevel={regions_visited.maxzoom}
+              belowLayerID="waterway-name"
+            />
+          </>
+        )}
+        {type === 'countries' && (
+          <>
+            <MapLibreGL.LineLayer
+              id="countries-line-layer"
+              sourceID={countries.source}
+              sourceLayerID={countries['source-layer']}
+              filter={countries.filter as any}
+              maxZoomLevel={countries.maxzoom}
+              style={{
+                lineColor: 'rgba(14, 80, 109, 1)',
+                lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
+                lineWidthTransition: { duration: 300, delay: 0 }
+              }}
+            />
+            <MapLibreGL.FillLayer
+              id={countries.id}
+              sourceID={countries.source}
+              sourceLayerID={countries['source-layer']}
+              filter={countries.filter as any}
+              style={countries.style}
+              maxZoomLevel={countries.maxzoom}
+              belowLayerID={countries_visited.id}
+            />
+            <MapLibreGL.FillLayer
+              id={countries_visited.id}
+              sourceID={countries_visited.source}
+              sourceLayerID={countries_visited['source-layer']}
+              filter={countriesVisitedFilter as any}
+              style={countries_visited.style}
+              maxZoomLevel={countries_visited.maxzoom}
+              belowLayerID="waterway-name"
+            />
+          </>
+        )}
+        {type === 'dare' && (
+          <>
+            <MapLibreGL.FillLayer
+              id={dare.id}
+              sourceID={dare.source}
+              sourceLayerID={dare['source-layer']}
+              filter={dare.filter as any}
+              style={dare.style}
+              maxZoomLevel={dare.maxzoom}
+              belowLayerID={dare_visited.id}
+            />
+            <MapLibreGL.FillLayer
+              id={dare_visited.id}
+              sourceID={dare_visited.source}
+              sourceLayerID={dare_visited['source-layer']}
+              filter={dareVisitedFilter as any}
+              style={dare_visited.style}
+              maxZoomLevel={dare_visited.maxzoom}
+              belowLayerID="waterway-name"
+            />
+          </>
+        )}
+
+        <MapLibreGL.Camera ref={cameraRef} />
         {location && (
-          <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
-            <Animation.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
-          </AnimatedMarker>
+          <MapLibreGL.UserLocation
+            animated={true}
+            showsUserHeadingIndicator={true}
+          ></MapLibreGL.UserLocation>
         )}
-      </MapView>
+      </MapLibreGL.MapView>
 
       {!isExpanded ? (
         <TouchableOpacity
@@ -298,10 +481,9 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         tilesTypes={tilesTypes}
         tilesType={tilesType}
         setTilesType={setTilesType}
-        type={type}
         setType={setType}
         userId={userId}
-        setVisitedTiles={setVisitedTiles}
+        setRegionsFilter={setRegionsFilter}
         isPublicView={true}
         isLogged={true}
       />
@@ -328,7 +510,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         setIndex={setIndex}
         token={token}
       />
-    </View>
+    </SafeAreaView>
   );
 };
 

+ 226 - 0
src/screens/LocationSharingScreen/index.tsx

@@ -0,0 +1,226 @@
+import React, { useEffect, useState } from 'react';
+import {
+  View,
+  Linking,
+  Text,
+  Switch,
+  Platform,
+  TouchableOpacity,
+  AppState,
+  StyleSheet
+} from 'react-native';
+import * as Location from 'expo-location';
+
+import { Header, PageWrapper, WarningModal } from 'src/components';
+import { styles } from 'src/components/MenuButton/style';
+import { StoreType, storage } from 'src/storage';
+import { Colors } from 'src/theme';
+
+import UsersIcon from 'assets/icons/bottom-navigation/travellers.svg';
+import { useFocusEffect } from '@react-navigation/native';
+import {
+  usePostGetSettingsQuery,
+  usePostSetSettingsMutation,
+  usePostUpdateLocationMutation
+} from '@api/location';
+import LocationIcon from 'assets/icons/location.svg';
+
+const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const { data: locationSettings, refetch } = usePostGetSettingsQuery(token, !!token);
+  const { mutateAsync: setSettings } = usePostSetSettingsMutation();
+  const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
+
+  const [initialPermissionStatus, setInitialPermissionStatus] = useState<
+    'granted' | 'denied' | 'undetermined' | null
+  >(null);
+  const [isSharingWithEveryone, setIsSharingWithEveryone] = useState(false);
+  const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
+  const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
+
+  useEffect(() => {
+    const syncSettings = async () => {
+      if (locationSettings) {
+        let { status } = await Location.getForegroundPermissionsAsync();
+        setIsSharingWithEveryone(locationSettings.sharing !== 0 && status === 'granted');
+      }
+    };
+
+    syncSettings();
+  }, [locationSettings]);
+
+  useEffect(() => {
+    const subscription = AppState.addEventListener('change', async (nextAppState) => {
+      if (nextAppState === 'active' && initialPermissionStatus !== null) {
+        const currentStatus = await checkLocationPermissions();
+
+        if (initialPermissionStatus !== 'granted' && currentStatus === 'granted') {
+          setInitialPermissionStatus(currentStatus);
+        } else if (
+          currentStatus !== 'granted' &&
+          (isSharingWithEveryone || initialPermissionStatus === 'granted')
+        ) {
+          setSettings({ token, sharing: 0 });
+          storage.set('showNomads', false);
+          setIsSharingWithEveryone(false);
+        }
+      }
+    });
+
+    return () => {
+      subscription.remove();
+    };
+  }, [initialPermissionStatus]);
+
+  useEffect(() => {
+    const getInitialPermissionsStatus = async () => {
+      const status = await checkLocationPermissions();
+      if (status !== 'granted' && isSharingWithEveryone) {
+        setSettings({ token, sharing: 0 });
+        storage.set('showNomads', false);
+        setIsSharingWithEveryone(false);
+      }
+      setInitialPermissionStatus(status);
+    };
+
+    getInitialPermissionsStatus();
+  }, []);
+
+  useFocusEffect(() => {
+    refetchData();
+  });
+
+  const refetchData = async () => {
+    await refetch();
+  };
+
+  const checkLocationPermissions = async () => {
+    let { status } = await Location.getForegroundPermissionsAsync();
+    return status;
+  };
+
+  const toggleSettingsSwitch = async () => {
+    if (!isSharingWithEveryone) {
+      handleGetLocation();
+    } else {
+      setSettings({ token, sharing: 0 });
+      storage.set('showNomads', false);
+      setIsSharingWithEveryone(false);
+    }
+  };
+
+  const handleGetLocation = async () => {
+    let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    } else {
+      setAskLocationVisible(true);
+    }
+  };
+
+  const getLocation = async () => {
+    let currentLocation = await Location.getCurrentPositionAsync({
+      accuracy: Location.Accuracy.Balanced
+    });
+    setSettings({ token, sharing: 1 });
+    setIsSharingWithEveryone(true);
+    updateLocation({
+      token,
+      lat: currentLocation.coords.latitude,
+      lng: currentLocation.coords.longitude
+    });
+  };
+
+  const handleAcceptPermission = async () => {
+    setAskLocationVisible(false);
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    }
+  };
+
+  return (
+    <PageWrapper>
+      <Header label="Location sharing" />
+      <View style={textStyles.container}>
+        <Text style={textStyles.textWithIcon}>
+          Your location is shared each time you press the{'  '}
+          <View style={textStyles.icon}>
+            <LocationIcon width={12} height={12} />
+          </View>
+          {'  '}
+          button.
+        </Text>
+        <Text style={textStyles.text}>Your location is shared with ~250m radius precision.</Text>
+      </View>
+      <TouchableOpacity
+        style={[
+          styles.alignStyle,
+          styles.buttonWrapper,
+          {
+            justifyContent: 'space-between'
+          }
+        ]}
+        onPress={toggleSettingsSwitch}
+      >
+        <View style={styles.alignStyle}>
+          <UsersIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+          <Text style={styles.buttonLabel}>Share with everyone</Text>
+        </View>
+        <View>
+          <Switch
+            trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+            thumbColor={Colors.WHITE}
+            onValueChange={toggleSettingsSwitch}
+            value={isSharingWithEveryone}
+            style={{ transform: 'scale(0.8)' }}
+          />
+        </View>
+      </TouchableOpacity>
+
+      <WarningModal
+        type={'success'}
+        isVisible={askLocationVisible}
+        onClose={() => setAskLocationVisible(false)}
+        action={handleAcceptPermission}
+        message="To use this feature we need your permission to access your location. If you press OK your system will ask you to approve location sharing with NomadMania app."
+      />
+      <WarningModal
+        type={'success'}
+        isVisible={openSettingsVisible}
+        onClose={() => setOpenSettingsVisible(false)}
+        action={() =>
+          Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+        }
+        message="NomadMania app needs location permissions to function properly. Open settings?"
+      />
+    </PageWrapper>
+  );
+};
+
+const textStyles = StyleSheet.create({
+  container: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', marginBottom: 12 },
+  textWithIcon: { lineHeight: 26, fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
+  text: { fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
+  icon: {
+    backgroundColor: Colors.WHITE,
+    width: 26,
+    height: 26,
+    borderRadius: 13,
+    alignItems: 'center',
+    justifyContent: 'center',
+    shadowColor: 'rgba(0, 0, 0, 0.2)',
+    shadowOffset: { width: 0, height: 2 },
+    shadowRadius: 4,
+    shadowOpacity: 1,
+    elevation: 8
+  }
+});
+
+export default LocationSharingScreen;

+ 32 - 3
src/types/api.ts

@@ -22,7 +22,10 @@ export enum API_ROUTE {
   COUNTRIES = 'countries',
   FIXERS = 'fixers',
   NOTIFICATIONS = 'notifications',
-  CHAT = 'chat'
+  CHAT = 'chat',
+  MAPS = 'maps',
+  DARE = 'dare',
+  LOCATION = 'location',
 }
 
 export enum API_ENDPOINT {
@@ -141,7 +144,20 @@ export enum API_ENDPOINT {
   UNREACT_TO_MESSAGE = 'unreact-to-message',
   GET_BLOCKED = 'get-blocked',
   GET_UNREAD_MESSAGES_PRESENT = 'new-messages-present',
-  GET_LATEST_VERSION = 'latest-version'
+  GET_VISITED_REGIONS_IDS = 'get-visited-regions-ids',
+  GET_VISITED_COUNTRIES_IDS = 'get-visited-countries-ids',
+  GET_VISITED_DARE_IDS = 'get-visited-dare-ids',
+  GET_LIST_REGIONS = 'get-list-regions',
+  GET_LIST_COUNTRIES = 'get-list-countries',
+  GET_LIST_DARE = 'get-list-dare',
+  GET_LATEST_VERSION = 'latest-version',
+  GET_ICONS = 'get-icons',
+  GET_VISITED_SERIES_IDS = 'get-visited-series-ids',
+  GET_LOCATION_SETTINGS = 'get-settings',
+  SET_LOCATION_SETTINGS = 'set-settings',
+  UPDATE_LOCATION = 'update-location',
+  GET_USERS_LOCATION = 'get-users-location',
+  IS_FEATURE_ACTIVE = 'is-feature-active',
 }
 
 export enum API {
@@ -259,7 +275,20 @@ export enum API {
   UNREACT_TO_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.UNREACT_TO_MESSAGE}`,
   GET_BLOCKED = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_BLOCKED}`,
   GET_UNREAD_MESSAGES_PRESENT = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_UNREAD_MESSAGES_PRESENT}`,
-  LATEST_VERSION = `${API_ROUTE.APP}/${API_ENDPOINT.GET_LATEST_VERSION}`
+  GET_VISITED_REGIONS_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_REGIONS_IDS}`,
+  GET_VISITED_COUNTRIES_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_COUNTRIES_IDS}`,
+  GET_VISITED_DARE_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_DARE_IDS}`,
+  GET_LIST_REGIONS = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_LIST_REGIONS}`,
+  GET_LIST_COUNTRIES = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_LIST_COUNTRIES}`,
+  GET_LIST_DARE = `${API_ROUTE.DARE}/${API_ENDPOINT.GET_LIST_DARE}`,
+  LATEST_VERSION = `${API_ROUTE.APP}/${API_ENDPOINT.GET_LATEST_VERSION}`,
+  GET_ICONS = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_ICONS}`,
+  GET_VISITED_SERIES_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_SERIES_IDS}`,
+  GET_LOCATION_SETTINGS = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_LOCATION_SETTINGS}`,
+  SET_LOCATION_SETTINGS = `${API_ROUTE.LOCATION}/${API_ENDPOINT.SET_LOCATION_SETTINGS}`,
+  UPDATE_LOCATION = `${API_ROUTE.LOCATION}/${API_ENDPOINT.UPDATE_LOCATION}`,
+  GET_USERS_LOCATION = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_USERS_LOCATION}`,
+  IS_FEATURE_ACTIVE = `${API_ROUTE.LOCATION}/${API_ENDPOINT.IS_FEATURE_ACTIVE}`,
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 1
src/types/navigation.ts

@@ -69,5 +69,6 @@ export enum NAVIGATION_PAGES {
   SYSTEM_NOTIFICATIONS = 'inAppSystemNotifications',
   IN_APP_MESSAGES_TAB = 'Messages',
   CHATS_LIST = 'inAppChatsList',
-  CHAT = 'inAppChat'
+  CHAT = 'inAppChat',
+  LOCATION_SHARING = 'inAppLocationSharing',
 }

Някои файлове не бяха показани, защото твърде много файлове са промени