123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- import {
- Platform,
- TouchableOpacity,
- Image,
- Linking,
- TextInput,
- Dimensions,
- StatusBar,
- ActivityIndicator,
- ScrollView,
- View
- } from 'react-native';
- import React, { FC, useEffect, useRef, useState } from 'react';
- import * as Location from 'expo-location';
- import Animated, {
- Easing,
- useSharedValue,
- useAnimatedStyle,
- withTiming
- } from 'react-native-reanimated';
- import { styles } from './styles';
- import { API_HOST, VECTOR_MAP_HOST } from 'src/constants';
- import { CommonActions, NavigationProp } from '@react-navigation/native';
- import { AvatarWithInitials, LocationPopup } from 'src/components';
- import { Colors } from 'src/theme';
- import CloseSvg from 'assets/icons/close.svg';
- import LocationIcon from 'assets/icons/location.svg';
- import SearchIcon from 'assets/icons/search.svg';
- import FilterModal from '../../MapScreen/FilterModal';
- import SearchModal from '../../MapScreen/UniversalSearch';
- import { useGetUniversalSearch } from '@api/search';
- import { storage, StoreType } from 'src/storage';
- import { NAVIGATION_PAGES } from 'src/types';
- import { SafeAreaView } from 'react-native-safe-area-context';
- import * as MapLibreRN from '@maplibre/maplibre-react-native';
- import {
- usePostGetVisitedCountriesIdsQuery,
- usePostGetVisitedDareIdsQuery,
- usePostGetVisitedRegionsIdsQuery
- } from '@api/maps';
- import moment from 'moment';
- import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg';
- import MapButton from 'src/components/MapButton';
- import ScaleBar from 'src/components/ScaleBar';
- import _ from 'lodash';
- const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
- const generateFilter = (ids: number[]) => {
- return ids.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
- };
- let regions_visited = {
- id: 'regions_visited',
- type: 'fill',
- source: 'regions',
- 'source-layer': 'regions',
- style: {
- fillColor: 'rgba(255, 126, 0, 1)',
- fillOpacity: 0.5,
- fillOutlineColor: 'rgba(14, 80, 109, 0)'
- },
- filter: generateFilter([]),
- maxzoom: 12
- };
- let countries_visited = {
- id: 'countries_visited',
- type: 'fill',
- source: 'countries',
- 'source-layer': 'countries',
- style: {
- fillColor: 'rgba(255, 126, 0, 1)',
- fillOpacity: 0.5,
- fillOutlineColor: 'rgba(14, 80, 109, 0)'
- },
- filter: generateFilter([]),
- maxzoom: 12
- };
- let dare_visited = {
- id: 'dare_visited',
- type: 'fill',
- source: 'dare',
- 'source-layer': 'dare',
- style: {
- fillColor: 'rgba(255, 126, 0, 1)',
- fillOpacity: 0.5,
- fillOutlineColor: 'rgba(255, 126, 0, 1)'
- },
- filter: generateFilter([]),
- maxzoom: 12
- };
- let regions = {
- id: 'regions',
- type: 'fill',
- source: 'regions',
- 'source-layer': 'regions',
- style: {
- fillColor: 'rgba(15, 63, 79, 0)',
- fillOutlineColor: 'rgba(14, 80, 109, 0)'
- },
- filter: ['all'],
- maxzoom: 16
- };
- let countries = {
- id: 'countries',
- type: 'fill',
- source: 'countries',
- 'source-layer': 'countries',
- style: {
- fillColor: 'rgba(15, 63, 79, 0)',
- fillOutlineColor: 'rgba(14, 80, 109, 0)'
- },
- filter: ['all'],
- maxzoom: 16
- };
- let dare = {
- id: 'dare',
- type: 'fill',
- source: 'dare',
- 'source-layer': 'dare',
- style: {
- fillColor: 'rgba(14, 80, 109, 0.6)',
- fillOutlineColor: 'rgba(14, 80, 109, 1)'
- },
- filter: ['all'],
- maxzoom: 16
- };
- type Props = {
- navigation: NavigationProp<any>;
- route: any;
- };
- const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
- const token = storage.get('token', StoreType.STRING) as string;
- const userId = route.params?.userId;
- const data = route.params?.data;
- const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
- const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
- const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([]));
- const [regionsFilter, setRegionsFilter] = useState<any>({
- visitedLabel: 'by',
- year: moment().year()
- });
- const mapRef = useRef<MapLibreRN.MapViewRef>(null);
- const cameraRef = useRef<MapLibreRN.CameraRef>(null);
- const [isFilterVisible, setIsFilterVisible] = useState<string | null>(null);
- const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
- const tilesTypes = [
- { label: 'NM regions', value: 0 },
- { label: 'UN countries', value: 1 },
- { label: 'DARE places', value: 2 }
- ];
- const [type, setType] = useState('regions');
- const [location, setLocation] = useState<Location.LocationObjectCoords | null>(null);
- const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
- const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
- const [isExpanded, setIsExpanded] = useState(false);
- const [searchVisible, setSearchVisible] = useState(false);
- const [index, setIndex] = useState<number>(0);
- const width = useSharedValue(48);
- const usableWidth = Dimensions.get('window').width - 32;
- const [search, setSearch] = useState('');
- const [searchInput, setSearchInput] = useState('');
- const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
- const [isLocationLoading, setIsLocationLoading] = useState(false);
- const [zoom, setZoom] = useState(0);
- const [center, setCenter] = useState<number[] | null>(null);
- const [isZooming, setIsZooming] = useState(true);
- const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
- const { data: visitedRegionIds } = usePostGetVisitedRegionsIdsQuery(
- token,
- regionsFilter.visitedLabel,
- regionsFilter.year,
- +userId,
- type === 'regions' && !!userId
- );
- const { data: visitedCountryIds } = usePostGetVisitedCountriesIdsQuery(
- token,
- regionsFilter.visitedLabel,
- regionsFilter.year,
- +userId,
- type === 'countries' && !!userId
- );
- const { data: visitedDareIds } = usePostGetVisitedDareIdsQuery(
- token,
- +userId,
- type === 'dare' && !!userId
- );
- useEffect(() => {
- if (visitedRegionIds) {
- setRegionsVisitedFilter(generateFilter(visitedRegionIds.ids));
- } else {
- setRegionsVisitedFilter(['==', 'id', -1]);
- }
- }, [visitedRegionIds]);
- useEffect(() => {
- if (visitedCountryIds) {
- setCountriesVisitedFilter(generateFilter(visitedCountryIds.ids));
- } else {
- setCountriesVisitedFilter(['==', 'id', -1]);
- }
- }, [visitedCountryIds]);
- useEffect(() => {
- if (visitedDareIds) {
- setDareVisitedFilter(generateFilter(visitedDareIds.ids));
- } else {
- setDareVisitedFilter(['==', 'id', -1]);
- }
- }, [visitedDareIds]);
- useEffect(() => {
- if (
- data.location_sharing &&
- data.location_last_seen_location?.lng &&
- data.location_last_seen_location?.lat &&
- cameraRef.current
- ) {
- cameraRef.current.flyTo(
- [data.location_last_seen_location.lng, data.location_last_seen_location.lat],
- 1000
- );
- }
- }, [data, cameraRef.current]);
- const handleMapChange = async () => {
- if (!mapRef.current) return;
- if (hideTimer.current) clearTimeout(hideTimer.current);
- setIsZooming(true);
- const currentZoom = await mapRef.current.getZoom();
- const currentCenter = await mapRef.current.getCenter();
- setZoom(currentZoom);
- setCenter(currentCenter);
- };
- const handleGetLocation = async () => {
- setIsLocationLoading(true);
- try {
- let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
- const isServicesEnabled = await Location.hasServicesEnabledAsync();
- if (status === 'granted' && isServicesEnabled) {
- await getLocation();
- } else if (!canAskAgain || !isServicesEnabled) {
- setOpenSettingsVisible(true);
- } else {
- setAskLocationVisible(true);
- }
- } finally {
- setIsLocationLoading(false);
- }
- };
- const getLocation = async () => {
- let currentLocation = await Location.getCurrentPositionAsync({
- accuracy: Location.Accuracy.Balanced
- });
- setLocation(currentLocation.coords);
- if (currentLocation.coords) {
- cameraRef.current?.flyTo(
- [currentLocation.coords.longitude, currentLocation.coords.latitude],
- 1000
- );
- }
- };
- const handleAcceptPermission = async () => {
- setAskLocationVisible(false);
- let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
- const isServicesEnabled = await Location.hasServicesEnabledAsync();
- if (status === 'granted' && isServicesEnabled) {
- getLocation();
- } else if (!canAskAgain || !isServicesEnabled) {
- setOpenSettingsVisible(true);
- }
- };
- const handlePress = () => {
- if (isExpanded) {
- setIndex(0);
- setSearchInput('');
- }
- setIsExpanded((prev) => !prev);
- width.value = withTiming(isExpanded ? 48 : usableWidth, {
- duration: 300,
- easing: Easing.inOut(Easing.ease)
- });
- };
- const animatedStyle = useAnimatedStyle(() => {
- return {
- width: width.value
- };
- });
- const handleSearch = async () => {
- setSearch(searchInput);
- setSearchVisible(true);
- };
- const handleGoBack = () => {
- navigation.goBack();
- };
- const handleCloseModal = () => {
- setSearchInput('');
- setSearchVisible(false);
- handlePress();
- };
- const handleFindRegion = (id: number, type: string) => {
- navigation.dispatch(
- CommonActions.reset({
- index: 1,
- routes: [
- {
- name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
- state: {
- routes: [
- {
- name: NAVIGATION_PAGES.MAP_TAB,
- params: { id, type }
- }
- ]
- }
- }
- ]
- })
- );
- };
- const locationFeature: GeoJSON.Feature<GeoJSON.Point> = {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [data.location_last_seen_location?.lng, data.location_last_seen_location?.lat]
- },
- properties: {}
- };
- return (
- <SafeAreaView style={{ height: '100%' }}>
- <StatusBar translucent backgroundColor="transparent" />
- <MapLibreRN.MapView
- ref={mapRef}
- style={styles.map}
- mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
- rotateEnabled={false}
- attributionEnabled={false}
- onRegionDidChange={() => {
- hideTimer.current = setTimeout(() => {
- setIsZooming(false);
- }, 2000);
- }}
- onRegionIsChanging={handleMapChange}
- onRegionWillChange={_.debounce(handleMapChange, 200)}
- >
- {type === 'regions' && (
- <>
- <MapLibreRN.LineLayer
- id="nm-regions-line-layer"
- sourceID={regions.source}
- sourceLayerID={regions['source-layer']}
- filter={regions.filter as any}
- maxZoomLevel={regions.maxzoom}
- style={{
- lineColor: 'rgba(14, 80, 109, 1)',
- lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
- lineWidthTransition: { duration: 300, delay: 0 }
- }}
- belowLayerID="waterway-name"
- />
- <MapLibreRN.FillLayer
- id={regions.id}
- sourceID={regions.source}
- sourceLayerID={regions['source-layer']}
- filter={regions.filter as any}
- style={regions.style}
- maxZoomLevel={regions.maxzoom}
- belowLayerID={regions_visited.id}
- />
- <MapLibreRN.FillLayer
- id={regions_visited.id}
- sourceID={regions_visited.source}
- sourceLayerID={regions_visited['source-layer']}
- filter={regionsVisitedFilter as any}
- style={regions_visited.style}
- maxZoomLevel={regions_visited.maxzoom}
- belowLayerID="waterway-name"
- />
- </>
- )}
- {type === 'countries' && (
- <>
- <MapLibreRN.LineLayer
- id="countries-line-layer"
- sourceID={countries.source}
- sourceLayerID={countries['source-layer']}
- filter={countries.filter as any}
- maxZoomLevel={countries.maxzoom}
- style={{
- lineColor: 'rgba(14, 80, 109, 1)',
- lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
- lineWidthTransition: { duration: 300, delay: 0 }
- }}
- belowLayerID="waterway-name"
- />
- <MapLibreRN.FillLayer
- id={countries.id}
- sourceID={countries.source}
- sourceLayerID={countries['source-layer']}
- filter={countries.filter as any}
- style={countries.style}
- maxZoomLevel={countries.maxzoom}
- belowLayerID={countries_visited.id}
- />
- <MapLibreRN.FillLayer
- id={countries_visited.id}
- sourceID={countries_visited.source}
- sourceLayerID={countries_visited['source-layer']}
- filter={countriesVisitedFilter as any}
- style={countries_visited.style}
- maxZoomLevel={countries_visited.maxzoom}
- belowLayerID="waterway-name"
- />
- </>
- )}
- {type === 'dare' && (
- <>
- <MapLibreRN.FillLayer
- id={dare.id}
- sourceID={dare.source}
- sourceLayerID={dare['source-layer']}
- filter={dare.filter as any}
- style={dare.style}
- maxZoomLevel={dare.maxzoom}
- belowLayerID={dare_visited.id}
- />
- <MapLibreRN.FillLayer
- id={dare_visited.id}
- sourceID={dare_visited.source}
- sourceLayerID={dare_visited['source-layer']}
- filter={dareVisitedFilter as any}
- style={dare_visited.style}
- maxZoomLevel={dare_visited.maxzoom}
- belowLayerID="waterway-name"
- />
- </>
- )}
- {data.location_sharing && data.location_last_seen_location && data.own_profile !== 1 && (
- <MapLibreRN.ShapeSource id="user_location" shape={locationFeature}>
- <MapLibreRN.SymbolLayer
- id="user_symbol"
- filter={['!', ['has', 'point_count']]}
- aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
- style={{
- iconImage: defaultUserAvatar,
- iconSize: [
- 'interpolate',
- ['linear'],
- ['zoom'],
- 0,
- 0.24,
- 5,
- 0.28,
- 10,
- 0.33,
- 15,
- 0.38,
- 20,
- 0.42
- ],
- iconAllowOverlap: true
- }}
- />
- </MapLibreRN.ShapeSource>
- )}
- <MapLibreRN.Camera ref={cameraRef} />
- {location && (
- <MapLibreRN.UserLocation
- animated={true}
- showsUserHeadingIndicator={true}
- ></MapLibreRN.UserLocation>
- )}
- </MapLibreRN.MapView>
- {center ? (
- <ScaleBar zoom={zoom} latitude={center[1]} isVisible={isZooming} bottom={80} />
- ) : null}
- {!isExpanded ? (
- <TouchableOpacity
- style={[styles.cornerButton, styles.topRightButton]}
- onPress={handleGoBack}
- >
- {data.user_data.avatar ? (
- <Image
- style={styles.avatar}
- source={{ uri: API_HOST + '/img/avatars/' + data.user_data.avatar }}
- />
- ) : (
- <AvatarWithInitials
- text={`${data.user_data.first_name[0] ?? ''}${data.user_data.last_name[0] ?? ''}`}
- flag={API_HOST + '/img/flags_new/' + data.user_data.flag1}
- size={48}
- borderColor={Colors.WHITE}
- />
- )}
- </TouchableOpacity>
- ) : null}
- <View style={styles.tabs}>
- <ScrollView
- horizontal
- showsHorizontalScrollIndicator={false}
- contentContainerStyle={{ marginLeft: 12, gap: 12, flexDirection: 'row' }}
- >
- <MapButton
- onPress={() => {
- setIsFilterVisible('regions');
- }}
- icon={TravelsIcon}
- text="Travels"
- />
- </ScrollView>
- </View>
- <TouchableOpacity
- onPress={handleGetLocation}
- style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
- >
- {isLocationLoading ? (
- <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
- ) : (
- <LocationIcon />
- )}
- </TouchableOpacity>
- <Animated.View
- style={[
- styles.searchContainer,
- styles.cornerButton,
- styles.topLeftButton,
- animatedStyle,
- { padding: 5 }
- ]}
- >
- {isExpanded ? (
- <>
- <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
- <CloseSvg fill={'#0F3F4F'} />
- </TouchableOpacity>
- <TextInput
- style={styles.input}
- placeholder="Search regions, places, nomads"
- placeholderTextColor={Colors.LIGHT_GRAY}
- value={searchInput}
- onChangeText={(text) => setSearchInput(text)}
- onSubmitEditing={handleSearch}
- />
- <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
- <SearchIcon fill={'#0F3F4F'} />
- </TouchableOpacity>
- </>
- ) : (
- <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
- <SearchIcon fill={'#0F3F4F'} />
- </TouchableOpacity>
- )}
- </Animated.View>
- <FilterModal
- isFilterVisible={isFilterVisible}
- setIsFilterVisible={setIsFilterVisible}
- tilesTypes={tilesTypes}
- tilesType={tilesType}
- setTilesType={setTilesType}
- setType={setType}
- userId={userId}
- setRegionsFilter={setRegionsFilter}
- isPublicView={true}
- isLogged={true}
- />
- <LocationPopup
- visible={askLocationVisible}
- onClose={() => setAskLocationVisible(false)}
- onAccept={handleAcceptPermission}
- modalText="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."
- />
- <LocationPopup
- visible={openSettingsVisible}
- onClose={() => setOpenSettingsVisible(false)}
- onAccept={async () => {
- const isServicesEnabled = await Location.hasServicesEnabledAsync();
- if (!isServicesEnabled) {
- Platform.OS === 'ios'
- ? Linking.openURL('app-settings:')
- : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
- } else {
- Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
- }
- }}
- modalText="NomadMania app needs location permissions to function properly. Open settings?"
- />
- <SearchModal
- searchVisible={searchVisible}
- handleCloseModal={handleCloseModal}
- handleFindRegion={handleFindRegion}
- index={index}
- searchData={searchData}
- setIndex={setIndex}
- token={token}
- />
- </SafeAreaView>
- );
- };
- export default UsersMapScreen;
|