index.tsx 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971
  1. import {
  2. Dimensions,
  3. Linking,
  4. Platform,
  5. Text,
  6. TextInput,
  7. TouchableOpacity,
  8. View,
  9. Image,
  10. StatusBar,
  11. ActivityIndicator,
  12. ScrollView
  13. } from 'react-native';
  14. import React, { useEffect, useRef, useState, useCallback } from 'react';
  15. import * as MapLibreRN from '@maplibre/maplibre-react-native';
  16. import { styles } from './style';
  17. import { SafeAreaView } from 'react-native-safe-area-context';
  18. import { Colors } from 'src/theme';
  19. import { storage, StoreType } from 'src/storage';
  20. import * as turf from '@turf/turf';
  21. import * as Location from 'expo-location';
  22. import { Image as ExpoImage } from 'expo-image';
  23. import SearchIcon from 'assets/icons/search.svg';
  24. import LocationIcon from 'assets/icons/location.svg';
  25. import CloseSvg from 'assets/icons/close.svg';
  26. import FilterIcon from 'assets/icons/filter.svg';
  27. import ProfileIcon from 'assets/icons/bottom-navigation/profile.svg';
  28. import RegionPopup from 'src/components/RegionPopup';
  29. import { useRegion } from 'src/contexts/RegionContext';
  30. import { qualityOptions } from '../TravelsScreen/utils/constants';
  31. import { AvatarWithInitials, EditNmModal, WarningModal } from 'src/components';
  32. import { API_HOST, VECTOR_MAP_HOST } from 'src/constants';
  33. import { NAVIGATION_PAGES } from 'src/types';
  34. import Animated, {
  35. Easing,
  36. useAnimatedStyle,
  37. useSharedValue,
  38. withTiming
  39. } from 'react-native-reanimated';
  40. import { getData } from 'src/modules/map/regionData';
  41. import {
  42. getCountriesDatabase,
  43. getFirstDatabase,
  44. getSecondDatabase,
  45. refreshDatabases
  46. } from 'src/db';
  47. import { fetchUserData, fetchUserDataDare, useGetListRegionsQuery } from '@api/regions';
  48. import { SQLiteDatabase } from 'expo-sqlite/legacy';
  49. import { useFocusEffect } from '@react-navigation/native';
  50. import { useGetUniversalSearch } from '@api/search';
  51. import { fetchCountryUserData, useGetListCountriesQuery } from '@api/countries';
  52. import SearchModal from './UniversalSearch';
  53. import EditModal from '../TravelsScreen/Components/EditSlowModal';
  54. import * as FileSystem from 'expo-file-system';
  55. import CheckSvg from 'assets/icons/mark.svg';
  56. import moment from 'moment';
  57. import {
  58. usePostGetVisitedCountriesIdsQuery,
  59. usePostGetVisitedDareIdsQuery,
  60. usePostGetVisitedRegionsIdsQuery,
  61. usePostGetVisitedSeriesIdsQuery
  62. } from '@api/maps';
  63. import FilterModal from './FilterModal';
  64. import { useGetListDareQuery } from '@api/myDARE';
  65. import { useGetIconsQuery, usePostSetToggleItem } from '@api/series';
  66. import MarkerItem from './MarkerItem';
  67. import {
  68. usePostGetSettingsQuery,
  69. usePostGetUsersCountQuery,
  70. usePostGetUsersLocationQuery,
  71. usePostUpdateLocationMutation
  72. } from '@api/location';
  73. import UserItem from './UserItem';
  74. import { useConnection } from 'src/contexts/ConnectionContext';
  75. import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg';
  76. import SeriesIcon from 'assets/icons/travels-section/series.svg';
  77. import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
  78. import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
  79. import MapButton from 'src/components/MapButton';
  80. import { useAvatarStore } from 'src/stores/avatarVersionStore';
  81. import _ from 'lodash';
  82. import ScaleBar from 'src/components/ScaleBar';
  83. import MessagesDot from 'src/components/MessagesDot';
  84. import {
  85. restartBackgroundLocationUpdates,
  86. startBackgroundLocationUpdates,
  87. stopBackgroundLocationUpdates
  88. } from 'src/utils/backgroundLocation';
  89. const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png');
  90. const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
  91. const logo = require('assets/logo-ua.png');
  92. const defaultSeriesIcon = require('assets/series-default.png');
  93. MapLibreRN.Logger.setLogLevel('error');
  94. const generateFilter = (ids: number[]) => {
  95. return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
  96. };
  97. // to do refactor
  98. let regions_visited = {
  99. id: 'regions_visited',
  100. type: 'fill',
  101. source: 'regions',
  102. 'source-layer': 'regions',
  103. style: {
  104. fillColor: 'rgba(255, 126, 0, 1)',
  105. fillOpacity: 0.6,
  106. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  107. },
  108. filter: generateFilter([]),
  109. maxzoom: 10
  110. };
  111. let countries_visited = {
  112. id: 'countries_visited',
  113. type: 'fill',
  114. source: 'countries',
  115. 'source-layer': 'countries',
  116. style: {
  117. fillColor: 'rgba(255, 126, 0, 1)',
  118. fillOpacity: 0.6,
  119. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  120. },
  121. filter: generateFilter([]),
  122. maxzoom: 10
  123. };
  124. let dare_visited = {
  125. id: 'dare_visited',
  126. type: 'fill',
  127. source: 'dare',
  128. 'source-layer': 'dare',
  129. style: {
  130. fillColor: 'rgba(255, 126, 0, 0.6)',
  131. fillOutlineColor: 'rgba(255, 126, 0, 0)'
  132. },
  133. filter: generateFilter([]),
  134. maxzoom: 12
  135. };
  136. let regions = {
  137. id: 'regions',
  138. type: 'fill',
  139. source: 'regions',
  140. 'source-layer': 'regions',
  141. style: {
  142. fillColor: 'rgba(15, 63, 79, 0)',
  143. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  144. },
  145. filter: ['all'],
  146. maxzoom: 16
  147. };
  148. let countries = {
  149. id: 'countries',
  150. type: 'fill',
  151. source: 'countries',
  152. 'source-layer': 'countries',
  153. style: {
  154. fillColor: 'rgba(15, 63, 79, 0)',
  155. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  156. },
  157. filter: ['all'],
  158. maxzoom: 16
  159. };
  160. let dare = {
  161. id: 'dare',
  162. type: 'fill',
  163. source: 'dare',
  164. 'source-layer': 'dare',
  165. style: {
  166. fillColor: 'rgba(14, 80, 109, 0.6)',
  167. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  168. },
  169. filter: ['all'],
  170. maxzoom: 16
  171. };
  172. let selected_region = {
  173. id: 'selected_region',
  174. type: 'fill',
  175. source: 'regions',
  176. 'source-layer': 'regions',
  177. style: {
  178. fillColor: 'rgba(57, 115, 172, 0.3)'
  179. },
  180. maxzoom: 12
  181. };
  182. let selected_region_outline = {
  183. id: 'selected_region_outline',
  184. type: 'line',
  185. source: 'regions',
  186. 'source-layer': 'regions',
  187. style: {
  188. lineColor: '#ED9334',
  189. lineTranslate: [0, 0],
  190. lineTranslateAnchor: 'map',
  191. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 2, 4, 3, 5, 4, 12, 5]
  192. },
  193. maxzoom: 12
  194. };
  195. let series_layer = {
  196. id: 'series_layer',
  197. type: 'symbol',
  198. source: 'nomadmania_series',
  199. 'source-layer': 'series',
  200. minzoom: 6,
  201. maxzoom: 60,
  202. layout: {
  203. 'symbol-spacing': 1,
  204. 'icon-image': '{series_id}',
  205. 'icon-size': 0.15,
  206. 'icon-allow-overlap': true,
  207. 'icon-ignore-placement': true,
  208. 'text-anchor': 'top',
  209. 'text-field': '{name}',
  210. 'text-font': ['Noto Sans Regular'],
  211. 'text-max-width': 9,
  212. 'text-offset': [0, 0.6],
  213. 'text-padding': 2,
  214. 'text-size': 12,
  215. visibility: 'visible',
  216. 'text-optional': true,
  217. 'text-ignore-placement': false,
  218. 'text-allow-overlap': false
  219. },
  220. paint: {
  221. 'text-color': '#666',
  222. 'text-halo-blur': 0.5,
  223. 'text-halo-color': '#ffffff',
  224. 'text-halo-width': 1
  225. },
  226. filter: generateFilter([])
  227. };
  228. let series_visited = {
  229. id: 'series_visited',
  230. type: 'symbol',
  231. source: 'nomadmania_series',
  232. 'source-layer': 'series',
  233. minzoom: 6,
  234. maxzoom: 60,
  235. layout: {
  236. 'symbol-spacing': 1,
  237. 'icon-image': '{series_id}v',
  238. 'icon-size': 0.15,
  239. 'icon-allow-overlap': true,
  240. 'icon-ignore-placement': true,
  241. 'text-anchor': 'top',
  242. 'text-field': '{name}',
  243. 'text-font': ['Noto Sans Regular'],
  244. 'text-max-width': 9,
  245. 'text-offset': [0, 0.6],
  246. 'text-padding': 2,
  247. 'text-size': 12,
  248. visibility: 'visible',
  249. 'text-optional': true,
  250. 'text-ignore-placement': false,
  251. 'text-allow-overlap': false
  252. },
  253. paint: {
  254. 'text-color': '#666',
  255. 'text-halo-blur': 0.5,
  256. 'text-halo-color': '#ffffff',
  257. 'text-halo-width': 1
  258. },
  259. filter: generateFilter([])
  260. };
  261. const INITIAL_REGION = {
  262. latitude: 0,
  263. longitude: 0,
  264. latitudeDelta: 180,
  265. longitudeDelta: 180
  266. };
  267. const ICONS_DIR = FileSystem.documentDirectory + 'series_icons/';
  268. const MapScreen: any = ({ navigation, route }: { navigation: any; route: any }) => {
  269. const tabBarHeight = useBottomTabBarHeight();
  270. const userId = storage.get('uid', StoreType.STRING) as string;
  271. const token = storage.get('token', StoreType.STRING) as string;
  272. const [isConnected, setIsConnected] = useState<boolean>(true);
  273. const netInfo = useConnection();
  274. const { avatarVersion } = useAvatarStore();
  275. const { data: usersOnMapCount } = usePostGetUsersCountQuery(token, !!token && isConnected);
  276. const { data: regionsList } = useGetListRegionsQuery(isConnected);
  277. const { data: countriesList } = useGetListCountriesQuery(isConnected);
  278. const { data: dareList } = useGetListDareQuery(isConnected);
  279. const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
  280. const tilesTypes = [
  281. { label: 'Blank', value: -1 },
  282. { label: 'NM regions', value: 0 },
  283. { label: 'UN countries', value: 1 },
  284. { label: 'DARE places', value: 2 }
  285. ];
  286. const [type, setType] = useState<'regions' | 'countries' | 'dare' | 'blank'>('regions');
  287. const [seriesFilter, setSeriesFilter] = useState<any>({
  288. visible: true,
  289. groups: [],
  290. applied: false,
  291. status: -1
  292. });
  293. const [regionsFilter, setRegionsFilter] = useState<any>({
  294. visitedLabel: 'by',
  295. year: moment().year()
  296. });
  297. const [showNomads, setShowNomads] = useState(
  298. (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false
  299. );
  300. const { data: locationSettings, refetch } = usePostGetSettingsQuery(
  301. token,
  302. !!token && isConnected
  303. );
  304. const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
  305. const { data: visitedRegionIds, refetch: refetchVisitedRegions } =
  306. usePostGetVisitedRegionsIdsQuery(
  307. token,
  308. regionsFilter.visitedLabel,
  309. regionsFilter.year,
  310. +userId,
  311. type === 'regions' && !!userId && isConnected
  312. );
  313. const { data: visitedCountryIds, refetch: refetchVisitedCountries } =
  314. usePostGetVisitedCountriesIdsQuery(
  315. token,
  316. regionsFilter.visitedLabel,
  317. regionsFilter.year,
  318. +userId,
  319. type === 'countries' && !!userId && isConnected
  320. );
  321. const { data: visitedDareIds, refetch: refetchVisitedDare } = usePostGetVisitedDareIdsQuery(
  322. token,
  323. +userId,
  324. type === 'dare' && !!userId && isConnected
  325. );
  326. const { data: visitedSeriesIds } = usePostGetVisitedSeriesIdsQuery(
  327. token,
  328. !!userId && isConnected
  329. );
  330. const { data: seriesIcons } = useGetIconsQuery(isConnected);
  331. const userInfo = storage.get('currentUserData', StoreType.STRING) as string;
  332. const { mutateAsync: mutateUserData } = fetchUserData();
  333. const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
  334. const { mutateAsync: mutateCountriesData } = fetchCountryUserData();
  335. const [selectedRegion, setSelectedRegion] = useState<number | null>(null);
  336. const [initialRegion, setInitialRegion] = useState(INITIAL_REGION);
  337. const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false);
  338. const [regionData, setRegionData] = useState<any | null>(null);
  339. const [location, setLocation] = useState<any | null>(null);
  340. const [userAvatars, setUserAvatars] = useState<string[]>([]);
  341. const [userInfoData, setUserInfoData] = useState<any>(null);
  342. const [selectedMarker, setSelectedMarker] = useState<any>(null);
  343. const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
  344. const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
  345. const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
  346. const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState<boolean>(false);
  347. const [isEditModalVisible, setIsEditModalVisible] = useState(false);
  348. const [isFilterVisible, setIsFilterVisible] = useState<string | null>(null);
  349. const [isLocationLoading, setIsLocationLoading] = useState(false);
  350. const [modalState, setModalState] = useState({
  351. selectedFirstYear: 2021,
  352. selectedLastYear: 2021,
  353. selectedQuality: qualityOptions[2],
  354. selectedNoOfVisits: 1,
  355. years: [],
  356. id: null
  357. });
  358. const [isExpanded, setIsExpanded] = useState(false);
  359. const [search, setSearch] = useState('');
  360. const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
  361. const [searchInput, setSearchInput] = useState('');
  362. const [searchVisible, setSearchVisible] = useState<boolean>(false);
  363. const [index, setIndex] = useState<number>(0);
  364. const width = useSharedValue(48);
  365. const usableWidth = Dimensions.get('window').width - 32;
  366. const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion();
  367. const [db1, setDb1] = useState<SQLiteDatabase | null>(null);
  368. const [db2, setDb2] = useState<SQLiteDatabase | null>(null);
  369. const [db3, setDb3] = useState<SQLiteDatabase | null>(null);
  370. const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
  371. const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
  372. const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([]));
  373. const [seriesVisitedFilter, setSeriesVisitedFilter] = useState(generateFilter([]));
  374. const [seriesNotVisitedFilter, setSeriesNotVisitedFilter] = useState(generateFilter([]));
  375. const [regionsVisited, setRegionsVisited] = useState<any[]>([]);
  376. const [countriesVisited, setCountriesVisited] = useState<any[]>([]);
  377. const [dareVisited, setDareVisited] = useState<any[]>([]);
  378. const [seriesVisited, setSeriesVisited] = useState<any[]>([]);
  379. const [images, setImages] = useState<any>({});
  380. const { mutateAsync: updateSeriesItem } = usePostSetToggleItem();
  381. const [nomads, setNomads] = useState<GeoJSON.FeatureCollection | null>(null);
  382. const { data: usersLocation, refetch: refetchUsersLocation } = usePostGetUsersLocationQuery(
  383. token,
  384. !!token && showNomads && Boolean(location) && isConnected
  385. );
  386. const [selectedUser, setSelectedUser] = useState<any>(null);
  387. const [zoom, setZoom] = useState(0);
  388. const [center, setCenter] = useState<number[] | null>(null);
  389. const [isZooming, setIsZooming] = useState(true);
  390. const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  391. const [markerCoords, setMarkerCoords] = useState<any>(null);
  392. const [interval, setInterval] = useState(0);
  393. const isSmallScreen = Dimensions.get('window').width < 383;
  394. const processedImages = useRef(new Set<string>());
  395. useEffect(() => {
  396. if (netInfo && netInfo.isConnected !== null) {
  397. setIsConnected(netInfo.isConnected);
  398. }
  399. }, [netInfo]);
  400. useEffect(() => {
  401. if (showNomads) {
  402. refetchUsersLocation();
  403. }
  404. }, [showNomads]);
  405. useEffect(() => {
  406. if (usersLocation && usersLocation.geojson && showNomads) {
  407. const filteredNomads: GeoJSON.FeatureCollection = {
  408. type: 'FeatureCollection',
  409. features: usersLocation.geojson.features.filter(
  410. (feature: GeoJSON.Feature) => feature.properties?.id !== +userId
  411. )
  412. };
  413. if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) {
  414. setNomads(filteredNomads);
  415. }
  416. }
  417. }, [usersLocation, showNomads]);
  418. useEffect(() => {
  419. const loadCachedIcons = async () => {
  420. try {
  421. const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR);
  422. if (!dirInfo.exists) return;
  423. const files = await FileSystem.readDirectoryAsync(ICONS_DIR);
  424. const cachedImages: Record<string, { uri: string }> = {};
  425. files.forEach((fileName) => {
  426. if (!fileName.endsWith('.png')) return;
  427. const key = fileName.replace('.png', '');
  428. cachedImages[key] = {
  429. uri: ICONS_DIR + fileName
  430. };
  431. processedImages.current.add(key);
  432. });
  433. setImages((prev: any) => ({ ...prev, ...cachedImages }));
  434. } catch (e) {
  435. console.warn('Error loading cached icons:', e);
  436. }
  437. };
  438. loadCachedIcons();
  439. }, []);
  440. useEffect(() => {
  441. if (!seriesIcons) return;
  442. const updateCacheFromAPI = async () => {
  443. const loadedImages: Record<string, { uri: string }> = {};
  444. const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR);
  445. if (!dirInfo.exists) {
  446. await FileSystem.makeDirectoryAsync(ICONS_DIR, { intermediates: true });
  447. }
  448. const promises = seriesIcons.data.map(async (icon) => {
  449. const id = icon.id?.toString();
  450. if (!id || processedImages.current.has(id)) return;
  451. const imgUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_png}`;
  452. const imgVisitedUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_visited_png}`;
  453. const localPath = `${ICONS_DIR}${id}.png`;
  454. const localPathVisited = `${ICONS_DIR}${id}v.png`;
  455. const [imgInfo, visitedInfo] = await Promise.all([
  456. FileSystem.getInfoAsync(localPath),
  457. FileSystem.getInfoAsync(localPathVisited)
  458. ]);
  459. try {
  460. if (!imgInfo.exists) {
  461. await FileSystem.downloadAsync(imgUrl, localPath);
  462. }
  463. if (!visitedInfo.exists) {
  464. await FileSystem.downloadAsync(imgVisitedUrl, localPathVisited);
  465. }
  466. } catch (e) {
  467. console.warn(`Download failed for ${id}:`, e);
  468. return;
  469. }
  470. processedImages.current.add(id);
  471. processedImages.current.add(`${id}v`);
  472. loadedImages[id] = { uri: localPath };
  473. loadedImages[`${id}v`] = { uri: localPathVisited };
  474. });
  475. await Promise.all(promises);
  476. setImages((prev: any) => ({ ...prev, ...loadedImages }));
  477. };
  478. updateCacheFromAPI();
  479. }, [seriesIcons]);
  480. useEffect(() => {
  481. const loadDatabases = async () => {
  482. const firstDb = await getFirstDatabase();
  483. const secondDb = await getSecondDatabase();
  484. const countriesDb = await getCountriesDatabase();
  485. setDb1(firstDb);
  486. setDb2(secondDb);
  487. setDb3(countriesDb);
  488. };
  489. if (!db1 || !db2 || !db3) {
  490. loadDatabases();
  491. }
  492. }, [db1, db2, db3]);
  493. useEffect(() => {
  494. const savedFilterSettings = storage.get('filterSettings', StoreType.STRING) as string;
  495. if (savedFilterSettings) {
  496. const filterSettings = JSON.parse(savedFilterSettings);
  497. setTilesType(filterSettings.tilesType);
  498. setType(filterSettings.type);
  499. setRegionsFilter({
  500. visitedLabel:
  501. filterSettings.selectedVisible?.value && filterSettings.selectedVisible.value === 1
  502. ? 'in'
  503. : 'by',
  504. year: filterSettings.selectedYear?.value ?? moment().year()
  505. });
  506. setSeriesFilter(filterSettings.seriesFilter);
  507. }
  508. }, []);
  509. useFocusEffect(
  510. useCallback(() => {
  511. if (token) {
  512. refetchVisitedRegions();
  513. refetchVisitedCountries();
  514. refetchVisitedDare();
  515. }
  516. }, [navigation])
  517. );
  518. // to do refactor
  519. useEffect(() => {
  520. if (visitedRegionIds) {
  521. setRegionsVisited(visitedRegionIds.ids);
  522. storage.set('visitedRegions', JSON.stringify(visitedRegionIds.ids));
  523. } else {
  524. const storedVisited = storage.get('visitedRegions', StoreType.STRING) as string;
  525. setRegionsVisited(storedVisited ? JSON.parse(storedVisited) : []);
  526. }
  527. }, [visitedRegionIds]);
  528. useEffect(() => {
  529. if (visitedCountryIds) {
  530. setCountriesVisited(visitedCountryIds.ids);
  531. storage.set('visitedCountries', JSON.stringify(visitedCountryIds.ids));
  532. } else {
  533. const storedVisited = storage.get('visitedCountries', StoreType.STRING) as string;
  534. setCountriesVisited(storedVisited ? JSON.parse(storedVisited) : []);
  535. }
  536. }, [visitedCountryIds]);
  537. useEffect(() => {
  538. if (visitedDareIds) {
  539. setDareVisited(visitedDareIds.ids);
  540. storage.set('visitedDares', JSON.stringify(visitedDareIds.ids));
  541. } else {
  542. const storedVisited = storage.get('visitedDares', StoreType.STRING) as string;
  543. setDareVisited(storedVisited ? JSON.parse(storedVisited) : []);
  544. }
  545. }, [visitedDareIds]);
  546. useEffect(() => {
  547. if (visitedSeriesIds && token) {
  548. setSeriesVisited(visitedSeriesIds.ids);
  549. storage.set('visitedSeries', JSON.stringify(visitedSeriesIds.ids));
  550. } else {
  551. const storedVisited = storage.get('visitedSeries', StoreType.STRING) as string;
  552. setSeriesVisited(storedVisited ? JSON.parse(storedVisited) : []);
  553. }
  554. }, [visitedSeriesIds]);
  555. useEffect(() => {
  556. if (regionsVisited && regionsVisited.length) {
  557. setRegionsVisitedFilter(generateFilter(regionsVisited));
  558. } else {
  559. setRegionsVisitedFilter(['==', 'id', -1]);
  560. }
  561. }, [regionsVisited]);
  562. useEffect(() => {
  563. if (countriesVisited && countriesVisited.length) {
  564. setCountriesVisitedFilter(generateFilter(countriesVisited));
  565. } else {
  566. setCountriesVisitedFilter(['==', 'id', -1]);
  567. }
  568. }, [countriesVisited]);
  569. useEffect(() => {
  570. if (dareVisited && dareVisited.length) {
  571. setDareVisitedFilter(generateFilter(dareVisited));
  572. } else {
  573. setDareVisitedFilter(['==', 'id', -1]);
  574. }
  575. }, [dareVisited]);
  576. useEffect(() => {
  577. if (!seriesFilter.visible) {
  578. setSeriesVisitedFilter(generateFilter([]));
  579. setSeriesNotVisitedFilter(generateFilter([]));
  580. return;
  581. }
  582. if (seriesFilter.applied) {
  583. if (seriesVisited?.length) {
  584. setSeriesVisitedFilter([
  585. 'all',
  586. ['any', ...seriesVisited.map((id) => ['==', 'id', id])],
  587. ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])]
  588. ]);
  589. setSeriesNotVisitedFilter([
  590. 'all',
  591. ['all', ...seriesVisited.map((id) => ['!=', 'id', id])],
  592. ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])]
  593. ]);
  594. } else {
  595. setSeriesNotVisitedFilter([
  596. 'any',
  597. ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])
  598. ]);
  599. }
  600. } else {
  601. setSeriesVisitedFilter(['any', ...seriesVisited.map((id) => ['==', 'id', id])]);
  602. setSeriesNotVisitedFilter(['all', ...seriesVisited.map((id) => ['!=', 'id', id])]);
  603. }
  604. }, [seriesVisited, seriesFilter]);
  605. useEffect(() => {
  606. if (route.params?.lon && route.params?.lat) {
  607. setMarkerCoords([route.params.lon, route.params.lat]);
  608. const timeoutId = setTimeout(() => {
  609. if (cameraRef.current) {
  610. cameraRef.current.setCamera({
  611. centerCoordinate: [route.params?.lon, route.params?.lat],
  612. zoomLevel: 15,
  613. animationDuration: 800
  614. });
  615. } else {
  616. console.warn('Camera ref is not available.');
  617. }
  618. }, 800);
  619. return () => clearTimeout(timeoutId);
  620. }
  621. if (route.params?.id && route.params?.type && db1 && db2 && db3) {
  622. handleFindRegion(route.params?.id, route.params?.type);
  623. }
  624. }, [route, db1, db2, db3]);
  625. useFocusEffect(
  626. useCallback(() => {
  627. if (token) {
  628. refetch();
  629. }
  630. }, [])
  631. );
  632. useEffect(() => {
  633. if (interval > 0 && showNomads) {
  634. const intervalId = setInterval(() => {
  635. if (location && token && showNomads) {
  636. refetchUsersLocation();
  637. }
  638. }, interval);
  639. return () => clearInterval(intervalId);
  640. }
  641. }, [interval, showNomads]);
  642. useEffect(() => {
  643. (async () => {
  644. let { status } = await Location.getForegroundPermissionsAsync();
  645. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  646. if (locationSettings && locationSettings.sharing_refresh_interval) {
  647. setInterval(locationSettings.sharing_refresh_interval * 1000);
  648. }
  649. if (
  650. status !== 'granted' ||
  651. !token ||
  652. (locationSettings && locationSettings.sharing === 0) ||
  653. !isServicesEnabled
  654. ) {
  655. setShowNomads(false);
  656. storage.set('showNomads', false);
  657. await stopBackgroundLocationUpdates();
  658. return;
  659. }
  660. const bgStatus = await Location.getBackgroundPermissionsAsync();
  661. if (bgStatus.status !== 'granted') {
  662. const { status } = await Location.requestBackgroundPermissionsAsync();
  663. if (status === Location.PermissionStatus.GRANTED) {
  664. await startBackgroundLocationUpdates();
  665. } else {
  666. await stopBackgroundLocationUpdates();
  667. }
  668. } else {
  669. // await startBackgroundLocationUpdates();
  670. await restartBackgroundLocationUpdates();
  671. }
  672. try {
  673. let currentLocation = await Location.getCurrentPositionAsync({
  674. accuracy: Location.Accuracy.Balanced
  675. });
  676. setLocation(currentLocation.coords);
  677. if (locationSettings && locationSettings.sharing === 1 && token) {
  678. updateLocation({
  679. token,
  680. lat: currentLocation.coords.latitude,
  681. lng: currentLocation.coords.longitude
  682. });
  683. showNomads && refetchUsersLocation();
  684. }
  685. } catch (error) {
  686. console.error('Error fetching user location:', error);
  687. }
  688. })();
  689. }, [locationSettings]);
  690. useEffect(() => {
  691. const currentYear = moment().year();
  692. let yearSelector: { label: string; value: number }[] = [{ label: 'visited', value: 1 }];
  693. for (let i = currentYear; i >= 1951; i--) {
  694. yearSelector.push({ label: i.toString(), value: i });
  695. }
  696. handleModalStateChange({ years: yearSelector });
  697. }, []);
  698. useFocusEffect(
  699. useCallback(() => {
  700. navigation.getParent()?.setOptions({
  701. tabBarStyle: {
  702. display: regionPopupVisible ? 'none' : 'flex',
  703. position: 'absolute',
  704. ...Platform.select({
  705. android: {
  706. height: 58
  707. }
  708. })
  709. }
  710. });
  711. }, [regionPopupVisible, navigation])
  712. );
  713. const mapRef = useRef<MapLibreRN.MapViewRef>(null);
  714. const cameraRef = useRef<MapLibreRN.CameraRef>(null);
  715. const shapeSourceRef = useRef<MapLibreRN.ShapeSourceRef>(null);
  716. useEffect(() => {
  717. if (userInfo) {
  718. setUserInfoData(JSON.parse(userInfo));
  719. }
  720. }, [userInfo]);
  721. const handlePress = () => {
  722. if (isExpanded) {
  723. setSearchInput('');
  724. }
  725. setIsExpanded((prev) => !prev);
  726. width.value = withTiming(isExpanded ? 48 : usableWidth, {
  727. duration: 300,
  728. easing: Easing.inOut(Easing.ease)
  729. });
  730. };
  731. const animatedStyle = useAnimatedStyle(() => {
  732. return {
  733. width: width.value
  734. };
  735. });
  736. const loadInitialRegion = () => {
  737. try {
  738. const savedInitialRegion = storage.get('initialRegion', StoreType.STRING) as string;
  739. if (savedInitialRegion) {
  740. const region = JSON.parse(savedInitialRegion);
  741. setInitialRegion(region);
  742. }
  743. } catch (e) {
  744. console.error('Failed to load saved initial region:', e);
  745. }
  746. };
  747. useEffect(() => {
  748. loadInitialRegion();
  749. }, []);
  750. useEffect(() => {
  751. if (initialRegion && !route.params?.id) {
  752. const timeoutId = setTimeout(() => {
  753. if (cameraRef.current) {
  754. cameraRef.current.setCamera({
  755. centerCoordinate: [initialRegion.longitude, initialRegion.latitude],
  756. zoomLevel: Math.log2(360 / initialRegion.latitudeDelta),
  757. animationDuration: 500
  758. });
  759. } else {
  760. console.warn('Camera ref is not available.');
  761. }
  762. }, 500);
  763. return () => clearTimeout(timeoutId);
  764. }
  765. }, [initialRegion]);
  766. const handleMapChange = async () => {
  767. if (!mapRef.current) return;
  768. if (hideTimer.current) clearTimeout(hideTimer.current);
  769. setIsZooming(true);
  770. const currentZoom = await mapRef.current.getZoom();
  771. const currentCenter = await mapRef.current.getCenter();
  772. setZoom(currentZoom);
  773. setCenter(currentCenter);
  774. };
  775. const onMapPress = async (event: any) => {
  776. if (!mapRef.current) return;
  777. if (selectedMarker || selectedUser) {
  778. closeCallout();
  779. return;
  780. }
  781. if (type === 'blank') return;
  782. try {
  783. const { screenPointX, screenPointY } = event.properties;
  784. const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
  785. [screenPointX, screenPointY],
  786. undefined,
  787. ['regions', 'countries', 'dare']
  788. );
  789. if (features?.length) {
  790. const region = features[0];
  791. if (selectedRegion === region.properties?.id) return;
  792. let db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
  793. let tableName = type === 'dare' ? 'places' : type;
  794. let foundRegion = region.properties?.id;
  795. setSelectedRegion(region.properties?.id);
  796. await getData(db, foundRegion, tableName, handleRegionData)
  797. .then(() => {
  798. setRegionPopupVisible(true);
  799. })
  800. .catch((error) => {
  801. console.error('Error fetching data', error);
  802. refreshDatabases();
  803. });
  804. if (tableName === 'regions') {
  805. token
  806. ? await mutateUserData(
  807. { region_id: +foundRegion, token: String(token) },
  808. {
  809. onSuccess: (data) => {
  810. setUserData({ type: 'nm', id: +foundRegion, ...data });
  811. }
  812. }
  813. )
  814. : setUserData({ type: 'nm', id: +foundRegion });
  815. if (regionsList && regionsList.data) {
  816. const region = regionsList.data.find((region) => region.id === +foundRegion);
  817. if (region) {
  818. const bounds = turf.bbox(region.bbox);
  819. cameraRef.current?.fitBounds(
  820. [bounds[2], bounds[3]],
  821. [bounds[0], bounds[1]],
  822. [10, 10, 50, 10],
  823. 1000
  824. );
  825. }
  826. }
  827. } else if (tableName === 'countries') {
  828. token
  829. ? await mutateCountriesData(
  830. { id: +foundRegion, token },
  831. {
  832. onSuccess: (data) => {
  833. setUserData({ type: 'countries', id: +foundRegion, ...data.data });
  834. }
  835. }
  836. )
  837. : setUserData({ type: 'countries', id: +foundRegion });
  838. if (countriesList && countriesList.data) {
  839. const region = countriesList.data.find((region) => region.id === +foundRegion);
  840. if (region) {
  841. const bounds = turf.bbox(region.bbox);
  842. cameraRef.current?.fitBounds(
  843. [bounds[2], bounds[3]],
  844. [bounds[0], bounds[1]],
  845. [10, 10, 50, 10],
  846. 1000
  847. );
  848. }
  849. }
  850. } else {
  851. token
  852. ? await mutateUserDataDare(
  853. { dare_id: +foundRegion, token: String(token) },
  854. {
  855. onSuccess: (data) => {
  856. setUserData({ type: 'dare', id: +foundRegion, ...data });
  857. }
  858. }
  859. )
  860. : setUserData({ type: 'dare', id: +foundRegion });
  861. if (dareList && dareList.data) {
  862. const region = dareList.data.find((region) => region.id === +foundRegion);
  863. if (region) {
  864. const bounds = turf.bbox(region.bbox);
  865. cameraRef.current?.fitBounds(
  866. [bounds[2], bounds[3]],
  867. [bounds[0], bounds[1]],
  868. [10, 10, 50, 10],
  869. 1000
  870. );
  871. }
  872. }
  873. }
  874. } else {
  875. handleClosePopup();
  876. }
  877. } catch (error) {
  878. console.error('Error onMapPress features:', error);
  879. }
  880. };
  881. const handleRegionDidChange = async (feature: GeoJSON.Feature<GeoJSON.Point, any>) => {
  882. hideTimer.current = setTimeout(() => {
  883. setIsZooming(false);
  884. }, 2000);
  885. if (!feature) return;
  886. const { zoomLevel } = feature.properties;
  887. const { coordinates } = feature.geometry;
  888. if (!zoomLevel || !coordinates) return;
  889. const latitudeDelta = 360 / 2 ** zoomLevel;
  890. const longitudeDelta = latitudeDelta;
  891. const region = {
  892. latitude: coordinates[1],
  893. longitude: coordinates[0],
  894. latitudeDelta,
  895. longitudeDelta
  896. };
  897. storage.set('initialRegion', JSON.stringify(region));
  898. };
  899. const handleClosePopup = async () => {
  900. setSelectedRegion(null);
  901. setRegionPopupVisible(false);
  902. setRegionData(null);
  903. };
  904. const handleGetLocation = async () => {
  905. setIsLocationLoading(true);
  906. try {
  907. let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
  908. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  909. if (status === 'granted' && isServicesEnabled) {
  910. const bgStatus = await Location.getBackgroundPermissionsAsync();
  911. if (bgStatus.status !== 'granted') {
  912. const { status } = await Location.requestBackgroundPermissionsAsync();
  913. if (status === Location.PermissionStatus.GRANTED) {
  914. await startBackgroundLocationUpdates();
  915. } else {
  916. await stopBackgroundLocationUpdates();
  917. }
  918. } else {
  919. await startBackgroundLocationUpdates();
  920. }
  921. await getLocation();
  922. } else if (!canAskAgain || !isServicesEnabled) {
  923. setOpenSettingsVisible(true);
  924. } else {
  925. setAskLocationVisible(true);
  926. }
  927. } finally {
  928. setIsLocationLoading(false);
  929. }
  930. };
  931. const getLocation = async () => {
  932. try {
  933. let currentLocation = await Location.getCurrentPositionAsync({
  934. accuracy: Location.Accuracy.Balanced
  935. });
  936. setLocation(currentLocation.coords);
  937. if (currentLocation.coords) {
  938. cameraRef.current?.flyTo(
  939. [currentLocation.coords.longitude, currentLocation.coords.latitude],
  940. 1000
  941. );
  942. }
  943. if (locationSettings && locationSettings.sharing === 1 && token) {
  944. updateLocation({
  945. token,
  946. lat: currentLocation.coords.latitude,
  947. lng: currentLocation.coords.longitude
  948. });
  949. showNomads && refetchUsersLocation();
  950. }
  951. handleClosePopup();
  952. } catch (error) {
  953. console.error('Error fetching user location:', error);
  954. }
  955. };
  956. const handleAcceptPermission = async () => {
  957. setAskLocationVisible(false);
  958. let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
  959. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  960. if (status === 'granted' && isServicesEnabled) {
  961. getLocation();
  962. } else if (!canAskAgain || !isServicesEnabled) {
  963. setOpenSettingsVisible(true);
  964. }
  965. };
  966. const handleOpenEditModal = () => {
  967. handleModalStateChange({
  968. selectedFirstYear: userData?.first_visit_year,
  969. selectedLastYear: userData?.last_visit_year,
  970. selectedQuality:
  971. qualityOptions.find((quality) => quality.id === userData?.best_visit_quality) ||
  972. qualityOptions[2],
  973. selectedNoOfVisits: userData?.no_of_visits || 1,
  974. id: regionData?.id
  975. });
  976. setIsEditModalVisible(true);
  977. };
  978. const handleOpenEditSlowModal = () => {
  979. setIsEditSlowModalVisible(true);
  980. };
  981. const handleSearch = async () => {
  982. setSearch(searchInput);
  983. setSearchVisible(true);
  984. };
  985. const handleCloseModal = () => {
  986. setSearchInput('');
  987. setSearchVisible(false);
  988. handlePress();
  989. };
  990. const handleRegionData = async (regionData: any, avatars: string[]) => {
  991. if (!regionData) {
  992. await refreshDatabases();
  993. }
  994. setRegionData(regionData);
  995. setUserAvatars(avatars);
  996. };
  997. const handleFindRegion = async (id: number, type: 'regions' | 'countries' | 'places') => {
  998. setType(type === 'places' ? 'dare' : type);
  999. if (!db1 || !db2 || !db3) {
  1000. return;
  1001. }
  1002. const db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
  1003. if (id) {
  1004. setSelectedRegion(id);
  1005. await getData(db, id, type, handleRegionData)
  1006. .then(() => {
  1007. setRegionPopupVisible(true);
  1008. })
  1009. .catch((error) => {
  1010. console.error('Error fetching data', error);
  1011. refreshDatabases();
  1012. });
  1013. if (type === 'regions') {
  1014. token
  1015. ? await mutateUserData(
  1016. { region_id: id, token: String(token) },
  1017. {
  1018. onSuccess: (data) => {
  1019. setUserData({ type: 'nm', id, ...data });
  1020. }
  1021. }
  1022. )
  1023. : setUserData({ type: 'nm', id });
  1024. if (regionsList && regionsList.data) {
  1025. const region = regionsList.data.find((region) => region.id === +id);
  1026. if (region) {
  1027. const bounds = turf.bbox(region.bbox);
  1028. cameraRef.current?.fitBounds(
  1029. [bounds[2], bounds[3]],
  1030. [bounds[0], bounds[1]],
  1031. [10, 10, 50, 10],
  1032. 1000
  1033. );
  1034. }
  1035. }
  1036. } else if (type === 'countries') {
  1037. token
  1038. ? await mutateCountriesData(
  1039. { id, token },
  1040. {
  1041. onSuccess: (data) => {
  1042. setUserData({ type: 'countries', id, ...data.data });
  1043. }
  1044. }
  1045. )
  1046. : setUserData({ type: 'countries', id });
  1047. if (countriesList && countriesList.data) {
  1048. const region = countriesList.data.find((region) => region.id === +id);
  1049. if (region) {
  1050. const bounds = turf.bbox(region.bbox);
  1051. cameraRef.current?.fitBounds(
  1052. [bounds[2], bounds[3]],
  1053. [bounds[0], bounds[1]],
  1054. [10, 10, 50, 10],
  1055. 1000
  1056. );
  1057. }
  1058. }
  1059. } else {
  1060. token
  1061. ? await mutateUserDataDare(
  1062. { dare_id: +id, token: String(token) },
  1063. {
  1064. onSuccess: (data) => {
  1065. setUserData({ type: 'dare', id: +id, ...data });
  1066. }
  1067. }
  1068. )
  1069. : setUserData({ type: 'dare', id: +id });
  1070. if (dareList && dareList.data) {
  1071. const region = dareList.data.find((region) => region.id === +id);
  1072. if (region) {
  1073. const bounds = turf.bbox(region.bbox);
  1074. cameraRef.current?.fitBounds(
  1075. [bounds[2], bounds[3]],
  1076. [bounds[0], bounds[1]],
  1077. [10, 10, 50, 10],
  1078. 1000
  1079. );
  1080. }
  1081. }
  1082. }
  1083. } else {
  1084. handleClosePopup();
  1085. }
  1086. };
  1087. const handleMarkerPress = async (event: any) => {
  1088. const { features } = event;
  1089. if (features?.length) {
  1090. const selectedFeature = features[0];
  1091. const { coordinates } = selectedFeature.geometry;
  1092. const visited = seriesVisited.includes(selectedFeature.properties.id) ? 1 : 0;
  1093. const icon = images[selectedFeature.properties.series_id];
  1094. const { name, description, series_name, series_id, id } = selectedFeature.properties;
  1095. if (coordinates) {
  1096. setSelectedMarker({
  1097. coordinates,
  1098. name,
  1099. icon,
  1100. description,
  1101. series_name,
  1102. visited,
  1103. series_id,
  1104. id
  1105. });
  1106. setSelectedUser(null);
  1107. }
  1108. }
  1109. };
  1110. const closeCallout = () => {
  1111. setSelectedMarker(null);
  1112. setSelectedUser(null);
  1113. };
  1114. const toggleSeries = useCallback(
  1115. async (item: any) => {
  1116. if (!token) {
  1117. setIsWarningModalVisible(true);
  1118. return;
  1119. }
  1120. const itemData = {
  1121. token,
  1122. series_id: item.series_id,
  1123. item_id: item.id,
  1124. checked: (item.visited === 0 ? 1 : 0) as 0 | 1,
  1125. double: 0 as 0 | 1
  1126. };
  1127. try {
  1128. updateSeriesItem(itemData);
  1129. if (item.visited === 1) {
  1130. setSeriesVisited((current) => current.filter((id) => id !== item.id));
  1131. setSelectedMarker((current: any) => ({ ...current, visited: 0 }));
  1132. } else {
  1133. setSeriesVisited((current) => [...current, item.id]);
  1134. setSelectedMarker((current: any) => ({ ...current, visited: 1 }));
  1135. }
  1136. } catch (error) {
  1137. console.error('Failed to update series state', error);
  1138. }
  1139. },
  1140. [token, updateSeriesItem]
  1141. );
  1142. const handleModalStateChange = (updates: { [key: string]: any }) => {
  1143. setModalState((prevState) => ({ ...prevState, ...updates }));
  1144. };
  1145. const handleUserPress = (event: any) => {
  1146. const selectedFeature = event.features[0];
  1147. const { coordinates } = selectedFeature.geometry;
  1148. const { avatar, first_name, last_name, flag, id, last_seen } = selectedFeature.properties;
  1149. if (selectedFeature) {
  1150. setSelectedUser({
  1151. coordinates,
  1152. avatar: avatar ? { uri: API_HOST + avatar } : logo,
  1153. first_name,
  1154. last_name,
  1155. flag: { uri: API_HOST + flag },
  1156. id,
  1157. last_seen
  1158. });
  1159. setSelectedMarker(null);
  1160. }
  1161. };
  1162. return (
  1163. <SafeAreaView style={{ height: '100%' }}>
  1164. <StatusBar translucent backgroundColor="transparent" />
  1165. <MapLibreRN.MapView
  1166. ref={mapRef}
  1167. style={styles.map}
  1168. mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
  1169. rotateEnabled={false}
  1170. attributionEnabled={false}
  1171. onPress={onMapPress}
  1172. onRegionDidChange={handleRegionDidChange}
  1173. onRegionIsChanging={handleMapChange}
  1174. onRegionWillChange={_.debounce(handleMapChange, 200)}
  1175. >
  1176. <MapLibreRN.Images
  1177. images={images}
  1178. onImageMissing={(image) => {
  1179. if (processedImages.current.has(image)) {
  1180. return;
  1181. }
  1182. processedImages.current.add(image);
  1183. setImages((prevImages: any) => ({
  1184. ...prevImages,
  1185. [image]: defaultSeriesIcon
  1186. }));
  1187. }}
  1188. >
  1189. <View />
  1190. </MapLibreRN.Images>
  1191. {markerCoords && (
  1192. <MapLibreRN.PointAnnotation id="marker" coordinate={markerCoords}>
  1193. <View
  1194. style={{
  1195. height: 24,
  1196. width: 24,
  1197. backgroundColor: Colors.ORANGE,
  1198. borderRadius: 12,
  1199. borderColor: Colors.WHITE,
  1200. borderWidth: 2
  1201. }}
  1202. />
  1203. </MapLibreRN.PointAnnotation>
  1204. )}
  1205. {type === 'regions' && (
  1206. <>
  1207. <MapLibreRN.LineLayer
  1208. id="nm-regions-line-layer"
  1209. sourceID={regions.source}
  1210. sourceLayerID={regions['source-layer']}
  1211. filter={regions.filter as any}
  1212. maxZoomLevel={regions.maxzoom}
  1213. style={{
  1214. lineColor: 'rgba(14, 80, 109, 1)',
  1215. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
  1216. lineWidthTransition: { duration: 300, delay: 0 }
  1217. }}
  1218. belowLayerID="waterway-name"
  1219. />
  1220. <MapLibreRN.FillLayer
  1221. id={regions.id}
  1222. sourceID={regions.source}
  1223. sourceLayerID={regions['source-layer']}
  1224. filter={regions.filter as any}
  1225. style={regions.style}
  1226. maxZoomLevel={regions.maxzoom}
  1227. belowLayerID={regions_visited.id}
  1228. />
  1229. <MapLibreRN.FillLayer
  1230. id={regions_visited.id}
  1231. sourceID={regions_visited.source}
  1232. sourceLayerID={regions_visited['source-layer']}
  1233. filter={regionsVisitedFilter as any}
  1234. style={regions_visited.style}
  1235. maxZoomLevel={regions_visited.maxzoom}
  1236. belowLayerID="waterway-name"
  1237. />
  1238. </>
  1239. )}
  1240. {type === 'countries' && (
  1241. <>
  1242. <MapLibreRN.LineLayer
  1243. id="countries-line-layer"
  1244. sourceID={countries.source}
  1245. sourceLayerID={countries['source-layer']}
  1246. filter={countries.filter as any}
  1247. maxZoomLevel={countries.maxzoom}
  1248. style={{
  1249. lineColor: 'rgba(14, 80, 109, 1)',
  1250. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
  1251. lineWidthTransition: { duration: 300, delay: 0 }
  1252. }}
  1253. belowLayerID="waterway-name"
  1254. />
  1255. <MapLibreRN.FillLayer
  1256. id={countries.id}
  1257. sourceID={countries.source}
  1258. sourceLayerID={countries['source-layer']}
  1259. filter={countries.filter as any}
  1260. style={countries.style}
  1261. maxZoomLevel={countries.maxzoom}
  1262. belowLayerID={countries_visited.id}
  1263. />
  1264. <MapLibreRN.FillLayer
  1265. id={countries_visited.id}
  1266. sourceID={countries_visited.source}
  1267. sourceLayerID={countries_visited['source-layer']}
  1268. filter={countriesVisitedFilter as any}
  1269. style={countries_visited.style}
  1270. maxZoomLevel={countries_visited.maxzoom}
  1271. belowLayerID="waterway-name"
  1272. />
  1273. </>
  1274. )}
  1275. {type === 'dare' && (
  1276. <>
  1277. <MapLibreRN.FillLayer
  1278. id={dare.id}
  1279. sourceID={dare.source}
  1280. sourceLayerID={dare['source-layer']}
  1281. filter={dare.filter as any}
  1282. style={dare.style}
  1283. maxZoomLevel={dare.maxzoom}
  1284. belowLayerID={dare_visited.id}
  1285. />
  1286. <MapLibreRN.FillLayer
  1287. id={dare_visited.id}
  1288. sourceID={dare_visited.source}
  1289. sourceLayerID={dare_visited['source-layer']}
  1290. filter={dareVisitedFilter as any}
  1291. style={dare_visited.style}
  1292. maxZoomLevel={dare_visited.maxzoom}
  1293. belowLayerID="waterway-name"
  1294. />
  1295. </>
  1296. )}
  1297. {selectedRegion && type && (
  1298. <>
  1299. <MapLibreRN.FillLayer
  1300. id={selected_region.id}
  1301. sourceID={type}
  1302. sourceLayerID={type}
  1303. filter={['==', 'id', selectedRegion]}
  1304. style={selected_region.style}
  1305. maxZoomLevel={selected_region.maxzoom}
  1306. belowLayerID="waterway-name"
  1307. />
  1308. <MapLibreRN.LineLayer
  1309. id={selected_region_outline.id}
  1310. sourceID={type}
  1311. sourceLayerID={type}
  1312. filter={['==', 'id', selectedRegion]}
  1313. style={selected_region_outline.style as any}
  1314. maxZoomLevel={selected_region_outline.maxzoom}
  1315. belowLayerID="waterway-name"
  1316. />
  1317. </>
  1318. )}
  1319. <MapLibreRN.VectorSource
  1320. id="nomadmania_series"
  1321. tileUrlTemplates={[VECTOR_MAP_HOST + '/tiles/series/{z}/{x}/{y}.pbf']}
  1322. onPress={handleMarkerPress}
  1323. >
  1324. {seriesFilter.status !== 1 && Object.keys(images).length > 0 ? (
  1325. <MapLibreRN.SymbolLayer
  1326. id={series_layer.id}
  1327. sourceID={series_layer.source}
  1328. sourceLayerID={series_layer['source-layer']}
  1329. aboveLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined}
  1330. filter={seriesNotVisitedFilter as any}
  1331. minZoomLevel={series_layer.minzoom}
  1332. maxZoomLevel={series_layer.maxzoom}
  1333. style={{
  1334. symbolSpacing: 1,
  1335. iconImage: ['get', 'series_id'],
  1336. iconSize: 0.51,
  1337. iconAllowOverlap: true,
  1338. iconIgnorePlacement: true,
  1339. visibility: 'visible',
  1340. iconColor: '#666',
  1341. iconOpacity: 1,
  1342. iconHaloColor: '#ffffff',
  1343. iconHaloWidth: 1,
  1344. iconHaloBlur: 0.5
  1345. }}
  1346. />
  1347. ) : (
  1348. <></>
  1349. )}
  1350. {seriesFilter.status !== 0 && Object.keys(images).length > 0 ? (
  1351. <MapLibreRN.SymbolLayer
  1352. id={series_visited.id}
  1353. sourceID={series_visited.source}
  1354. sourceLayerID={series_visited['source-layer']}
  1355. aboveLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined}
  1356. filter={seriesVisitedFilter as any}
  1357. minZoomLevel={series_visited.minzoom}
  1358. maxZoomLevel={series_visited.maxzoom}
  1359. style={{
  1360. symbolSpacing: 1,
  1361. iconImage: '{series_id}v',
  1362. iconSize: 0.51,
  1363. iconAllowOverlap: true,
  1364. iconIgnorePlacement: true,
  1365. visibility: 'visible',
  1366. iconColor: '#666',
  1367. iconOpacity: 1,
  1368. iconHaloColor: '#ffffff',
  1369. iconHaloWidth: 1,
  1370. iconHaloBlur: 0.5
  1371. }}
  1372. />
  1373. ) : (
  1374. <></>
  1375. )}
  1376. </MapLibreRN.VectorSource>
  1377. {nomads && showNomads && (
  1378. <MapLibreRN.ShapeSource
  1379. ref={shapeSourceRef}
  1380. tolerance={20}
  1381. id="nomads"
  1382. shape={nomads}
  1383. onPress={async (event) => {
  1384. const feature = event.features[0];
  1385. const isCluster = feature.properties?.cluster;
  1386. if (isCluster) {
  1387. const clusterCoordinates = (feature.geometry as GeoJSON.Point).coordinates;
  1388. const zoom = await shapeSourceRef.current?.getClusterExpansionZoom(
  1389. feature as GeoJSON.Feature<GeoJSON.Geometry>
  1390. );
  1391. const newZoom = zoom ?? 2;
  1392. cameraRef.current?.setCamera({
  1393. centerCoordinate: clusterCoordinates,
  1394. zoomLevel: newZoom,
  1395. animationDuration: 500,
  1396. animationMode: 'flyTo'
  1397. });
  1398. return;
  1399. } else {
  1400. handleUserPress(event);
  1401. }
  1402. }}
  1403. cluster={true}
  1404. clusterRadius={50}
  1405. >
  1406. <MapLibreRN.SymbolLayer
  1407. id="nomads_circle"
  1408. filter={['has', 'point_count']}
  1409. aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
  1410. style={{
  1411. iconImage: clusteredUsersIcon,
  1412. iconSize: [
  1413. 'interpolate',
  1414. ['linear'],
  1415. ['get', 'point_count'],
  1416. 0,
  1417. 0.33,
  1418. 10,
  1419. 0.35,
  1420. 20,
  1421. 0.37,
  1422. 50,
  1423. 0.39,
  1424. 75,
  1425. 0.41,
  1426. 100,
  1427. 0.43
  1428. ],
  1429. iconAllowOverlap: true
  1430. }}
  1431. ></MapLibreRN.SymbolLayer>
  1432. <MapLibreRN.SymbolLayer
  1433. id="nomads_count"
  1434. filter={['has', 'point_count']}
  1435. aboveLayerID={Platform.OS === 'android' ? 'nomads_circle' : undefined}
  1436. style={{
  1437. textField: [
  1438. 'case',
  1439. ['<', ['get', 'point_count'], 1000],
  1440. ['get', 'point_count'],
  1441. ['concat', ['/', ['round', ['/', ['get', 'point_count'], 100]], 10], 'k']
  1442. ],
  1443. textFont: ['Noto Sans Bold'],
  1444. textSize: [
  1445. 'interpolate',
  1446. ['linear'],
  1447. ['get', 'point_count'],
  1448. 0,
  1449. 13.5,
  1450. 20,
  1451. 14,
  1452. 75,
  1453. 15
  1454. ],
  1455. textColor: '#FFFFFF',
  1456. textAnchor: 'center',
  1457. textOffset: [
  1458. 'interpolate',
  1459. ['linear'],
  1460. ['get', 'point_count'],
  1461. 0,
  1462. ['literal', [0, 0.85]],
  1463. 20,
  1464. ['literal', [0, 0.92]],
  1465. 75,
  1466. ['literal', [0, 1]]
  1467. ],
  1468. textAllowOverlap: true
  1469. }}
  1470. />
  1471. <MapLibreRN.SymbolLayer
  1472. id="nomads_symbol"
  1473. filter={['!', ['has', 'point_count']]}
  1474. aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
  1475. style={{
  1476. iconImage: defaultUserAvatar,
  1477. iconSize: [
  1478. 'interpolate',
  1479. ['linear'],
  1480. ['zoom'],
  1481. 0,
  1482. 0.24,
  1483. 5,
  1484. 0.28,
  1485. 10,
  1486. 0.33,
  1487. 15,
  1488. 0.38,
  1489. 20,
  1490. 0.42
  1491. ],
  1492. iconAllowOverlap: true
  1493. }}
  1494. ></MapLibreRN.SymbolLayer>
  1495. </MapLibreRN.ShapeSource>
  1496. )}
  1497. {selectedUser && <UserItem marker={selectedUser} />}
  1498. {selectedMarker && (
  1499. <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} />
  1500. )}
  1501. <MapLibreRN.Camera ref={cameraRef} />
  1502. {location && (
  1503. <MapLibreRN.UserLocation
  1504. animated={true}
  1505. showsUserHeadingIndicator={true}
  1506. onPress={async () => {
  1507. const currentZoom = await mapRef.current?.getZoom();
  1508. const newZoom = (currentZoom || 0) + 2;
  1509. cameraRef.current?.setCamera({
  1510. centerCoordinate: [location.longitude, location.latitude],
  1511. zoomLevel: newZoom,
  1512. animationDuration: 500,
  1513. animationMode: 'flyTo'
  1514. });
  1515. }}
  1516. >
  1517. {/* to do custom user location */}
  1518. </MapLibreRN.UserLocation>
  1519. )}
  1520. </MapLibreRN.MapView>
  1521. {center ? (
  1522. <ScaleBar
  1523. zoom={zoom}
  1524. latitude={center[1]}
  1525. isVisible={isZooming}
  1526. bottom={tabBarHeight + 80}
  1527. />
  1528. ) : null}
  1529. {regionPopupVisible && regionData ? (
  1530. <>
  1531. <TouchableOpacity
  1532. style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
  1533. onPress={handleClosePopup}
  1534. >
  1535. <CloseSvg fill="white" width={13} height={13} />
  1536. <Text style={styles.textClose}>Close</Text>
  1537. </TouchableOpacity>
  1538. <TouchableOpacity
  1539. onPress={handleGetLocation}
  1540. style={[
  1541. styles.cornerButton,
  1542. styles.topRightButton,
  1543. styles.bottomButton,
  1544. { bottom: tabBarHeight + 20 }
  1545. ]}
  1546. >
  1547. {isLocationLoading ? (
  1548. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  1549. ) : (
  1550. <LocationIcon />
  1551. )}
  1552. </TouchableOpacity>
  1553. <RegionPopup
  1554. region={regionData}
  1555. userAvatars={userAvatars}
  1556. userData={userData}
  1557. openEditModal={handleOpenEditModal}
  1558. updateNM={(id, first, last, visits, quality) => {
  1559. if (!token) {
  1560. setIsWarningModalVisible(true);
  1561. return;
  1562. }
  1563. handleUpdateNM(id, first, last, visits, quality);
  1564. const updatedIds = regionsVisited.includes(id)
  1565. ? regionsVisited.filter((visitedId) => visitedId !== id)
  1566. : [...regionsVisited, id];
  1567. setRegionsVisited(updatedIds);
  1568. refetchVisitedCountries();
  1569. }}
  1570. updateDare={(id, visits) => {
  1571. if (!token) {
  1572. setIsWarningModalVisible(true);
  1573. return;
  1574. }
  1575. handleUpdateDare(id, visits);
  1576. const updatedIds = dareVisited.includes(id)
  1577. ? dareVisited.filter((visitedId) => visitedId !== id)
  1578. : [...dareVisited, id];
  1579. setDareVisited(updatedIds);
  1580. }}
  1581. disabled={!token || !isConnected}
  1582. updateSlow={(id, v, s11, s31, s101) => {
  1583. if (!token) {
  1584. setIsWarningModalVisible(true);
  1585. return;
  1586. }
  1587. handleUpdateSlow(id, v, s11, s31, s101);
  1588. const updatedIds = countriesVisited.includes(id)
  1589. ? countriesVisited.filter((visitedId) => visitedId !== id)
  1590. : [...countriesVisited, id];
  1591. setCountriesVisited(updatedIds);
  1592. }}
  1593. openEditSlowModal={handleOpenEditSlowModal}
  1594. />
  1595. </>
  1596. ) : (
  1597. <>
  1598. {!isExpanded ? (
  1599. <TouchableOpacity
  1600. style={[styles.cornerButton, styles.topRightButton]}
  1601. onPress={() => navigation.navigate(NAVIGATION_PAGES.PROFILE_TAB)}
  1602. >
  1603. {token ? (
  1604. userInfoData?.avatar ? (
  1605. <Image
  1606. style={styles.avatar}
  1607. source={{
  1608. uri: API_HOST + '/img/avatars/' + userInfoData?.avatar + '?v=' + avatarVersion
  1609. }}
  1610. />
  1611. ) : (
  1612. <AvatarWithInitials
  1613. text={`${userInfoData?.first_name ? userInfoData?.first_name[0] : ''}${userInfoData?.last_name ? userInfoData?.last_name[0] : ''}`}
  1614. flag={API_HOST + '/img/flags_new/' + userInfoData?.homebase_flag}
  1615. size={48}
  1616. borderColor={Colors.WHITE}
  1617. />
  1618. )
  1619. ) : (
  1620. <ProfileIcon fill={Colors.DARK_BLUE} />
  1621. )}
  1622. </TouchableOpacity>
  1623. ) : null}
  1624. <Animated.View
  1625. style={[
  1626. styles.searchContainer,
  1627. styles.cornerButton,
  1628. styles.topLeftButton,
  1629. animatedStyle,
  1630. { padding: 5 }
  1631. ]}
  1632. >
  1633. {isExpanded ? (
  1634. <>
  1635. <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
  1636. <CloseSvg fill={'#0F3F4F'} />
  1637. </TouchableOpacity>
  1638. <TextInput
  1639. style={styles.input}
  1640. placeholder="Search regions, places, nomads"
  1641. placeholderTextColor={Colors.LIGHT_GRAY}
  1642. value={searchInput}
  1643. onChangeText={(text) => setSearchInput(text)}
  1644. onSubmitEditing={handleSearch}
  1645. />
  1646. <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
  1647. <SearchIcon fill={'#0F3F4F'} />
  1648. </TouchableOpacity>
  1649. </>
  1650. ) : (
  1651. <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
  1652. <SearchIcon fill={'#0F3F4F'} />
  1653. </TouchableOpacity>
  1654. )}
  1655. </Animated.View>
  1656. <View style={[styles.tabs, { bottom: tabBarHeight + 20 }]}>
  1657. <ScrollView
  1658. horizontal
  1659. showsHorizontalScrollIndicator={false}
  1660. contentContainerStyle={{
  1661. paddingHorizontal: 12,
  1662. paddingTop: 6,
  1663. gap: isSmallScreen ? 8 : 12,
  1664. flexDirection: 'row'
  1665. }}
  1666. >
  1667. <MapButton
  1668. onPress={() => {
  1669. try {
  1670. setIsFilterVisible('regions');
  1671. closeCallout();
  1672. } catch (error) {
  1673. console.error('Error opening filter:', error);
  1674. }
  1675. }}
  1676. icon={TravelsIcon}
  1677. text="Travels"
  1678. active={type !== 'blank'}
  1679. />
  1680. <MapButton
  1681. onPress={() => {
  1682. try {
  1683. setIsFilterVisible('series');
  1684. closeCallout();
  1685. } catch (error) {
  1686. console.error('Error opening filter:', error);
  1687. }
  1688. }}
  1689. icon={SeriesIcon}
  1690. text="Series"
  1691. active={seriesFilter.visible}
  1692. />
  1693. {token ? (
  1694. <MapButton
  1695. onPress={() => {
  1696. try {
  1697. setIsFilterVisible('nomads');
  1698. closeCallout();
  1699. } catch (error) {
  1700. console.error('Error opening filter:', error);
  1701. }
  1702. }}
  1703. icon={NomadsIcon}
  1704. text="Nomads"
  1705. active={showNomads}
  1706. >
  1707. {usersOnMapCount && usersOnMapCount?.count > 0 ? (
  1708. <MessagesDot
  1709. messagesCount={usersOnMapCount.count}
  1710. fullNumber={true}
  1711. right={-10}
  1712. top={-8}
  1713. />
  1714. ) : null}
  1715. </MapButton>
  1716. ) : null}
  1717. </ScrollView>
  1718. </View>
  1719. <TouchableOpacity
  1720. onPress={handleGetLocation}
  1721. style={[
  1722. styles.cornerButton,
  1723. styles.bottomButton,
  1724. styles.bottomRightButton,
  1725. { bottom: tabBarHeight + 20 }
  1726. ]}
  1727. >
  1728. {isLocationLoading ? (
  1729. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  1730. ) : (
  1731. <LocationIcon />
  1732. )}
  1733. </TouchableOpacity>
  1734. </>
  1735. )}
  1736. <SearchModal
  1737. searchVisible={searchVisible}
  1738. handleCloseModal={handleCloseModal}
  1739. handleFindRegion={handleFindRegion}
  1740. index={index}
  1741. searchData={searchData}
  1742. setIndex={setIndex}
  1743. token={token}
  1744. />
  1745. <WarningModal
  1746. type={'unauthorized'}
  1747. isVisible={isWarningModalVisible}
  1748. onClose={() => setIsWarningModalVisible(false)}
  1749. />
  1750. <EditNmModal
  1751. isVisible={isEditModalVisible}
  1752. onClose={() => setIsEditModalVisible(false)}
  1753. modalState={modalState}
  1754. updateModalState={handleModalStateChange}
  1755. updateNM={handleUpdateNM}
  1756. />
  1757. <FilterModal
  1758. isFilterVisible={isFilterVisible}
  1759. setIsFilterVisible={setIsFilterVisible}
  1760. tilesTypes={tilesTypes}
  1761. tilesType={tilesType}
  1762. setTilesType={setTilesType}
  1763. setType={setType}
  1764. userId={userId ? +userId : 0}
  1765. setRegionsFilter={setRegionsFilter}
  1766. setSeriesFilter={setSeriesFilter}
  1767. setShowNomads={setShowNomads}
  1768. showNomads={showNomads}
  1769. isPublicView={false}
  1770. isLogged={token ? true : false}
  1771. usersOnMapCount={token && usersOnMapCount?.count ? usersOnMapCount.count : null}
  1772. isConnected={isConnected}
  1773. />
  1774. <EditModal
  1775. isVisible={isEditSlowModalVisible}
  1776. onClose={() => setIsEditSlowModalVisible(false)}
  1777. item={{ ...userData, country_id: regionData?.id }}
  1778. updateSlow={(id, v, s11, s31, s101) => handleUpdateSlow(id, v, s11, s31, s101)}
  1779. />
  1780. <WarningModal
  1781. type={'success'}
  1782. isVisible={askLocationVisible}
  1783. onClose={() => setAskLocationVisible(false)}
  1784. action={handleAcceptPermission}
  1785. 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."
  1786. />
  1787. <WarningModal
  1788. type={'success'}
  1789. isVisible={openSettingsVisible}
  1790. onClose={() => setOpenSettingsVisible(false)}
  1791. action={async () => {
  1792. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  1793. if (!isServicesEnabled) {
  1794. Platform.OS === 'ios'
  1795. ? Linking.openURL('app-settings:')
  1796. : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
  1797. } else {
  1798. Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
  1799. }
  1800. }}
  1801. message="NomadMania app needs location permissions to function properly. Open settings?"
  1802. />
  1803. </SafeAreaView>
  1804. );
  1805. };
  1806. export default MapScreen;