Browse Source

location sharing

Viktoriia 7 months ago
parent
commit
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 ChatScreen from 'src/screens/InAppScreens/MessagesScreen/ChatScreen';
 import { Splash } from 'src/components/SplashSpinner';
 import { Splash } from 'src/components/SplashSpinner';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import LocationSharingScreen from 'src/screens/LocationSharingScreen';
 
 
 enableScreens();
 enableScreens();
 
 
@@ -143,7 +144,7 @@ const Route = () => {
     storage.remove('token');
     storage.remove('token');
     storage.remove('uid');
     storage.remove('uid');
     storage.remove('currentUserData');
     storage.remove('currentUserData');
-    storage.remove('visitedTilesUrl');
+    storage.remove('showNomads');
     storage.remove('filterSettings');
     storage.remove('filterSettings');
     updateNotificationStatus();
     updateNotificationStatus();
     updateUnreadMessagesCount();
     updateUnreadMessagesCount();
@@ -458,6 +459,10 @@ const Route = () => {
               name={NAVIGATION_PAGES.SYSTEM_NOTIFICATIONS}
               name={NAVIGATION_PAGES.SYSTEM_NOTIFICATIONS}
               component={SystemNotificationsScreen}
               component={SystemNotificationsScreen}
             />
             />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.LOCATION_SHARING}
+              component={LocationSharingScreen}
+            />
           </ScreenStack.Navigator>
           </ScreenStack.Navigator>
         )}
         )}
       </BottomTab.Screen>
       </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('token');
       storage.remove('uid');
       storage.remove('uid');
       storage.remove('currentUserData');
       storage.remove('currentUserData');
