Переглянути джерело

locate me btn for region selectors

Viktoriia 1 тиждень тому
батько
коміт
3631e2805a

+ 118 - 2
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx

@@ -1,11 +1,13 @@
 import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { View, Text, TouchableOpacity } from 'react-native';
+import { View, Text, TouchableOpacity, ActivityIndicator, Platform, Linking } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { useNavigation } from '@react-navigation/native';
 import * as turf from '@turf/turf';
 import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import * as Location from 'expo-location';
+import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
 
-import { Header, Modal, FlatList as List } from 'src/components';
+import { Header, Modal, FlatList as List, WarningModal } from 'src/components';
 
 import { VECTOR_MAP_HOST } from 'src/constants';
 import { Colors } from 'src/theme';
@@ -17,6 +19,7 @@ import { styles } from './styles';
 
 import SearchSvg from '../../../../../assets/icons/search.svg';
 import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg';
+import LocationIcon from 'assets/icons/location.svg';
 
 const generateFilter = (ids: number[]) => {
   return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
@@ -50,6 +53,7 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
   const { data } = useGetRegionsForTripsQuery(true);
   const { data: regionsList } = useGetListRegionsQuery(true);
   const navigation = useNavigation();
+  const tabBarHeight = useBottomTabBarHeight();
 
   const [regions, setRegions] = useState<RegionAddData[] | null>(null);
   const [isModalVisible, setIsModalVisible] = useState(false);
@@ -61,6 +65,10 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
   const cameraRef = useRef<MapLibreRN.CameraRef>(null);
 
   const [filterSelectedRegions, setFilterSelectedRegions] = useState<any[]>(generateFilter([]));
+  const [isLocationLoading, setIsLocationLoading] = useState(false);
+  const [location, setLocation] = useState<any | null>(null);
+  const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
+  const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
 
   useEffect(() => {
     if (data && data.regions) {
@@ -190,6 +198,54 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
     [selectedRegions, regions]
   );
 
+  const handleGetLocation = async () => {
+    setIsLocationLoading(true);
+    try {
+      let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+      const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+      if (status === 'granted' && isServicesEnabled) {
+        await getLocation();
+      } else if (!canAskAgain || !isServicesEnabled) {
+        setOpenSettingsVisible(true);
+      } else {
+        setAskLocationVisible(true);
+      }
+    } finally {
+      setIsLocationLoading(false);
+    }
+  };
+
+  const getLocation = async () => {
+    try {
+      let currentLocation = await Location.getCurrentPositionAsync({
+        accuracy: Location.Accuracy.Balanced
+      });
+      setLocation(currentLocation.coords);
+
+      if (currentLocation.coords) {
+        cameraRef.current?.flyTo(
+          [currentLocation.coords.longitude, currentLocation.coords.latitude],
+          1000
+        );
+      }
+    } catch (error) {
+      console.error('Error fetching user location:', error);
+    }
+  };
+
+  const handleAcceptPermission = async () => {
+    setAskLocationVisible(false);
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
+    const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+    if (status === 'granted' && isServicesEnabled) {
+      getLocation();
+    } else if (!canAskAgain || !isServicesEnabled) {
+      setOpenSettingsVisible(true);
+    }
+  };
+
   return (
     <SafeAreaView style={{ height: '100%' }} edges={['top']}>
       <View style={styles.wrapper}>
@@ -257,7 +313,41 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
               belowLayerID="nm-regions-line-layer"
             />
           ) : null}
+
+          {location && (
+            <MapLibreRN.UserLocation
+              animated={true}
+              showsUserHeadingIndicator={true}
+              onPress={async () => {
+                const currentZoom = await mapRef.current?.getZoom();
+                const newZoom = (currentZoom || 0) + 2;
+
+                cameraRef.current?.setCamera({
+                  centerCoordinate: [location.longitude, location.latitude],
+                  zoomLevel: newZoom,
+                  animationDuration: 500,
+                  animationMode: 'flyTo'
+                });
+              }}
+            ></MapLibreRN.UserLocation>
+          )}
         </MapLibreRN.MapView>
+
+        <TouchableOpacity
+          onPress={handleGetLocation}
+          style={[
+            styles.cornerButton,
+            styles.bottomButton,
+            styles.bottomRightButton,
+            { bottom: 20 }
+          ]}
+        >
+          {isLocationLoading ? (
+            <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
+          ) : (
+            <LocationIcon />
+          )}
+        </TouchableOpacity>
       </View>
       {regionPopupVisible && regionData && (
         <View style={styles.popupWrapper}>
@@ -279,6 +369,32 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
           }}
         />
       </Modal>
+
+      <WarningModal
+        type={'success'}
+        isVisible={openSettingsVisible}
+        onClose={() => setOpenSettingsVisible(false)}
+        action={async () => {
+          const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+          if (!isServicesEnabled) {
+            Platform.OS === 'ios'
+              ? Linking.openURL('app-settings:')
+              : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
+          } else {
+            Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
+          }
+        }}
+        message="NomadMania app needs location permissions to function properly. Open settings?"
+      />
+
+      <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."
+      />
     </SafeAreaView>
   );
 };

