index.tsx 64 KB

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