Viktoriia пре 7 месеци
родитељ
комит
25cc418525

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 197 - 7
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -1,10 +1,19 @@
 import React, { useEffect, useState } from 'react';
-import { View, Text, TouchableOpacity, Image, Switch, Dimensions } from 'react-native';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Image,
+  Switch,
+  Dimensions,
+  Platform,
+  Linking
+} from 'react-native';
 import ReactModal from 'react-native-modal';
 import { Colors } from 'src/theme';
 import { ModalStyles } from '../../TravellersScreen/Components/styles';
 import { Dropdown, MultiSelect } from 'react-native-searchable-dropdown-kj';
-import { Button } from 'src/components';
+import { Button, WarningModal } from 'src/components';
 import { ButtonVariants } from 'src/types/components';
 import { styles } from './styles';
 import { TabBar, TabView } from 'react-native-tab-view';
@@ -15,6 +24,12 @@ import { useGetListQuery } from '@api/series';
 import { RadioButton } from 'react-native-paper';
 import { storage, StoreType } from 'src/storage';
 import moment from 'moment';
+import {
+  usePostGetSettingsQuery,
+  usePostSetSettingsMutation,
+  usePostUpdateLocationMutation
+} from '@api/location';
+import * as Location from 'expo-location';
 
 const FilterModal = ({
   isFilterVisible,
@@ -27,7 +42,9 @@ const FilterModal = ({
   setRegionsFilter,
   setSeriesFilter,
   isPublicView,
-  isLogged = true
+  isLogged = true,
+  showNomads,
+  setShowNomads
 }: {
   isFilterVisible: boolean;
   setIsFilterVisible: (isVisible: boolean) => void;
@@ -40,8 +57,13 @@ const FilterModal = ({
   setSeriesFilter?: (filter: any) => void;
   isPublicView: boolean;
   isLogged: boolean;
+  showNomads?: boolean;
+  setShowNomads?: (showNomads: boolean) => void;
 }) => {
   const token = storage.get('token', StoreType.STRING) as string;
+  const { data: locationSettings } = usePostGetSettingsQuery(token, isLogged && !isPublicView);
+  const { mutateAsync: setSettings } = usePostSetSettingsMutation();
+  const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
   const [index, setIndex] = useState(0);
   const [selectedYear, setSelectedYear] = useState<{ label: string; value: number } | null>(null);
   const [allYears, setAllYears] = useState<{ label: string; value: number }[]>([]);
@@ -52,7 +74,8 @@ const FilterModal = ({
   ];
   const [routes] = useState([
     { key: 'regions', title: 'Travels' },
-    { key: 'series', title: 'Series' }
+    { key: 'series', title: 'Series' },
+    { key: 'nomads', title: 'Nomads' }
   ]);
   const { data } = usePostGetMapYearsQuery(token as string, userId, isLogged ? true : false);
   const { data: seriesList } = useGetListQuery(true);
@@ -64,6 +87,21 @@ const FilterModal = ({
     ? (storage.get('filterSettings', StoreType.STRING) as string)
     : null;
 
+  const [isSharing, setIsSharing] = useState(false);
+  const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
+  const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
+
+  useEffect(() => {
+    const syncSettings = async () => {
+      if (locationSettings) {
+        let { status } = await Location.getForegroundPermissionsAsync();
+        setIsSharing(locationSettings.sharing !== 0 && status === 'granted');
+      }
+    };
+
+    syncSettings();
+  }, [locationSettings]);
+
   useEffect(() => {
     const loadFilterSettings = () => {
       try {
@@ -192,8 +230,8 @@ const FilterModal = ({
     setIsFilterVisible(false);
   };
 
-  const renderScene = ({ route }: { route: any }) => {
-    return route.key === 'regions' ? (
+  const renderRegions = () => {
+    return (
       <View style={styles.sceneContainer}>
         <View style={styles.optionsContainer}>
           <View style={styles.rowWrapper}>
@@ -301,7 +339,11 @@ const FilterModal = ({
           />
         </View>
       </View>
-    ) : (
+    );
+  };
+
+  const renderSeries = () => {
+    return (
       <View style={styles.sceneContainer}>
         <View style={styles.optionsContainer}>
           <View style={[styles.row, { gap: 8 }]}>
@@ -450,6 +492,154 @@ const FilterModal = ({
     );
   };
 
+  const toggleSettingsSwitch = async () => {
+    if (!isSharing) {
+      handleGetLocation();
+    } else {
+      setSettings({ token, sharing: 0 });
+      setShowNomads && setShowNomads(false);
+      storage.set('showNomads', false);
+      setIsSharing(false);
+    }
+  };
+
+  const handleGetLocation = async () => {
+    let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    } else {
+      setAskLocationVisible(true);
+    }
+  };
+
+  const getLocation = async () => {
+    let currentLocation = await Location.getCurrentPositionAsync({
+      accuracy: Location.Accuracy.Balanced
+    });
+    setSettings(
+      { token, sharing: 1 },
+      {
+        onSuccess: (res) => {
+          console.log('Settings updated', res);
+        }
+      }
+    );
+    setIsSharing(true);
+    updateLocation({
+      token,
+      lat: currentLocation.coords.latitude,
+      lng: currentLocation.coords.longitude
+    });
+  };
+
+  const handleAcceptPermission = async () => {
+    setAskLocationVisible(false);
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
+
+    if (status === 'granted') {
+      getLocation();
+    } else if (!canAskAgain) {
+      setOpenSettingsVisible(true);
+    }
+  };
+
+  const toggleNomadsSwitch = () => {
+    setShowNomads && setShowNomads(!showNomads);
+    storage.set('showNomads', !showNomads);
+  };
+
+  const renderNomads = () => {
+    return (
+      <View style={[styles.sceneContainer, { flex: 0 }]}>
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleNomadsSwitch}
+          disabled={!isSharing}
+        >
+          <View style={styles.alignStyle}>
+            {/* <BellIcon fill={Colors.DARK_BLUE} width={20} height={20} /> */}
+            <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
+              Show nomads
+            </Text>
+          </View>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleNomadsSwitch}
+              value={showNomads}
+              style={{ transform: 'scale(0.8)' }}
+              disabled={!isSharing}
+            />
+          </View>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleSettingsSwitch}
+        >
+          <View style={styles.alignStyle}>
+            {/* <BellIcon fill={Colors.DARK_BLUE} width={20} height={20} /> */}
+            <Text style={styles.buttonLabel}>Share location</Text>
+          </View>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleSettingsSwitch}
+              value={isSharing}
+              style={{ transform: 'scale(0.8)' }}
+            />
+          </View>
+        </TouchableOpacity>
+        <WarningModal
+          type={'success'}
+          isVisible={askLocationVisible}
+          onClose={() => setAskLocationVisible(false)}
+          action={handleAcceptPermission}
+          message="To use this feature we need your permission to access your location. If you press OK your system will ask you to approve location sharing with NomadMania app."
+        />
+        <WarningModal
+          type={'success'}
+          isVisible={openSettingsVisible}
+          onClose={() => setOpenSettingsVisible(false)}
+          action={() =>
+            Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
+          }
+          message="NomadMania app needs location permissions to function properly. Open settings?"
+        />
+      </View>
+    );
+  };
+
+  const renderScene = ({ route }: { route: any }) => {
+    switch (route.key) {
+      case 'regions':
+        return renderRegions();
+      case 'series':
+        return renderSeries();
+      case 'nomads':
+        return renderNomads();
+      default:
+        return null;
+    }
+  };
+
   const isSmallScreen = Dimensions.get('window').width < 383;
 
   return (

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

@@ -66,5 +66,20 @@ export const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center'
   },
-  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 }
+  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 },
+  alignStyle: {
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  buttonWrapper: {
+    width: '100%',
+    height: 48
+  },
+  buttonLabel: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(12),
+    fontWeight: '700',
+    marginLeft: 15
+  }
 });

+ 71 - 4
src/screens/InAppScreens/MapScreen/index.tsx

@@ -64,6 +64,7 @@ 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';
 
 MapLibreGL.setAccessToken(null);
 MapLibreGL.Logger.setLogLevel('error');
@@ -256,6 +257,10 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     visitedLabel: 'by',
     year: moment().year()
   });
+  const [showNomads, setShowNomads] = useState(
+    (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false
+  );
+  const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
   const { data: visitedRegionIds, refetch: refetchVisitedRegions } =
     usePostGetVisitedRegionsIdsQuery(
       token,
@@ -336,11 +341,29 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [seriesVisited, setSeriesVisited] = useState<any[]>([]);
   const [images, setImages] = useState<any>({});
   const { mutateAsync: updateSeriesItem } = usePostSetToggleItem();
+  const [nomads, setNomads] = useState<GeoJSON.FeatureCollection | null>(null);
+  const { data: usersLocation } = usePostGetUsersLocationQuery(
+    token,
+    !!token && showNomads && Boolean(location)
+  );
 
   useEffect(() => {
-    if (seriesIcons) {
-      let loadedImages: any = {};
+    if (!showNomads) {
+      setNomads(null);
+    }
+  }, [showNomads]);
 
+  useEffect(() => {
+    if (usersLocation) {
+      console.log('usersLocation', usersLocation);
+      setNomads(usersLocation.geojson);
+    }
+  }, [usersLocation]);
+
+  useEffect(() => {
+    let loadedImages: any = {};
+
+    if (seriesIcons) {
       seriesIcons.data.forEach(async (icon) => {
         const id = icon.id;
         const img = API_HOST + '/static/img/series_new2/' + icon.new_icon_png;
@@ -356,10 +379,23 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
           console.error(`Error loading icon for series_id ${id}:`, error);
         }
       });
+    }
 
-      setImages(loadedImages);
+    if (nomads && nomads.features) {
+      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 };
+          if (feature.properties) {
+            feature.properties.icon_key = user_id;
+          }
+        }
+      });
     }
-  }, [seriesIcons]);
+
+    setImages(loadedImages);
+  }, [nomads, seriesIcons]);
 
   useEffect(() => {
     const loadDatabases = async () => {
@@ -491,6 +527,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     (async () => {
       let { status } = await Location.getForegroundPermissionsAsync();
       if (status !== 'granted') {
+        setShowNomads(false);
+        storage.set('showNomads', false);
         return;
       }
 
@@ -498,6 +536,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         accuracy: Location.Accuracy.Balanced
       });
       setLocation(currentLocation.coords);
+      updateLocation({
+        token,
+        lat: currentLocation.coords.latitude,
+        lng: currentLocation.coords.longitude
+      });
     })();
   }, []);
 
@@ -735,6 +778,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       accuracy: Location.Accuracy.Balanced
     });
     setLocation(currentLocation.coords);
+    updateLocation({
+      token,
+      lat: currentLocation.coords.latitude,
+      lng: currentLocation.coords.longitude
+    });
     if (currentLocation.coords) {
       cameraRef.current?.flyTo(
         [currentLocation.coords.longitude, currentLocation.coords.latitude],
@@ -1093,6 +1141,23 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
           )}
         </MapLibreGL.VectorSource>
 
+        {nomads && (
+          <MapLibreGL.ShapeSource
+            id="nomads"
+            shape={nomads}
+            onPress={(event) => console.log(event.features)}
+          >
+            <MapLibreGL.SymbolLayer
+              id="nomads_symbol"
+              style={{
+                iconImage: ['get', 'icon_key'],
+                iconSize: 0.15,
+                iconAllowOverlap: true
+              }}
+            ></MapLibreGL.SymbolLayer>
+          </MapLibreGL.ShapeSource>
+        )}
+
         {selectedMarker && (
           <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} />
         )}
