import { Dimensions, Linking, Platform, Text, TextInput, TouchableOpacity, View, Image, StatusBar, ActivityIndicator, ScrollView } from 'react-native'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import * as MapLibreRN from '@maplibre/maplibre-react-native'; import { styles } from './style'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Colors } from 'src/theme'; import { storage, StoreType } from 'src/storage'; import * as turf from '@turf/turf'; import * as Location from 'expo-location'; import { Image as ExpoImage } from 'expo-image'; import SearchIcon from 'assets/icons/search.svg'; import LocationIcon from 'assets/icons/location.svg'; import CloseSvg from 'assets/icons/close.svg'; import FilterIcon from 'assets/icons/filter.svg'; import ProfileIcon from 'assets/icons/bottom-navigation/profile.svg'; import RegionPopup from 'src/components/RegionPopup'; import { useRegion } from 'src/contexts/RegionContext'; import { qualityOptions } from '../TravelsScreen/utils/constants'; import { AvatarWithInitials, EditNmModal, WarningModal } from 'src/components'; import { API_HOST, VECTOR_MAP_HOST } from 'src/constants'; import { NAVIGATION_PAGES } from 'src/types'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { getData } from 'src/modules/map/regionData'; import { getCountriesDatabase, getFirstDatabase, getSecondDatabase, refreshDatabases } from 'src/db'; import { fetchUserData, fetchUserDataDare, useGetListRegionsQuery } from '@api/regions'; import { SQLiteDatabase } from 'expo-sqlite/legacy'; import { useFocusEffect } from '@react-navigation/native'; import { useGetUniversalSearch } from '@api/search'; import { fetchCountryUserData, useGetListCountriesQuery } from '@api/countries'; import SearchModal from './UniversalSearch'; import EditModal from '../TravelsScreen/Components/EditSlowModal'; import * as FileSystem from 'expo-file-system'; import CheckSvg from 'assets/icons/mark.svg'; import moment from 'moment'; import { usePostGetVisitedCountriesIdsQuery, usePostGetVisitedDareIdsQuery, usePostGetVisitedRegionsIdsQuery, usePostGetVisitedSeriesIdsQuery } from '@api/maps'; import FilterModal from './FilterModal'; import { useGetListDareQuery } from '@api/myDARE'; import { useGetIconsQuery, usePostSetToggleItem } from '@api/series'; import MarkerItem from './MarkerItem'; import { usePostGetSettingsQuery, usePostGetUsersCountQuery, usePostGetUsersLocationQuery, usePostUpdateLocationMutation } from '@api/location'; import UserItem from './UserItem'; import { useConnection } from 'src/contexts/ConnectionContext'; import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg'; import SeriesIcon from 'assets/icons/travels-section/series.svg'; import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import MapButton from 'src/components/MapButton'; import { useAvatarStore } from 'src/stores/avatarVersionStore'; import _ from 'lodash'; import ScaleBar from 'src/components/ScaleBar'; import MessagesDot from 'src/components/MessagesDot'; import { restartBackgroundLocationUpdates, startBackgroundLocationUpdates, stopBackgroundLocationUpdates } from 'src/utils/backgroundLocation'; const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png'); const defaultUserAvatar = require('assets/icon-user-share-location-solid.png'); const logo = require('assets/logo-ua.png'); const defaultSeriesIcon = require('assets/series-default.png'); MapLibreRN.Logger.setLogLevel('error'); const generateFilter = (ids: number[]) => { return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1]; }; // to do refactor let regions_visited = { id: 'regions_visited', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(255, 126, 0, 1)', fillOpacity: 0.6, fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: generateFilter([]), maxzoom: 10 }; let countries_visited = { id: 'countries_visited', type: 'fill', source: 'countries', 'source-layer': 'countries', style: { fillColor: 'rgba(255, 126, 0, 1)', fillOpacity: 0.6, fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: generateFilter([]), maxzoom: 10 }; let dare_visited = { id: 'dare_visited', type: 'fill', source: 'dare', 'source-layer': 'dare', style: { fillColor: 'rgba(255, 126, 0, 0.6)', fillOutlineColor: 'rgba(255, 126, 0, 0)' }, filter: generateFilter([]), maxzoom: 12 }; let regions = { id: 'regions', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(15, 63, 79, 0)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let countries = { id: 'countries', type: 'fill', source: 'countries', 'source-layer': 'countries', style: { fillColor: 'rgba(15, 63, 79, 0)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let dare = { id: 'dare', type: 'fill', source: 'dare', 'source-layer': 'dare', style: { fillColor: 'rgba(14, 80, 109, 0.6)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let selected_region = { id: 'selected_region', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(57, 115, 172, 0.3)' }, maxzoom: 12 }; let selected_region_outline = { id: 'selected_region_outline', type: 'line', source: 'regions', 'source-layer': 'regions', style: { lineColor: '#ED9334', lineTranslate: [0, 0], lineTranslateAnchor: 'map', lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 2, 4, 3, 5, 4, 12, 5] }, maxzoom: 12 }; let series_layer = { id: 'series_layer', type: 'symbol', source: 'nomadmania_series', 'source-layer': 'series', minzoom: 6, maxzoom: 60, layout: { 'symbol-spacing': 1, 'icon-image': '{series_id}', 'icon-size': 0.15, 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'text-anchor': 'top', 'text-field': '{name}', 'text-font': ['Noto Sans Regular'], 'text-max-width': 9, 'text-offset': [0, 0.6], 'text-padding': 2, 'text-size': 12, visibility: 'visible', 'text-optional': true, 'text-ignore-placement': false, 'text-allow-overlap': false }, paint: { 'text-color': '#666', 'text-halo-blur': 0.5, 'text-halo-color': '#ffffff', 'text-halo-width': 1 }, filter: generateFilter([]) }; let series_visited = { id: 'series_visited', type: 'symbol', source: 'nomadmania_series', 'source-layer': 'series', minzoom: 6, maxzoom: 60, layout: { 'symbol-spacing': 1, 'icon-image': '{series_id}v', 'icon-size': 0.15, 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'text-anchor': 'top', 'text-field': '{name}', 'text-font': ['Noto Sans Regular'], 'text-max-width': 9, 'text-offset': [0, 0.6], 'text-padding': 2, 'text-size': 12, visibility: 'visible', 'text-optional': true, 'text-ignore-placement': false, 'text-allow-overlap': false }, paint: { 'text-color': '#666', 'text-halo-blur': 0.5, 'text-halo-color': '#ffffff', 'text-halo-width': 1 }, filter: generateFilter([]) }; const INITIAL_REGION = { latitude: 0, longitude: 0, latitudeDelta: 180, longitudeDelta: 180 }; const ICONS_DIR = FileSystem.documentDirectory + 'series_icons/'; const MapScreen: any = ({ navigation, route }: { navigation: any; route: any }) => { const tabBarHeight = useBottomTabBarHeight(); const userId = storage.get('uid', StoreType.STRING) as string; const token = storage.get('token', StoreType.STRING) as string; const [isConnected, setIsConnected] = useState<boolean>(true); const netInfo = useConnection(); const { avatarVersion } = useAvatarStore(); const { data: usersOnMapCount } = usePostGetUsersCountQuery(token, !!token && isConnected); const { data: regionsList } = useGetListRegionsQuery(isConnected); const { data: countriesList } = useGetListCountriesQuery(isConnected); const { data: dareList } = useGetListDareQuery(isConnected); const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 }); const tilesTypes = [ { label: 'Blank', value: -1 }, { label: 'NM regions', value: 0 }, { label: 'UN countries', value: 1 }, { label: 'DARE places', value: 2 } ]; const [type, setType] = useState<'regions' | 'countries' | 'dare' | 'blank'>('regions'); const [seriesFilter, setSeriesFilter] = useState<any>({ visible: true, groups: [], applied: false, status: -1 }); const [regionsFilter, setRegionsFilter] = useState<any>({ visitedLabel: 'by', year: moment().year() }); const [showNomads, setShowNomads] = useState( (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false ); const { data: locationSettings, refetch } = usePostGetSettingsQuery( token, !!token && isConnected ); const { mutateAsync: updateLocation } = usePostUpdateLocationMutation(); const { data: visitedRegionIds, refetch: refetchVisitedRegions } = usePostGetVisitedRegionsIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'regions' && !!userId && isConnected ); const { data: visitedCountryIds, refetch: refetchVisitedCountries } = usePostGetVisitedCountriesIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'countries' && !!userId && isConnected ); const { data: visitedDareIds, refetch: refetchVisitedDare } = usePostGetVisitedDareIdsQuery( token, +userId, type === 'dare' && !!userId && isConnected ); const { data: visitedSeriesIds } = usePostGetVisitedSeriesIdsQuery( token, !!userId && isConnected ); const { data: seriesIcons } = useGetIconsQuery(isConnected); const userInfo = storage.get('currentUserData', StoreType.STRING) as string; const { mutateAsync: mutateUserData } = fetchUserData(); const { mutateAsync: mutateUserDataDare } = fetchUserDataDare(); const { mutateAsync: mutateCountriesData } = fetchCountryUserData(); const [selectedRegion, setSelectedRegion] = useState<number | null>(null); const [initialRegion, setInitialRegion] = useState(INITIAL_REGION); const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false); const [regionData, setRegionData] = useState<any | null>(null); const [location, setLocation] = useState<any | null>(null); const [userAvatars, setUserAvatars] = useState<string[]>([]); const [userInfoData, setUserInfoData] = useState<any>(null); const [selectedMarker, setSelectedMarker] = useState<any>(null); const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false); const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false); const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false); const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState<boolean>(false); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isFilterVisible, setIsFilterVisible] = useState<string | null>(null); const [isLocationLoading, setIsLocationLoading] = useState(false); const [modalState, setModalState] = useState({ selectedFirstYear: 2021, selectedLastYear: 2021, selectedQuality: qualityOptions[2], selectedNoOfVisits: 1, years: [], id: null }); const [isExpanded, setIsExpanded] = useState(false); const [search, setSearch] = useState(''); const { data: searchData } = useGetUniversalSearch(search, search.length > 0); const [searchInput, setSearchInput] = useState(''); const [searchVisible, setSearchVisible] = useState<boolean>(false); const [index, setIndex] = useState<number>(0); const width = useSharedValue(48); const usableWidth = Dimensions.get('window').width - 32; const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion(); const [db1, setDb1] = useState<SQLiteDatabase | null>(null); const [db2, setDb2] = useState<SQLiteDatabase | null>(null); const [db3, setDb3] = useState<SQLiteDatabase | null>(null); const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([])); const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([])); const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([])); const [seriesVisitedFilter, setSeriesVisitedFilter] = useState(generateFilter([])); const [seriesNotVisitedFilter, setSeriesNotVisitedFilter] = useState(generateFilter([])); const [regionsVisited, setRegionsVisited] = useState<any[]>([]); const [countriesVisited, setCountriesVisited] = useState<any[]>([]); const [dareVisited, setDareVisited] = useState<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, refetch: refetchUsersLocation } = usePostGetUsersLocationQuery( token, !!token && showNomads && Boolean(location) && isConnected ); const [selectedUser, setSelectedUser] = useState<any>(null); const [zoom, setZoom] = useState(0); const [center, setCenter] = useState<number[] | null>(null); const [isZooming, setIsZooming] = useState(true); const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const [markerCoords, setMarkerCoords] = useState<any>(null); const [interval, setInterval] = useState(0); const isSmallScreen = Dimensions.get('window').width < 383; const processedImages = useRef(new Set<string>()); useEffect(() => { if (netInfo && netInfo.isConnected !== null) { setIsConnected(netInfo.isConnected); } }, [netInfo]); useEffect(() => { if (showNomads) { refetchUsersLocation(); } }, [showNomads]); useEffect(() => { if (usersLocation && usersLocation.geojson && showNomads) { const filteredNomads: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: usersLocation.geojson.features.filter( (feature: GeoJSON.Feature) => feature.properties?.id !== +userId ) }; if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) { setNomads(filteredNomads); } } }, [usersLocation, showNomads]); useEffect(() => { const loadCachedIcons = async () => { try { const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR); if (!dirInfo.exists) return; const files = await FileSystem.readDirectoryAsync(ICONS_DIR); const cachedImages: Record<string, { uri: string }> = {}; files.forEach((fileName) => { if (!fileName.endsWith('.png')) return; const key = fileName.replace('.png', ''); cachedImages[key] = { uri: ICONS_DIR + fileName }; processedImages.current.add(key); }); setImages((prev: any) => ({ ...prev, ...cachedImages })); } catch (e) { console.warn('Error loading cached icons:', e); } }; loadCachedIcons(); }, []); useEffect(() => { if (!seriesIcons) return; const updateCacheFromAPI = async () => { const loadedImages: Record<string, { uri: string }> = {}; const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR); if (!dirInfo.exists) { await FileSystem.makeDirectoryAsync(ICONS_DIR, { intermediates: true }); } const promises = seriesIcons.data.map(async (icon) => { const id = icon.id?.toString(); if (!id || processedImages.current.has(id)) return; const imgUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_png}`; const imgVisitedUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_visited_png}`; const localPath = `${ICONS_DIR}${id}.png`; const localPathVisited = `${ICONS_DIR}${id}v.png`; const [imgInfo, visitedInfo] = await Promise.all([ FileSystem.getInfoAsync(localPath), FileSystem.getInfoAsync(localPathVisited) ]); try { if (!imgInfo.exists) { await FileSystem.downloadAsync(imgUrl, localPath); } if (!visitedInfo.exists) { await FileSystem.downloadAsync(imgVisitedUrl, localPathVisited); } } catch (e) { console.warn(`Download failed for ${id}:`, e); return; } processedImages.current.add(id); processedImages.current.add(`${id}v`); loadedImages[id] = { uri: localPath }; loadedImages[`${id}v`] = { uri: localPathVisited }; }); await Promise.all(promises); setImages((prev: any) => ({ ...prev, ...loadedImages })); }; updateCacheFromAPI(); }, [seriesIcons]); useEffect(() => { const loadDatabases = async () => { const firstDb = await getFirstDatabase(); const secondDb = await getSecondDatabase(); const countriesDb = await getCountriesDatabase(); setDb1(firstDb); setDb2(secondDb); setDb3(countriesDb); }; if (!db1 || !db2 || !db3) { loadDatabases(); } }, [db1, db2, db3]); useEffect(() => { const savedFilterSettings = storage.get('filterSettings', StoreType.STRING) as string; if (savedFilterSettings) { const filterSettings = JSON.parse(savedFilterSettings); setTilesType(filterSettings.tilesType); setType(filterSettings.type); setRegionsFilter({ visitedLabel: filterSettings.selectedVisible?.value && filterSettings.selectedVisible.value === 1 ? 'in' : 'by', year: filterSettings.selectedYear?.value ?? moment().year() }); setSeriesFilter(filterSettings.seriesFilter); } }, []); useFocusEffect( useCallback(() => { if (token) { refetchVisitedRegions(); refetchVisitedCountries(); refetchVisitedDare(); } }, [navigation]) ); // to do refactor useEffect(() => { if (visitedRegionIds) { setRegionsVisited(visitedRegionIds.ids); storage.set('visitedRegions', JSON.stringify(visitedRegionIds.ids)); } else { const storedVisited = storage.get('visitedRegions', StoreType.STRING) as string; setRegionsVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedRegionIds]); useEffect(() => { if (visitedCountryIds) { setCountriesVisited(visitedCountryIds.ids); storage.set('visitedCountries', JSON.stringify(visitedCountryIds.ids)); } else { const storedVisited = storage.get('visitedCountries', StoreType.STRING) as string; setCountriesVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedCountryIds]); useEffect(() => { if (visitedDareIds) { setDareVisited(visitedDareIds.ids); storage.set('visitedDares', JSON.stringify(visitedDareIds.ids)); } else { const storedVisited = storage.get('visitedDares', StoreType.STRING) as string; setDareVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedDareIds]); useEffect(() => { if (visitedSeriesIds && token) { setSeriesVisited(visitedSeriesIds.ids); storage.set('visitedSeries', JSON.stringify(visitedSeriesIds.ids)); } else { const storedVisited = storage.get('visitedSeries', StoreType.STRING) as string; setSeriesVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedSeriesIds]); useEffect(() => { if (regionsVisited && regionsVisited.length) { setRegionsVisitedFilter(generateFilter(regionsVisited)); } else { setRegionsVisitedFilter(['==', 'id', -1]); } }, [regionsVisited]); useEffect(() => { if (countriesVisited && countriesVisited.length) { setCountriesVisitedFilter(generateFilter(countriesVisited)); } else { setCountriesVisitedFilter(['==', 'id', -1]); } }, [countriesVisited]); useEffect(() => { if (dareVisited && dareVisited.length) { setDareVisitedFilter(generateFilter(dareVisited)); } else { setDareVisitedFilter(['==', 'id', -1]); } }, [dareVisited]); useEffect(() => { if (!seriesFilter.visible) { setSeriesVisitedFilter(generateFilter([])); setSeriesNotVisitedFilter(generateFilter([])); return; } if (seriesFilter.applied) { if (seriesVisited?.length) { setSeriesVisitedFilter([ 'all', ['any', ...seriesVisited.map((id) => ['==', 'id', id])], ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])] ]); setSeriesNotVisitedFilter([ 'all', ['all', ...seriesVisited.map((id) => ['!=', 'id', id])], ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])] ]); } else { setSeriesNotVisitedFilter([ 'any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId]) ]); } } else { setSeriesVisitedFilter(['any', ...seriesVisited.map((id) => ['==', 'id', id])]); setSeriesNotVisitedFilter(['all', ...seriesVisited.map((id) => ['!=', 'id', id])]); } }, [seriesVisited, seriesFilter]); useEffect(() => { if (route.params?.lon && route.params?.lat) { setMarkerCoords([route.params.lon, route.params.lat]); const timeoutId = setTimeout(() => { if (cameraRef.current) { cameraRef.current.setCamera({ centerCoordinate: [route.params?.lon, route.params?.lat], zoomLevel: 15, animationDuration: 800 }); } else { console.warn('Camera ref is not available.'); } }, 800); return () => clearTimeout(timeoutId); } if (route.params?.id && route.params?.type && db1 && db2 && db3) { handleFindRegion(route.params?.id, route.params?.type); } }, [route, db1, db2, db3]); useFocusEffect( useCallback(() => { if (token) { refetch(); } }, []) ); useEffect(() => { if (interval > 0 && showNomads) { const intervalId = setInterval(() => { if (location && token && showNomads) { refetchUsersLocation(); } }, interval); return () => clearInterval(intervalId); } }, [interval, showNomads]); useEffect(() => { (async () => { let { status } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (locationSettings && locationSettings.sharing_refresh_interval) { setInterval(locationSettings.sharing_refresh_interval * 1000); } if ( status !== 'granted' || !token || (locationSettings && locationSettings.sharing === 0) || !isServicesEnabled ) { setShowNomads(false); storage.set('showNomads', false); await stopBackgroundLocationUpdates(); return; } const bgStatus = await Location.getBackgroundPermissionsAsync(); if (bgStatus.status !== 'granted') { const { status } = await Location.requestBackgroundPermissionsAsync(); if (status === Location.PermissionStatus.GRANTED) { await startBackgroundLocationUpdates(); } else { await stopBackgroundLocationUpdates(); } } else { // await startBackgroundLocationUpdates(); await restartBackgroundLocationUpdates(); } try { let currentLocation = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); setLocation(currentLocation.coords); if (locationSettings && locationSettings.sharing === 1 && token) { updateLocation({ token, lat: currentLocation.coords.latitude, lng: currentLocation.coords.longitude }); showNomads && refetchUsersLocation(); } } catch (error) { console.error('Error fetching user location:', error); } })(); }, [locationSettings]); useEffect(() => { const currentYear = moment().year(); let yearSelector: { label: string; value: number }[] = [{ label: 'visited', value: 1 }]; for (let i = currentYear; i >= 1951; i--) { yearSelector.push({ label: i.toString(), value: i }); } handleModalStateChange({ years: yearSelector }); }, []); useFocusEffect( useCallback(() => { navigation.getParent()?.setOptions({ tabBarStyle: { display: regionPopupVisible ? 'none' : 'flex', position: 'absolute', ...Platform.select({ android: { height: 58 } }) } }); }, [regionPopupVisible, navigation]) ); const mapRef = useRef<MapLibreRN.MapViewRef>(null); const cameraRef = useRef<MapLibreRN.CameraRef>(null); const shapeSourceRef = useRef<MapLibreRN.ShapeSourceRef>(null); useEffect(() => { if (userInfo) { setUserInfoData(JSON.parse(userInfo)); } }, [userInfo]); const handlePress = () => { if (isExpanded) { setSearchInput(''); } setIsExpanded((prev) => !prev); width.value = withTiming(isExpanded ? 48 : usableWidth, { duration: 300, easing: Easing.inOut(Easing.ease) }); }; const animatedStyle = useAnimatedStyle(() => { return { width: width.value }; }); const loadInitialRegion = () => { try { const savedInitialRegion = storage.get('initialRegion', StoreType.STRING) as string; if (savedInitialRegion) { const region = JSON.parse(savedInitialRegion); setInitialRegion(region); } } catch (e) { console.error('Failed to load saved initial region:', e); } }; useEffect(() => { loadInitialRegion(); }, []); useEffect(() => { if (initialRegion && !route.params?.id) { const timeoutId = setTimeout(() => { if (cameraRef.current) { cameraRef.current.setCamera({ centerCoordinate: [initialRegion.longitude, initialRegion.latitude], zoomLevel: Math.log2(360 / initialRegion.latitudeDelta), animationDuration: 500 }); } else { console.warn('Camera ref is not available.'); } }, 500); return () => clearTimeout(timeoutId); } }, [initialRegion]); const handleMapChange = async () => { if (!mapRef.current) return; if (hideTimer.current) clearTimeout(hideTimer.current); setIsZooming(true); const currentZoom = await mapRef.current.getZoom(); const currentCenter = await mapRef.current.getCenter(); setZoom(currentZoom); setCenter(currentCenter); }; const onMapPress = async (event: any) => { if (!mapRef.current) return; if (selectedMarker || selectedUser) { closeCallout(); return; } if (type === 'blank') return; try { const { screenPointX, screenPointY } = event.properties; const { features } = await mapRef.current.queryRenderedFeaturesAtPoint( [screenPointX, screenPointY], undefined, ['regions', 'countries', 'dare'] ); if (features?.length) { const region = features[0]; if (selectedRegion === region.properties?.id) return; let db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2; let tableName = type === 'dare' ? 'places' : type; let foundRegion = region.properties?.id; setSelectedRegion(region.properties?.id); await getData(db, foundRegion, tableName, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); if (tableName === 'regions') { token ? await mutateUserData( { region_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id: +foundRegion, ...data }); } } ) : setUserData({ type: 'nm', id: +foundRegion }); if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else if (tableName === 'countries') { token ? await mutateCountriesData( { id: +foundRegion, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id: +foundRegion, ...data.data }); } } ) : setUserData({ type: 'countries', id: +foundRegion }); if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else { token ? await mutateUserDataDare( { dare_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +foundRegion, ...data }); } } ) : setUserData({ type: 'dare', id: +foundRegion }); if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { handleClosePopup(); } } catch (error) { console.error('Error onMapPress features:', error); } }; const handleRegionDidChange = async (feature: GeoJSON.Feature<GeoJSON.Point, any>) => { hideTimer.current = setTimeout(() => { setIsZooming(false); }, 2000); if (!feature) return; const { zoomLevel } = feature.properties; const { coordinates } = feature.geometry; if (!zoomLevel || !coordinates) return; const latitudeDelta = 360 / 2 ** zoomLevel; const longitudeDelta = latitudeDelta; const region = { latitude: coordinates[1], longitude: coordinates[0], latitudeDelta, longitudeDelta }; storage.set('initialRegion', JSON.stringify(region)); }; const handleClosePopup = async () => { setSelectedRegion(null); setRegionPopupVisible(false); setRegionData(null); }; const handleGetLocation = async () => { setIsLocationLoading(true); try { let { status, canAskAgain } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (status === 'granted' && isServicesEnabled) { const bgStatus = await Location.getBackgroundPermissionsAsync(); if (bgStatus.status !== 'granted') { const { status } = await Location.requestBackgroundPermissionsAsync(); if (status === Location.PermissionStatus.GRANTED) { await startBackgroundLocationUpdates(); } else { await stopBackgroundLocationUpdates(); } } else { await startBackgroundLocationUpdates(); } 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 ); } if (locationSettings && locationSettings.sharing === 1 && token) { updateLocation({ token, lat: currentLocation.coords.latitude, lng: currentLocation.coords.longitude }); showNomads && refetchUsersLocation(); } handleClosePopup(); } 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); } }; const handleOpenEditModal = () => { handleModalStateChange({ selectedFirstYear: userData?.first_visit_year, selectedLastYear: userData?.last_visit_year, selectedQuality: qualityOptions.find((quality) => quality.id === userData?.best_visit_quality) || qualityOptions[2], selectedNoOfVisits: userData?.no_of_visits || 1, id: regionData?.id }); setIsEditModalVisible(true); }; const handleOpenEditSlowModal = () => { setIsEditSlowModalVisible(true); }; const handleSearch = async () => { setSearch(searchInput); setSearchVisible(true); }; const handleCloseModal = () => { setSearchInput(''); setSearchVisible(false); handlePress(); }; const handleRegionData = async (regionData: any, avatars: string[]) => { if (!regionData) { await refreshDatabases(); } setRegionData(regionData); setUserAvatars(avatars); }; const handleFindRegion = async (id: number, type: 'regions' | 'countries' | 'places') => { setType(type === 'places' ? 'dare' : type); if (!db1 || !db2 || !db3) { return; } const db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2; if (id) { setSelectedRegion(id); await getData(db, id, type, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); if (type === 'regions') { token ? await mutateUserData( { region_id: id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id, ...data }); } } ) : setUserData({ type: 'nm', id }); if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else if (type === 'countries') { token ? await mutateCountriesData( { id, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id, ...data.data }); } } ) : setUserData({ type: 'countries', id }); if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else { token ? await mutateUserDataDare( { dare_id: +id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +id, ...data }); } } ) : setUserData({ type: 'dare', id: +id }); if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { handleClosePopup(); } }; const handleMarkerPress = async (event: any) => { const { features } = event; if (features?.length) { const selectedFeature = features[0]; const { coordinates } = selectedFeature.geometry; const visited = seriesVisited.includes(selectedFeature.properties.id) ? 1 : 0; const icon = images[selectedFeature.properties.series_id]; const { name, description, series_name, series_id, id } = selectedFeature.properties; if (coordinates) { setSelectedMarker({ coordinates, name, icon, description, series_name, visited, series_id, id }); setSelectedUser(null); } } }; const closeCallout = () => { setSelectedMarker(null); setSelectedUser(null); }; const toggleSeries = useCallback( async (item: any) => { if (!token) { setIsWarningModalVisible(true); return; } const itemData = { token, series_id: item.series_id, item_id: item.id, checked: (item.visited === 0 ? 1 : 0) as 0 | 1, double: 0 as 0 | 1 }; try { updateSeriesItem(itemData); if (item.visited === 1) { setSeriesVisited((current) => current.filter((id) => id !== item.id)); setSelectedMarker((current: any) => ({ ...current, visited: 0 })); } else { setSeriesVisited((current) => [...current, item.id]); setSelectedMarker((current: any) => ({ ...current, visited: 1 })); } } catch (error) { console.error('Failed to update series state', error); } }, [token, updateSeriesItem] ); const handleModalStateChange = (updates: { [key: string]: any }) => { setModalState((prevState) => ({ ...prevState, ...updates })); }; const handleUserPress = (event: any) => { const selectedFeature = event.features[0]; const { coordinates } = selectedFeature.geometry; const { avatar, first_name, last_name, flag, id, last_seen } = selectedFeature.properties; if (selectedFeature) { setSelectedUser({ coordinates, avatar: avatar ? { uri: API_HOST + avatar } : logo, first_name, last_name, flag: { uri: API_HOST + flag }, id, last_seen }); setSelectedMarker(null); } }; return ( <SafeAreaView style={{ height: '100%' }}> <StatusBar translucent backgroundColor="transparent" /> <MapLibreRN.MapView ref={mapRef} style={styles.map} mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'} rotateEnabled={false} attributionEnabled={false} onPress={onMapPress} onRegionDidChange={handleRegionDidChange} onRegionIsChanging={handleMapChange} onRegionWillChange={_.debounce(handleMapChange, 200)} > <MapLibreRN.Images images={images} onImageMissing={(image) => { if (processedImages.current.has(image)) { return; } processedImages.current.add(image); setImages((prevImages: any) => ({ ...prevImages, [image]: defaultSeriesIcon })); }} > <View /> </MapLibreRN.Images> {markerCoords && ( <MapLibreRN.PointAnnotation id="marker" coordinate={markerCoords}> <View style={{ height: 24, width: 24, backgroundColor: Colors.ORANGE, borderRadius: 12, borderColor: Colors.WHITE, borderWidth: 2 }} /> </MapLibreRN.PointAnnotation> )} {type === 'regions' && ( <> <MapLibreRN.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" /> <MapLibreRN.FillLayer id={regions.id} sourceID={regions.source} sourceLayerID={regions['source-layer']} filter={regions.filter as any} style={regions.style} maxZoomLevel={regions.maxzoom} belowLayerID={regions_visited.id} /> <MapLibreRN.FillLayer id={regions_visited.id} sourceID={regions_visited.source} sourceLayerID={regions_visited['source-layer']} filter={regionsVisitedFilter as any} style={regions_visited.style} maxZoomLevel={regions_visited.maxzoom} belowLayerID="waterway-name" /> </> )} {type === 'countries' && ( <> <MapLibreRN.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" /> <MapLibreRN.FillLayer id={countries.id} sourceID={countries.source} sourceLayerID={countries['source-layer']} filter={countries.filter as any} style={countries.style} maxZoomLevel={countries.maxzoom} belowLayerID={countries_visited.id} /> <MapLibreRN.FillLayer id={countries_visited.id} sourceID={countries_visited.source} sourceLayerID={countries_visited['source-layer']} filter={countriesVisitedFilter as any} style={countries_visited.style} maxZoomLevel={countries_visited.maxzoom} belowLayerID="waterway-name" /> </> )} {type === 'dare' && ( <> <MapLibreRN.FillLayer id={dare.id} sourceID={dare.source} sourceLayerID={dare['source-layer']} filter={dare.filter as any} style={dare.style} maxZoomLevel={dare.maxzoom} belowLayerID={dare_visited.id} /> <MapLibreRN.FillLayer id={dare_visited.id} sourceID={dare_visited.source} sourceLayerID={dare_visited['source-layer']} filter={dareVisitedFilter as any} style={dare_visited.style} maxZoomLevel={dare_visited.maxzoom} belowLayerID="waterway-name" /> </> )} {selectedRegion && type && ( <> <MapLibreRN.FillLayer id={selected_region.id} sourceID={type} sourceLayerID={type} filter={['==', 'id', selectedRegion]} style={selected_region.style} maxZoomLevel={selected_region.maxzoom} belowLayerID="waterway-name" /> <MapLibreRN.LineLayer id={selected_region_outline.id} sourceID={type} sourceLayerID={type} filter={['==', 'id', selectedRegion]} style={selected_region_outline.style as any} maxZoomLevel={selected_region_outline.maxzoom} belowLayerID="waterway-name" /> </> )} <MapLibreRN.VectorSource id="nomadmania_series" tileUrlTemplates={[VECTOR_MAP_HOST + '/tiles/series/{z}/{x}/{y}.pbf']} onPress={handleMarkerPress} > {seriesFilter.status !== 1 && Object.keys(images).length > 0 ? ( <MapLibreRN.SymbolLayer id={series_layer.id} sourceID={series_layer.source} sourceLayerID={series_layer['source-layer']} aboveLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined} filter={seriesNotVisitedFilter as any} minZoomLevel={series_layer.minzoom} maxZoomLevel={series_layer.maxzoom} style={{ symbolSpacing: 1, iconImage: ['get', 'series_id'], iconSize: 0.51, iconAllowOverlap: true, iconIgnorePlacement: true, visibility: 'visible', iconColor: '#666', iconOpacity: 1, iconHaloColor: '#ffffff', iconHaloWidth: 1, iconHaloBlur: 0.5 }} /> ) : ( <></> )} {seriesFilter.status !== 0 && Object.keys(images).length > 0 ? ( <MapLibreRN.SymbolLayer id={series_visited.id} sourceID={series_visited.source} sourceLayerID={series_visited['source-layer']} aboveLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined} filter={seriesVisitedFilter as any} minZoomLevel={series_visited.minzoom} maxZoomLevel={series_visited.maxzoom} style={{ symbolSpacing: 1, iconImage: '{series_id}v', iconSize: 0.51, iconAllowOverlap: true, iconIgnorePlacement: true, visibility: 'visible', iconColor: '#666', iconOpacity: 1, iconHaloColor: '#ffffff', iconHaloWidth: 1, iconHaloBlur: 0.5 }} /> ) : ( <></> )} </MapLibreRN.VectorSource> {nomads && showNomads && ( <MapLibreRN.ShapeSource ref={shapeSourceRef} tolerance={20} id="nomads" shape={nomads} onPress={async (event) => { const feature = event.features[0]; const isCluster = feature.properties?.cluster; if (isCluster) { const clusterCoordinates = (feature.geometry as GeoJSON.Point).coordinates; const zoom = await shapeSourceRef.current?.getClusterExpansionZoom( feature as GeoJSON.Feature<GeoJSON.Geometry> ); const newZoom = zoom ?? 2; cameraRef.current?.setCamera({ centerCoordinate: clusterCoordinates, zoomLevel: newZoom, animationDuration: 500, animationMode: 'flyTo' }); return; } else { handleUserPress(event); } }} cluster={true} clusterRadius={50} > <MapLibreRN.SymbolLayer id="nomads_circle" filter={['has', 'point_count']} aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined} style={{ iconImage: clusteredUsersIcon, iconSize: [ 'interpolate', ['linear'], ['get', 'point_count'], 0, 0.33, 10, 0.35, 20, 0.37, 50, 0.39, 75, 0.41, 100, 0.43 ], iconAllowOverlap: true }} ></MapLibreRN.SymbolLayer> <MapLibreRN.SymbolLayer id="nomads_count" filter={['has', 'point_count']} aboveLayerID={Platform.OS === 'android' ? 'nomads_circle' : undefined} style={{ textField: [ 'case', ['<', ['get', 'point_count'], 1000], ['get', 'point_count'], ['concat', ['/', ['round', ['/', ['get', 'point_count'], 100]], 10], 'k'] ], textFont: ['Noto Sans Bold'], textSize: [ 'interpolate', ['linear'], ['get', 'point_count'], 0, 13.5, 20, 14, 75, 15 ], textColor: '#FFFFFF', textAnchor: 'center', textOffset: [ 'interpolate', ['linear'], ['get', 'point_count'], 0, ['literal', [0, 0.85]], 20, ['literal', [0, 0.92]], 75, ['literal', [0, 1]] ], textAllowOverlap: true }} /> <MapLibreRN.SymbolLayer id="nomads_symbol" filter={['!', ['has', 'point_count']]} aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined} style={{ iconImage: defaultUserAvatar, iconSize: [ 'interpolate', ['linear'], ['zoom'], 0, 0.24, 5, 0.28, 10, 0.33, 15, 0.38, 20, 0.42 ], iconAllowOverlap: true }} ></MapLibreRN.SymbolLayer> </MapLibreRN.ShapeSource> )} {selectedUser && <UserItem marker={selectedUser} />} {selectedMarker && ( <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} /> )} <MapLibreRN.Camera ref={cameraRef} /> {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' }); }} > {/* to do custom user location */} </MapLibreRN.UserLocation> )} </MapLibreRN.MapView> {center ? ( <ScaleBar zoom={zoom} latitude={center[1]} isVisible={isZooming} bottom={tabBarHeight + 80} /> ) : null} {regionPopupVisible && regionData ? ( <> <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]} onPress={handleClosePopup} > <CloseSvg fill="white" width={13} height={13} /> <Text style={styles.textClose}>Close</Text> </TouchableOpacity> <TouchableOpacity onPress={handleGetLocation} style={[ styles.cornerButton, styles.topRightButton, styles.bottomButton, { bottom: tabBarHeight + 20 } ]} > {isLocationLoading ? ( <ActivityIndicator size="small" color={Colors.DARK_BLUE} /> ) : ( <LocationIcon /> )} </TouchableOpacity> <RegionPopup region={regionData} userAvatars={userAvatars} userData={userData} openEditModal={handleOpenEditModal} updateNM={(id, first, last, visits, quality) => { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateNM(id, first, last, visits, quality); const updatedIds = regionsVisited.includes(id) ? regionsVisited.filter((visitedId) => visitedId !== id) : [...regionsVisited, id]; setRegionsVisited(updatedIds); refetchVisitedCountries(); }} updateDare={(id, visits) => { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateDare(id, visits); const updatedIds = dareVisited.includes(id) ? dareVisited.filter((visitedId) => visitedId !== id) : [...dareVisited, id]; setDareVisited(updatedIds); }} disabled={!token || !isConnected} updateSlow={(id, v, s11, s31, s101) => { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateSlow(id, v, s11, s31, s101); const updatedIds = countriesVisited.includes(id) ? countriesVisited.filter((visitedId) => visitedId !== id) : [...countriesVisited, id]; setCountriesVisited(updatedIds); }} openEditSlowModal={handleOpenEditSlowModal} /> </> ) : ( <> {!isExpanded ? ( <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]} onPress={() => navigation.navigate(NAVIGATION_PAGES.PROFILE_TAB)} > {token ? ( userInfoData?.avatar ? ( <Image style={styles.avatar} source={{ uri: API_HOST + '/img/avatars/' + userInfoData?.avatar + '?v=' + avatarVersion }} /> ) : ( <AvatarWithInitials text={`${userInfoData?.first_name ? userInfoData?.first_name[0] : ''}${userInfoData?.last_name ? userInfoData?.last_name[0] : ''}`} flag={API_HOST + '/img/flags_new/' + userInfoData?.homebase_flag} size={48} borderColor={Colors.WHITE} /> ) ) : ( <ProfileIcon fill={Colors.DARK_BLUE} /> )} </TouchableOpacity> ) : null} <Animated.View style={[ styles.searchContainer, styles.cornerButton, styles.topLeftButton, animatedStyle, { padding: 5 } ]} > {isExpanded ? ( <> <TouchableOpacity onPress={handlePress} style={styles.iconButton}> <CloseSvg fill={'#0F3F4F'} /> </TouchableOpacity> <TextInput style={styles.input} placeholder="Search regions, places, nomads" placeholderTextColor={Colors.LIGHT_GRAY} value={searchInput} onChangeText={(text) => setSearchInput(text)} onSubmitEditing={handleSearch} /> <TouchableOpacity onPress={handleSearch} style={styles.iconButton}> <SearchIcon fill={'#0F3F4F'} /> </TouchableOpacity> </> ) : ( <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}> <SearchIcon fill={'#0F3F4F'} /> </TouchableOpacity> )} </Animated.View> <View style={[styles.tabs, { bottom: tabBarHeight + 20 }]}> <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 12, paddingTop: 6, gap: isSmallScreen ? 8 : 12, flexDirection: 'row' }} > <MapButton onPress={() => { try { setIsFilterVisible('regions'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={TravelsIcon} text="Travels" active={type !== 'blank'} /> <MapButton onPress={() => { try { setIsFilterVisible('series'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={SeriesIcon} text="Series" active={seriesFilter.visible} /> {token ? ( <MapButton onPress={() => { try { setIsFilterVisible('nomads'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={NomadsIcon} text="Nomads" active={showNomads} > {usersOnMapCount && usersOnMapCount?.count > 0 ? ( <MessagesDot messagesCount={usersOnMapCount.count} fullNumber={true} right={-10} top={-8} /> ) : null} </MapButton> ) : null} </ScrollView> </View> <TouchableOpacity onPress={handleGetLocation} style={[ styles.cornerButton, styles.bottomButton, styles.bottomRightButton, { bottom: tabBarHeight + 20 } ]} > {isLocationLoading ? ( <ActivityIndicator size="small" color={Colors.DARK_BLUE} /> ) : ( <LocationIcon /> )} </TouchableOpacity> </> )} <SearchModal searchVisible={searchVisible} handleCloseModal={handleCloseModal} handleFindRegion={handleFindRegion} index={index} searchData={searchData} setIndex={setIndex} token={token} /> <WarningModal type={'unauthorized'} isVisible={isWarningModalVisible} onClose={() => setIsWarningModalVisible(false)} /> <EditNmModal isVisible={isEditModalVisible} onClose={() => setIsEditModalVisible(false)} modalState={modalState} updateModalState={handleModalStateChange} updateNM={handleUpdateNM} /> <FilterModal isFilterVisible={isFilterVisible} setIsFilterVisible={setIsFilterVisible} tilesTypes={tilesTypes} tilesType={tilesType} setTilesType={setTilesType} setType={setType} userId={userId ? +userId : 0} setRegionsFilter={setRegionsFilter} setSeriesFilter={setSeriesFilter} setShowNomads={setShowNomads} showNomads={showNomads} isPublicView={false} isLogged={token ? true : false} usersOnMapCount={token && usersOnMapCount?.count ? usersOnMapCount.count : null} isConnected={isConnected} /> <EditModal isVisible={isEditSlowModalVisible} onClose={() => setIsEditSlowModalVisible(false)} item={{ ...userData, country_id: regionData?.id }} updateSlow={(id, v, s11, s31, s101) => handleUpdateSlow(id, v, s11, s31, s101)} /> <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={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?" /> </SafeAreaView> ); }; export default MapScreen;