+ 26 - 0
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/styles.tsx

@@ -69,5 +69,31 @@ export const styles = StyleSheet.create({
     color: Colors.WHITE,
     fontSize: getFontSize(12),
     fontWeight: '600'
+  },
+  cornerButton: {
+    position: 'absolute',
+    backgroundColor: Colors.WHITE,
+    padding: 12,
+    width: 48,
+    height: 48,
+    borderRadius: 24,
+    alignItems: 'center',
+    justifyContent: 'center',
+    shadowColor: '#000',
+    shadowOffset: {
+      width: 0,
+      height: 1
+    },
+    shadowOpacity: 0.25,
+    shadowRadius: 1.5,
+    elevation: 2
+  },
+  bottomButton: {
+    width: 42,
+    height: 42,
+    borderRadius: 21
+  },
+  bottomRightButton: {
+    right: 16
   }
 });

+ 150 - 1
src/screens/OfflineMapsScreen/SelectRegionsScreen/index.tsx

@@ -1,10 +1,20 @@
 import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { View, StyleSheet, TouchableOpacity, Text, ScrollView } from 'react-native';
+import {
+  View,
+  StyleSheet,
+  TouchableOpacity,
+  Text,
+  ScrollView,
+  Platform,
+  Linking,
+  ActivityIndicator as RNActivityIndicator
+} from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { Modal, FlatList as List, Header, WarningModal } from 'src/components';
 import * as turf from '@turf/turf';
 import * as MapLibreRN from '@maplibre/maplibre-react-native';
 import NetInfo from '@react-native-community/netinfo';
+import * as Location from 'expo-location';
 
 import { getFontSize } from 'src/utils';
 import { useGetRegionsForTripsQuery } from '@api/trips';
@@ -19,6 +29,7 @@ import { usePostGetMapDataForRegionMutation } from '@api/maps';
 import { formatBytes } from '../formatters';
 import { ActivityIndicator } from 'react-native-paper';
 import { offlineMapManager } from '../OfflineMapManager';
