index.tsx 74 KB

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