Viktoriia пре 7 месеци
родитељ
комит
642df3af21

+ 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>

+ 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 - 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,

+ 3 - 4
src/database/tilesService/index.ts

@@ -1,12 +1,11 @@
 import * as FileSystem from 'expo-file-system';
 import MapLibreGL from '@maplibre/maplibre-react-native';
 
-import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
+import { VECTOR_MAP_HOST } from 'src/constants';
 
 const baseTilesDir = `${FileSystem.cacheDirectory}tiles/`;
-// TO DO VECTOR_MAP_HOST
-const STYLE_URL = `${API_HOST}/omt/app.json`;
-const PACK_NAME = 'global-map-pack';
+const STYLE_URL = `${VECTOR_MAP_HOST}/nomadmania-maps.json`;
+const PACK_NAME = 'vector-map-pack';
 
 async function deleteCachedTilesIfExist(): Promise<void> {
   try {

+ 41 - 43
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -18,7 +18,7 @@ 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';
@@ -26,11 +26,16 @@ 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,
   setIsFilterVisible,
@@ -72,10 +77,10 @@ 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' },
-    { key: 'nomads', title: 'Nomads' }
+    { key: 'series', title: 'Series' }
   ]);
   const { data } = usePostGetMapYearsQuery(token as string, userId, isLogged ? true : false);
   const { data: seriesList } = useGetListQuery(true);
@@ -91,6 +96,16 @@ const FilterModal = ({
   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) {
@@ -173,56 +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);
-      return;
-    }
     if (selectedVisible.value === 0) {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
           visitedLabel: 'by',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
           visitedLabel: 'by',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
       }
     } else {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_in_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
           visitedLabel: 'in',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_in_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
           visitedLabel: 'in',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
       }
     }
-    !isPublicView && storage.set('visitedTilesUrl', tileUrl);
   };
 
   const handleCloseFilter = () => {
@@ -327,10 +321,6 @@ const FilterModal = ({
                     }
                   })
                 );
-                storage.set(
-                  'visitedTilesUrl',
-                  `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`
-                );
               }
             }}
             variant={ButtonVariants.OPACITY}
