index.tsx 68 KB

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