index.tsx 73 KB

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