@@ -1277,6 +1342,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         userId={userId ? +userId : 0}
         setRegionsFilter={setRegionsFilter}
         setSeriesFilter={setSeriesFilter}
+        setShowNomads={setShowNomads}
+        showNomads={showNomads}
         isPublicView={false}
         isLogged={token ? true : false}
       />

+ 13 - 2
src/types/api.ts

@@ -25,6 +25,7 @@ export enum API_ROUTE {
   CHAT = 'chat',
   MAPS = 'maps',
   DARE = 'dare',
+  LOCATION = 'location',
 }
 
 export enum API_ENDPOINT {
@@ -151,7 +152,12 @@ export enum API_ENDPOINT {
   GET_LIST_DARE = 'get-list-dare',
   GET_LATEST_VERSION = 'latest-version',
   GET_ICONS = 'get-icons',
-  GET_VISITED_SERIES_IDS = 'get-visited-series-ids'
+  GET_VISITED_SERIES_IDS = 'get-visited-series-ids',
+  GET_LOCATION_SETTINGS = 'get-settings',
+  SET_LOCATION_SETTINGS = 'set-settings',
+  UPDATE_LOCATION = 'update-location',
+  GET_USERS_LOCATION = 'get-users-location',
+  IS_FEATURE_ACTIVE = 'is-feature-active',
 }
 
 export enum API {
@@ -277,7 +283,12 @@ export enum API {
   GET_LIST_DARE = `${API_ROUTE.DARE}/${API_ENDPOINT.GET_LIST_DARE}`,
   LATEST_VERSION = `${API_ROUTE.APP}/${API_ENDPOINT.GET_LATEST_VERSION}`,
   GET_ICONS = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_ICONS}`,
-  GET_VISITED_SERIES_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_SERIES_IDS}`
+  GET_VISITED_SERIES_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_SERIES_IDS}`,
+  GET_LOCATION_SETTINGS = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_LOCATION_SETTINGS}`,
+  SET_LOCATION_SETTINGS = `${API_ROUTE.LOCATION}/${API_ENDPOINT.SET_LOCATION_SETTINGS}`,
+  UPDATE_LOCATION = `${API_ROUTE.LOCATION}/${API_ENDPOINT.UPDATE_LOCATION}`,
+  GET_USERS_LOCATION = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_USERS_LOCATION}`,
+  IS_FEATURE_ACTIVE = `${API_ROUTE.LOCATION}/${API_ENDPOINT.IS_FEATURE_ACTIVE}`,
 }
 
 export type BaseAxiosError = AxiosError;