-      storage.remove('visitedTilesUrl');
+      storage.remove('showNomads');
       storage.remove('filterSettings');
       storage.remove('filterSettings');
       updateNotificationStatus();
       updateNotificationStatus();
       updateUnreadMessagesCount();
       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 UserXMark from '../../../assets/icons/user-xmark.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import BellIcon from 'assets/icons/notifications/bell-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 { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { usePostIsFeatureActiveQuery } from '@api/location';
 
 
 export const MenuDrawer = (props: any) => {
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
   const { mutate: deleteUser } = useDeleteUserMutation();
   const token = storage.get('token', StoreType.STRING) as string;
   const token = storage.get('token', StoreType.STRING) as string;
+  const { data: isFeatureActive } = usePostIsFeatureActiveQuery(token, !!token);
   const navigation = useNavigation();
   const navigation = useNavigation();
   const [modalInfo, setModalInfo] = useState({
   const [modalInfo, setModalInfo] = useState({
     visible: false,
     visible: false,
@@ -51,7 +55,7 @@ export const MenuDrawer = (props: any) => {
     storage.remove('token');
     storage.remove('token');
     storage.remove('uid');
     storage.remove('uid');
     storage.remove('currentUserData');
     storage.remove('currentUserData');
-    storage.remove('visitedTilesUrl');
+    storage.remove('showNomads');
     storage.remove('filterSettings');
     storage.remove('filterSettings');
     updateNotificationStatus();
     updateNotificationStatus();
     updateUnreadMessagesCount();
     updateUnreadMessagesCount();
@@ -69,7 +73,7 @@ export const MenuDrawer = (props: any) => {
 
 
   return (
   return (
     <>
     <>
-      <View style={styles.container}>
+      <SafeAreaView style={styles.container}>
         <View style={{ flex: 1 }}>
         <View style={{ flex: 1 }}>
           <View style={styles.logoContainer}>
           <View style={styles.logoContainer}>
             <Image source={require('../../../assets/logo-ua.png')} style={styles.logo} />
             <Image source={require('../../../assets/logo-ua.png')} style={styles.logo} />
@@ -99,12 +103,26 @@ export const MenuDrawer = (props: any) => {
               red={false}
               red={false}
               buttonFn={() =>
               buttonFn={() =>
                 // todo: add types
                 // todo: add types
+                // @ts-ignore
                 navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
                 navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
                   screen: NAVIGATION_PAGES.NOTIFICATIONS
                   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>
 
 
         <View style={styles.bottomMenu}>
         <View style={styles.bottomMenu}>
@@ -141,7 +159,7 @@ export const MenuDrawer = (props: any) => {
             </Text>
             </Text>
           </View>
           </View>
         </View>
         </View>
-      </View>
+      </SafeAreaView>
 
 
       <WarningModal
       <WarningModal
         isVisible={modalInfo.visible}
         isVisible={modalInfo.visible}

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

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

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

@@ -1,12 +1,11 @@
 import * as FileSystem from 'expo-file-system';
 import * as FileSystem from 'expo-file-system';
 import MapLibreGL from '@maplibre/maplibre-react-native';
 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/`;
 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> {
 async function deleteCachedTilesIfExist(): Promise<void> {
   try {
   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 { styles } from './styles';
 import { TabBar, TabView } from 'react-native-tab-view';
 import { TabBar, TabView } from 'react-native-tab-view';
 import { usePostGetMapYearsQuery } from '@api/user';
 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 CheckSvg from 'assets/icons/mark.svg';
 import { useGetListQuery } from '@api/series';
 import { useGetListQuery } from '@api/series';
 import { RadioButton } from 'react-native-paper';
 import { RadioButton } from 'react-native-paper';
@@ -26,11 +26,16 @@ import { storage, StoreType } from 'src/storage';
 import moment from 'moment';
 import moment from 'moment';
 import {
 import {
   usePostGetSettingsQuery,
   usePostGetSettingsQuery,
+  usePostIsFeatureActiveQuery,
   usePostSetSettingsMutation,
   usePostSetSettingsMutation,
   usePostUpdateLocationMutation
   usePostUpdateLocationMutation
 } from '@api/location';
 } from '@api/location';
 import * as Location from 'expo-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 = ({
 const FilterModal = ({
   isFilterVisible,
   isFilterVisible,
   setIsFilterVisible,
   setIsFilterVisible,
@@ -72,10 +77,10 @@ const FilterModal = ({
     { label: 'visited by', value: 0 },
     { label: 'visited by', value: 0 },
     { label: 'visited in', value: 1 }
     { label: 'visited in', value: 1 }
   ];
   ];
-  const [routes] = useState([
+  const { data: isFeatureActive } = usePostIsFeatureActiveQuery(token, !!token);
+  const [routes, setRoutes] = useState([
     { key: 'regions', title: 'Travels' },
     { 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 } = usePostGetMapYearsQuery(token as string, userId, isLogged ? true : false);
   const { data: seriesList } = useGetListQuery(true);
   const { data: seriesList } = useGetListQuery(true);
@@ -91,6 +96,16 @@ const FilterModal = ({
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = 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(() => {
   useEffect(() => {
     const syncSettings = async () => {
     const syncSettings = async () => {
       if (locationSettings) {
       if (locationSettings) {
@@ -173,56 +188,35 @@ const FilterModal = ({
   if (!data && isLogged) return;
   if (!data && isLogged) return;
 
 
   const handleApplyFilter = () => {
   const handleApplyFilter = () => {
-    let tileUrl = `${FASTEST_MAP_HOST}/tiles_nm/`;
     if (!isLogged) {
     if (!isLogged) {
       return;
       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 (selectedVisible.value === 0) {
       if (tilesType.value === 0) {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
         setRegionsFilter({
           visitedLabel: 'by',
           visitedLabel: 'by',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
         });
       } else if (tilesType.value === 1) {
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
         setRegionsFilter({
           visitedLabel: 'by',
           visitedLabel: 'by',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
         });
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
       }
       }
     } else {
     } else {
       if (tilesType.value === 0) {
       if (tilesType.value === 0) {
-        tileUrl += 'user_visited_in_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
         setRegionsFilter({
           visitedLabel: 'in',
           visitedLabel: 'in',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
         });
       } else if (tilesType.value === 1) {
       } else if (tilesType.value === 1) {
-        tileUrl += 'user_visited_un_in_year/' + userId + '/' + selectedYear.value;
         setRegionsFilter({
         setRegionsFilter({
           visitedLabel: 'in',
           visitedLabel: 'in',
-          year: selectedYear.value
+          year: selectedYear ? selectedYear.value : moment().year()
         });
         });
-      } else {
-        tileUrl += 'user_visited_dare/' + userId;
       }
       }
     }
     }
-    !isPublicView && storage.set('visitedTilesUrl', tileUrl);
   };
   };
 
 
   const handleCloseFilter = () => {
   const handleCloseFilter = () => {
@@ -327,10 +321,6 @@ const FilterModal = ({
                     }
                     }
                   })
                   })
                 );
                 );
-                storage.set(
-                  'visitedTilesUrl',
-                  `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`
-                );
               }
               }
             }}
             }}
             variant={ButtonVariants.OPACITY}
             variant={ButtonVariants.OPACITY}
@@ -519,14 +509,7 @@ const FilterModal = ({
     let currentLocation = await Location.getCurrentPositionAsync({
     let currentLocation = await Location.getCurrentPositionAsync({
       accuracy: Location.Accuracy.Balanced
       accuracy: Location.Accuracy.Balanced
     });
     });
-    setSettings(
-      { token, sharing: 1 },
-      {
-        onSuccess: (res) => {
-          console.log('Settings updated', res);
-        }
-      }
-    );
+    setSettings({ token, sharing: 1 });
     setIsSharing(true);
     setIsSharing(true);
     updateLocation({
     updateLocation({
       token,
       token,
@@ -554,6 +537,17 @@ const FilterModal = ({
   const renderNomads = () => {
   const renderNomads = () => {
     return (
     return (
       <View style={[styles.sceneContainer, { flex: 0 }]}>
       <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
         <TouchableOpacity
           style={[
           style={[
             styles.alignStyle,
             styles.alignStyle,
@@ -566,7 +560,11 @@ const FilterModal = ({
           disabled={!isSharing}
           disabled={!isSharing}
         >
         >
           <View style={styles.alignStyle}>
           <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 } : {}]}>
             <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
               Show nomads
               Show nomads
             </Text>
             </Text>
@@ -594,7 +592,7 @@ const FilterModal = ({
           onPress={toggleSettingsSwitch}
           onPress={toggleSettingsSwitch}
         >
         >
           <View style={styles.alignStyle}>
           <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>
             <Text style={styles.buttonLabel}>Share location</Text>
           </View>
           </View>
           <View>
           <View>

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

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

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

@@ -17,7 +17,7 @@ import Animated, {
 } from 'react-native-reanimated';
 } from 'react-native-reanimated';
 
 
 import { styles } from './styles';
 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 { CommonActions, NavigationProp } from '@react-navigation/native';
 import { AvatarWithInitials, LocationPopup } from 'src/components';
 import { AvatarWithInitials, LocationPopup } from 'src/components';
 import { Colors } from 'src/theme';
 import { Colors } from 'src/theme';
@@ -55,7 +55,7 @@ let regions_visited = {
   style: {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.5,
     fillOpacity: 0.5,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   },
   filter: generateFilter([]),
   filter: generateFilter([]),
   maxzoom: 12
   maxzoom: 12
@@ -69,7 +69,7 @@ let countries_visited = {
   style: {
   style: {
     fillColor: 'rgba(255, 126, 0, 1)',
     fillColor: 'rgba(255, 126, 0, 1)',
     fillOpacity: 0.5,
     fillOpacity: 0.5,
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   },
   filter: generateFilter([]),
   filter: generateFilter([]),
   maxzoom: 12
   maxzoom: 12
@@ -96,7 +96,7 @@ let regions = {
   'source-layer': 'regions',
   'source-layer': 'regions',
   style: {
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   },
   filter: ['all'],
   filter: ['all'],
   maxzoom: 16
   maxzoom: 16
@@ -109,7 +109,7 @@ let countries = {
   'source-layer': 'countries',
   'source-layer': 'countries',
   style: {
   style: {
     fillColor: 'rgba(15, 63, 79, 0)',
     fillColor: 'rgba(15, 63, 79, 0)',
-    fillOutlineColor: 'rgba(14, 80, 109, 1)'
+    fillOutlineColor: 'rgba(14, 80, 109, 0)'
   },
   },
   filter: ['all'],
   filter: ['all'],
   maxzoom: 16
   maxzoom: 16
@@ -311,6 +311,18 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
       >
       >
         {type === 'regions' && (
         {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
             <MapLibreGL.FillLayer
               id={regions.id}
               id={regions.id}
               sourceID={regions.source}
               sourceID={regions.source}
@@ -333,6 +345,18 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         )}
         )}
         {type === 'countries' && (
         {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
             <MapLibreGL.FillLayer
               id={countries.id}
               id={countries.id}
               sourceID={countries.source}
               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',
   SYSTEM_NOTIFICATIONS = 'inAppSystemNotifications',
   IN_APP_MESSAGES_TAB = 'Messages',
   IN_APP_MESSAGES_TAB = 'Messages',
   CHATS_LIST = 'inAppChatsList',
   CHATS_LIST = 'inAppChatsList',
-  CHAT = 'inAppChat'
+  CHAT = 'inAppChat',
+  LOCATION_SHARING = 'inAppLocationSharing',
 }
 }