+import LocationIcon from 'assets/icons/location.svg';
 
 const generateFilter = (ids: number[]) => {
   return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
@@ -80,6 +91,10 @@ export const SelectRegionScreen = ({ navigation }: { navigation: any }) => {
 
   const pendingRegionRequests = new Set();
   const [isLoading, setIsLoading] = useState(false);
+  const [isLocationLoading, setIsLocationLoading] = useState(false);
+  const [location, setLocation] = useState<any | null>(null);
+  const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
+  const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
 
   useEffect(() => {
     if (data && data.regions) {
@@ -373,6 +388,54 @@ export const SelectRegionScreen = ({ navigation }: { navigation: any }) => {
     [selectedRegions, regions, regionsToSave]
   );
 
+  const handleGetLocation = async () => {
+    setIsLocationLoading(true);
+    try {
+      let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
+      const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+      if (status === 'granted' && isServicesEnabled) {
+        await getLocation();
+      } else if (!canAskAgain || !isServicesEnabled) {
+        setOpenSettingsVisible(true);
+      } else {
+        setAskLocationVisible(true);
+      }
+    } finally {
+      setIsLocationLoading(false);
+    }
+  };
+
+  const getLocation = async () => {
+    try {
+      let currentLocation = await Location.getCurrentPositionAsync({
+        accuracy: Location.Accuracy.Balanced
+      });
+      setLocation(currentLocation.coords);
+
+      if (currentLocation.coords) {
+        cameraRef.current?.flyTo(
+          [currentLocation.coords.longitude, currentLocation.coords.latitude],
+          1000
+        );
+      }
+    } catch (error) {
+      console.error('Error fetching user location:', error);
+    }
+  };
+
+  const handleAcceptPermission = async () => {
+    setAskLocationVisible(false);
+    let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
+    const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+    if (status === 'granted' && isServicesEnabled) {
+      getLocation();
+    } else if (!canAskAgain || !isServicesEnabled) {
+      setOpenSettingsVisible(true);
+    }
+  };
+
   return (
     <SafeAreaView style={{ height: '100%' }} edges={['top']}>
       <View style={styles.wrapper}>
@@ -440,7 +503,41 @@ export const SelectRegionScreen = ({ navigation }: { navigation: any }) => {
               belowLayerID="nm-regions-line-layer"
             />
           ) : null}
+
+          {location && (
+            <MapLibreRN.UserLocation
+              animated={true}
+              showsUserHeadingIndicator={true}
+              onPress={async () => {
+                const currentZoom = await mapRef.current?.getZoom();
+                const newZoom = (currentZoom || 0) + 2;
+
+                cameraRef.current?.setCamera({
+                  centerCoordinate: [location.longitude, location.latitude],
+                  zoomLevel: newZoom,
+                  animationDuration: 500,
+                  animationMode: 'flyTo'
+                });
+              }}
+            ></MapLibreRN.UserLocation>
+          )}
         </MapLibreRN.MapView>
+
+        <TouchableOpacity
+          onPress={handleGetLocation}
+          style={[
+            styles.cornerButton,
+            styles.bottomButton,
+            styles.bottomRightButton,
+            { bottom: 20 }
+          ]}
+        >
+          {isLocationLoading ? (
+            <RNActivityIndicator size="small" color={Colors.DARK_BLUE} />
+          ) : (
+            <LocationIcon />
+          )}
+        </TouchableOpacity>
       </View>
 
       <View style={styles.infoContainer}>
@@ -494,6 +591,32 @@ export const SelectRegionScreen = ({ navigation }: { navigation: any }) => {
         action={modalState.action}
         onClose={() => closeModal()}
       />
+
+      <WarningModal
+        type={'success'}
+        isVisible={openSettingsVisible}
+        onClose={() => setOpenSettingsVisible(false)}
+        action={async () => {
+          const isServicesEnabled = await Location.hasServicesEnabledAsync();
+
+          if (!isServicesEnabled) {
+            Platform.OS === 'ios'
+              ? Linking.openURL('app-settings:')
+              : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
+          } else {
+            Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
+          }
+        }}
+        message="NomadMania app needs location permissions to function properly. Open settings?"
+      />
+
+      <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."
+      />
     </SafeAreaView>
   );
 };
@@ -510,6 +633,32 @@ const styles = StyleSheet.create({
   map: {
     ...StyleSheet.absoluteFillObject
   },
+  cornerButton: {
+    position: 'absolute',
+    backgroundColor: Colors.WHITE,
+    padding: 12,
+    width: 48,
+    height: 48,
+    borderRadius: 24,
+    alignItems: 'center',
+    justifyContent: 'center',
+    shadowColor: '#000',
+    shadowOffset: {
+      width: 0,
+      height: 1
+    },
+    shadowOpacity: 0.25,
+    shadowRadius: 1.5,
+    elevation: 2
+  },
+  bottomButton: {
+    width: 42,
+    height: 42,
+    borderRadius: 21
+  },
+  bottomRightButton: {
+    right: 16
+  },
   searchContainer: {
     gap: 16,
     flexDirection: 'row',