@@ -519,14 +509,7 @@ const FilterModal = ({
     let currentLocation = await Location.getCurrentPositionAsync({
       accuracy: Location.Accuracy.Balanced
     });
-    setSettings(
-      { token, sharing: 1 },
-      {
-        onSuccess: (res) => {
-          console.log('Settings updated', res);
-        }
-      }
-    );
+    setSettings({ token, sharing: 1 });
     setIsSharing(true);
     updateLocation({
       token,
@@ -554,6 +537,17 @@ const FilterModal = ({
   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,
@@ -566,7 +560,11 @@ const FilterModal = ({
           disabled={!isSharing}
         >
           <View style={styles.alignStyle}>
-            {/* <BellIcon fill={Colors.DARK_BLUE} width={20} height={20} /> */}
+            <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>
@@ -594,7 +592,7 @@ const FilterModal = ({
           onPress={toggleSettingsSwitch}
         >
           <View style={styles.alignStyle}>
-            {/* <BellIcon fill={Colors.DARK_BLUE} width={20} height={20} /> */}
+            <SharingIcon fill={Colors.DARK_BLUE} width={20} height={20} />
             <Text style={styles.buttonLabel}>Share location</Text>
           </View>
           <View>

+ 16 - 0
src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx

@@ -81,5 +81,21 @@ export const styles = StyleSheet.create({
     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
   }
 });

+ 14 - 0
src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx

@@ -46,6 +46,20 @@ export const styles = StyleSheet.create({
     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,
     gap: 4,

+ 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;

+ 147 - 93
src/screens/InAppScreens/MapScreen/index.tsx

@@ -64,7 +64,12 @@ import FilterModal from './FilterModal';
 import { useGetListDareQuery } from '@api/myDARE';
 import { useGetIconsQuery, usePostSetToggleItem } from '@api/series';
 import MarkerItem from './MarkerItem';
-import { usePostGetUsersLocationQuery, usePostUpdateLocationMutation } from '@api/location';
+import {
+  usePostGetSettingsQuery,
+  usePostGetUsersLocationQuery,
+  usePostUpdateLocationMutation
+} from '@api/location';
+import UserItem from './UserItem';
 
 MapLibreGL.setAccessToken(null);
 MapLibreGL.Logger.setLogLevel('error');
@@ -82,7 +87,7 @@ let regions_visited = {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.6,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: generateFilter([]),
   maxzoom: 10
@@ -96,7 +101,7 @@ let countries_visited = {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.6,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: generateFilter([]),
   maxzoom: 10
@@ -122,18 +127,7 @@ let regions = {
   'source-layer': 'regions',
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
-  },
-  paint: {
-    'line-width': {
-      base: 0.2,
-      stops: [
-        [0, 0.2],
-        [4, 1],
-        [5, 1.5],
-        [12, 3]
-      ]
-    },
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: ['all'],
   maxzoom: 16
@@ -146,18 +140,7 @@ let countries = {
   'source-layer': 'countries',
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
-  },
-  paint: {
-    'line-width': {
-      base: 0.2,
-      stops: [
-        [0, 0.2],
-        [4, 1],
-        [5, 1.5],
-        [12, 3]
-      ]
-    }
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: ['all'],
   maxzoom: 16
@@ -208,7 +191,7 @@ let series_layer = {
     'text-offset': [0, 0.6],
     'text-padding': 2,
     'text-size': 12,
-    'visibility': 'visible',
+    visibility: 'visible',
     'text-optional': true,
     'text-ignore-placement': false,
     'text-allow-overlap': false
@@ -242,7 +225,7 @@ let series_visited = {
     'text-offset': [0, 0.6],
     'text-padding': 2,
     'text-size': 12,
-    'visibility': 'visible',
+    visibility: 'visible',
     'text-optional': true,
     'text-ignore-placement': false,
     'text-allow-overlap': false
@@ -291,6 +274,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [showNomads, setShowNomads] = useState(
     (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false
   );
+  const { data: locationSettings, refetch } = usePostGetSettingsQuery(token, !!token);
   const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
   const { data: visitedRegionIds, refetch: refetchVisitedRegions } =
     usePostGetVisitedRegionsIdsQuery(
@@ -373,60 +357,62 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [images, setImages] = useState<any>({});
   const { mutateAsync: updateSeriesItem } = usePostSetToggleItem();
   const [nomads, setNomads] = useState<GeoJSON.FeatureCollection | null>(null);
-  const { data: usersLocation } = usePostGetUsersLocationQuery(
+  const { data: usersLocation, refetch: refetchUsersLocation } = usePostGetUsersLocationQuery(
     token,
     !!token && showNomads && Boolean(location)
   );
+  const [selectedUser, setSelectedUser] = useState<any>(null);
 
   useEffect(() => {
     if (!showNomads) {
       setNomads(null);
+    } else {
+      refetchUsersLocation();
     }
   }, [showNomads]);
 
   useEffect(() => {
     if (usersLocation) {
-      console.log('usersLocation', usersLocation);
       setNomads(usersLocation.geojson);
     }
   }, [usersLocation]);
 
   useEffect(() => {
-    let loadedImages: any = {};
-
     if (seriesIcons) {
-      seriesIcons.data.forEach(async (icon) => {
+      let loadedSeriesImages: any = {};
+
+      seriesIcons.data.forEach((icon) => {
         const id = icon.id;
         const img = API_HOST + '/static/img/series_new2/' + icon.new_icon_png;
         const imgVisited = API_HOST + '/static/img/series_new2/' + icon.new_icon_visited_png;
-        if (!img || !imgVisited) return;
-        try {
-          const iconImage = { uri: img };
-          const visitedIconImage = { uri: imgVisited };
-
-          loadedImages[id] = iconImage;
-          loadedImages[`${id}v`] = visitedIconImage;
-        } catch (error) {
-          console.error(`Error loading icon for series_id ${id}:`, error);
+        if (img && imgVisited) {
+          loadedSeriesImages[id] = { uri: img };
+          loadedSeriesImages[`${id}v`] = { uri: imgVisited };
         }
       });
+
+      setImages((prevImages: any) => ({ ...prevImages, ...loadedSeriesImages }));
     }
+  }, [seriesIcons]);
 
+  useEffect(() => {
     if (nomads && nomads.features) {
+      let loadedNomadsImages: any = {};
+
       nomads.features.forEach((feature) => {
         const user_id = `user_${feature.properties?.id}`;
         const avatarUrl = `${API_HOST}${feature.properties?.avatar}`;
         if (avatarUrl) {
-          loadedImages[user_id] = { uri: avatarUrl };
+          loadedNomadsImages[user_id] = { uri: avatarUrl };
           if (feature.properties) {
             feature.properties.icon_key = user_id;
           }
         }
       });
-    }
 
-    setImages(loadedImages);
-  }, [nomads, seriesIcons]);
+      setImages((prevImages: any) => ({ ...prevImages, ...loadedNomadsImages }));
+    }
+  }, [nomads]);
 
   useEffect(() => {
     const loadDatabases = async () => {
@@ -554,12 +540,21 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   }, [selectedRegion]);
 
+  useFocusEffect(
+    useCallback(() => {
+      if (token) {
+        refetch();
+      }
+    }, [])
+  );
+
   useEffect(() => {
     (async () => {
       let { status } = await Location.getForegroundPermissionsAsync();
-      if (status !== 'granted') {
+      if (status !== 'granted' || !token || !locationSettings || locationSettings.sharing === 0) {
         setShowNomads(false);
         storage.set('showNomads', false);
+        setNomads(null);
         return;
       }
 
@@ -572,8 +567,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         lat: currentLocation.coords.latitude,
         lng: currentLocation.coords.longitude
       });
+      if (showNomads && token) {
+        refetchUsersLocation();
+      }
     })();
-  }, []);
+  }, [locationSettings]);
 
   useEffect(() => {
     const currentYear = moment().year();
@@ -657,7 +655,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   const onMapPress = async (event: any) => {
     if (!mapRef.current) return;
-    if (selectedMarker) {
+    if (selectedMarker || selectedUser) {
       closeCallout();
       return;
     }
@@ -692,13 +690,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         if (tableName === 'regions') {
           token
             ? await mutateUserData(
-              { region_id: +foundRegion, token: String(token) },
-              {
-                onSuccess: (data) => {
-                  setUserData({ type: 'nm', id: +foundRegion, ...data });
+                { region_id: +foundRegion, token: String(token) },
+                {
+                  onSuccess: (data) => {
+                    setUserData({ type: 'nm', id: +foundRegion, ...data });
+                  }
                 }
-              }
-            )
+              )
             : setUserData({ type: 'nm', id: +foundRegion });
           if (regionsList) {
             const region = regionsList.data.find((region) => region.id === +foundRegion);
@@ -715,13 +713,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         } else if (tableName === 'countries') {
           token
             ? await mutateCountriesData(
-              { id: +foundRegion, token },
-              {
-                onSuccess: (data) => {
-                  setUserData({ type: 'countries', id: +foundRegion, ...data.data });
+                { id: +foundRegion, token },
+                {
+                  onSuccess: (data) => {
+                    setUserData({ type: 'countries', id: +foundRegion, ...data.data });
+                  }
                 }
-              }
-            )
+              )
             : setUserData({ type: 'countries', id: +foundRegion });
           if (countriesList) {
             const region = countriesList.data.find((region) => region.id === +foundRegion);
@@ -738,13 +736,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         } else {
           token
             ? await mutateUserDataDare(
-              { dare_id: +foundRegion, token: String(token) },
-              {
-                onSuccess: (data) => {
-                  setUserData({ type: 'dare', id: +foundRegion, ...data });
+                { dare_id: +foundRegion, token: String(token) },
+                {
+                  onSuccess: (data) => {
+                    setUserData({ type: 'dare', id: +foundRegion, ...data });
+                  }
                 }
-              }
-            )
+              )
             : setUserData({ type: 'dare', id: +foundRegion });
           if (dareList) {
             const region = dareList.data.find((region) => region.id === +foundRegion);
@@ -814,6 +812,9 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       lat: currentLocation.coords.latitude,
       lng: currentLocation.coords.longitude
     });
+    if (showNomads && token) {
+      refetchUsersLocation();
+    }
     if (currentLocation.coords) {
       cameraRef.current?.flyTo(
         [currentLocation.coords.longitude, currentLocation.coords.latitude],
@@ -890,13 +891,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       if (type === 'regions') {
         token
           ? await mutateUserData(
-            { region_id: id, token: String(token) },
-            {
-              onSuccess: (data) => {
-                setUserData({ type: 'nm', id, ...data });
+              { region_id: id, token: String(token) },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'nm', id, ...data });
+                }
               }
-            }
-          )
+            )
           : setUserData({ type: 'nm', id });
 
         if (regionsList) {
@@ -914,13 +915,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       } else if (type === 'countries') {
         token
           ? await mutateCountriesData(
-            { id, token },
-            {
-              onSuccess: (data) => {
-                setUserData({ type: 'countries', id, ...data.data });
+              { id, token },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'countries', id, ...data.data });
+                }
               }
-            }
-          )
+            )
           : setUserData({ type: 'countries', id });
 
         if (countriesList) {
@@ -938,13 +939,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       } else {
         token
           ? await mutateUserDataDare(
-            { dare_id: +id, token: String(token) },
-            {
-              onSuccess: (data) => {
-                setUserData({ type: 'dare', id: +id, ...data });
+              { dare_id: +id, token: String(token) },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'dare', id: +id, ...data });
+                }
               }
-            }
-          )
+            )
           : setUserData({ type: 'dare', id: +id });
 
         if (dareList) {
@@ -985,11 +986,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         series_id,
         id
       });
+      setSelectedUser(null);
     }
   };
 
   const closeCallout = () => {
     setSelectedMarker(null);
+    setSelectedUser(null);
   };
 
   const toggleSeries = useCallback(
@@ -1027,6 +1030,24 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     setModalState((prevState) => ({ ...prevState, ...updates }));
   };
 
+  const handleUserPress = (event: any) => {
+    const selectedFeature = event.features[0];
+    const { coordinates } = selectedFeature.geometry;
+    const { avatar, first_name, last_name, flag, id, last_seen } = selectedFeature.properties;
+    if (selectedFeature) {
+      setSelectedUser({
+        coordinates,
+        avatar: { uri: API_HOST + avatar },
+        first_name,
+        last_name,
+        flag: { uri: API_HOST + flag },
+        id,
+        last_seen
+      });
+      setSelectedMarker(null);
+    }
+  };
+
   return (
     <SafeAreaView style={{ height: '100%' }}>
       <StatusBar translucent backgroundColor="transparent" />
@@ -1046,6 +1067,19 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
         {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 }
+              }}
+              belowLayerID="waterway-name"
+            />
             <MapLibreGL.FillLayer
               id={regions.id}
               sourceID={regions.source}
@@ -1068,6 +1102,19 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         )}
         {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 }
+              }}
+              belowLayerID="waterway-name"
+            />
             <MapLibreGL.FillLayer
               id={countries.id}
               sourceID={countries.source}
@@ -1182,23 +1229,30 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
           )}
         </MapLibreGL.VectorSource>
 
-        {nomads && (
-          <MapLibreGL.ShapeSource
-            id="nomads"
-            shape={nomads}
-            onPress={(event) => console.log(event.features)}
-          >
+        {nomads && showNomads && (
+          <MapLibreGL.ShapeSource id="nomads" shape={nomads} onPress={handleUserPress}>
             <MapLibreGL.SymbolLayer
               id="nomads_symbol"
               style={{
                 iconImage: ['get', 'icon_key'],
-                iconSize: 0.15,
+                iconSize: [
+                  'interpolate',
+                  ['linear'],
+                  ['zoom'],
+                  0, 0.07,
+                  10, 0.12,
+                  15, 0.18,
+                  20, 0.2
+                ],
                 iconAllowOverlap: true
               }}
+              filter={['!=', 'id', +userId]}
             ></MapLibreGL.SymbolLayer>
           </MapLibreGL.ShapeSource>
         )}
 
+        {selectedUser && <UserItem marker={selectedUser} />}
+
         {selectedMarker && (
           <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} />
         )}

+ 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();

+ 29 - 5
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -17,7 +17,7 @@ import Animated, {
 } from 'react-native-reanimated';
 
 import { styles } from './styles';
-import { API_HOST, FASTEST_MAP_HOST, VECTOR_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';
@@ -55,7 +55,7 @@ let regions_visited = {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.5,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: generateFilter([]),
   maxzoom: 12
@@ -69,7 +69,7 @@ let countries_visited = {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.5,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: generateFilter([]),
   maxzoom: 12
@@ -96,7 +96,7 @@ let regions = {
   'source-layer': 'regions',
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: ['all'],
   maxzoom: 16
@@ -109,7 +109,7 @@ let countries = {
   'source-layer': 'countries',
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   filter: ['all'],
   maxzoom: 16
@@ -311,6 +311,18 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
       >
         {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}
@@ -333,6 +345,18 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         )}
         {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}

+ 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;

+ 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',
 }