|
@@ -1,16 +1,41 @@
|
|
|
-import { Platform, TouchableOpacity, View, Image, Text } from 'react-native';
|
|
|
+import {
|
|
|
+ Platform,
|
|
|
+ TouchableOpacity,
|
|
|
+ View,
|
|
|
+ Image,
|
|
|
+ Animated as Animation,
|
|
|
+ Linking,
|
|
|
+ TextInput,
|
|
|
+ Dimensions
|
|
|
+} from 'react-native';
|
|
|
import React, { FC, useEffect, useRef, useState } from 'react';
|
|
|
-import MapView, { UrlTile } from 'react-native-maps';
|
|
|
+import MapView, { UrlTile, Marker } from 'react-native-maps';
|
|
|
+import * as Location from 'expo-location';
|
|
|
+import Animated, {
|
|
|
+ Easing,
|
|
|
+ useSharedValue,
|
|
|
+ useAnimatedStyle,
|
|
|
+ withTiming
|
|
|
+} from 'react-native-reanimated';
|
|
|
|
|
|
import { styles } from './styles';
|
|
|
import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
|
|
|
-import { NavigationProp } from '@react-navigation/native';
|
|
|
-import { AvatarWithInitials } from 'src/components';
|
|
|
+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 FilterIcon from 'assets/icons/filter.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';
|
|
|
+
|
|
|
+const AnimatedMarker = Animation.createAnimatedComponent(Marker);
|
|
|
|
|
|
type Props = {
|
|
|
navigation: NavigationProp<any>;
|
|
@@ -18,6 +43,7 @@ type Props = {
|
|
|
};
|
|
|
|
|
|
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;
|
|
|
|
|
@@ -26,6 +52,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
|
|
|
const visitedDefaultTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
|
|
|
|
|
|
const mapRef = useRef<MapView>(null);
|
|
|
+ const strokeWidthAnim = useRef(new Animation.Value(2)).current;
|
|
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
|
|
const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
|
|
|
const tilesTypes = [
|
|
@@ -35,20 +62,97 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
|
|
|
];
|
|
|
const [type, setType] = useState(0);
|
|
|
const [visitedTiles, setVisitedTiles] = useState(visitedDefaultTiles);
|
|
|
+ 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);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- navigation.getParent()?.setOptions({
|
|
|
- tabBarStyle: {
|
|
|
- display: 'none',
|
|
|
- position: 'absolute',
|
|
|
- ...Platform.select({
|
|
|
- android: {
|
|
|
- height: 58
|
|
|
- }
|
|
|
+ Animation.loop(
|
|
|
+ Animation.sequence([
|
|
|
+ Animation.timing(strokeWidthAnim, {
|
|
|
+ toValue: 3,
|
|
|
+ duration: 700,
|
|
|
+ useNativeDriver: false
|
|
|
+ }),
|
|
|
+ Animation.timing(strokeWidthAnim, {
|
|
|
+ toValue: 2,
|
|
|
+ duration: 700,
|
|
|
+ useNativeDriver: false
|
|
|
})
|
|
|
- }
|
|
|
+ ])
|
|
|
+ ).start();
|
|
|
+ }, [strokeWidthAnim]);
|
|
|
+
|
|
|
+ const handleGetLocation = async () => {
|
|
|
+ let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
|
|
|
+
|
|
|
+ if (status === 'granted') {
|
|
|
+ getLocation();
|
|
|
+ } else if (!canAskAgain) {
|
|
|
+ setOpenSettingsVisible(true);
|
|
|
+ } else {
|
|
|
+ setAskLocationVisible(true);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getLocation = async () => {
|
|
|
+ let currentLocation = await Location.getCurrentPositionAsync({
|
|
|
+ accuracy: Location.Accuracy.Balanced
|
|
|
+ });
|
|
|
+ setLocation(currentLocation.coords);
|
|
|
+
|
|
|
+ mapRef.current?.animateToRegion(
|
|
|
+ {
|
|
|
+ latitude: currentLocation.coords.latitude,
|
|
|
+ longitude: currentLocation.coords.longitude,
|
|
|
+ latitudeDelta: 5,
|
|
|
+ longitudeDelta: 5
|
|
|
+ },
|
|
|
+ 800
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAcceptPermission = async () => {
|
|
|
+ setAskLocationVisible(false);
|
|
|
+ let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
|
|
|
+
|
|
|
+ if (status === 'granted') {
|
|
|
+ getLocation();
|
|
|
+ } else if (!canAskAgain) {
|
|
|
+ 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)
|
|
|
});
|
|
|
- }, [navigation]);
|
|
|
+ };
|
|
|
+
|
|
|
+ const animatedStyle = useAnimatedStyle(() => {
|
|
|
+ return {
|
|
|
+ width: width.value
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleSearch = async () => {
|
|
|
+ setSearch(searchInput);
|
|
|
+ setSearchVisible(true);
|
|
|
+ };
|
|
|
|
|
|
const renderMapTiles = (url: string, zIndex: number, opacity = 1) => (
|
|
|
<UrlTile
|
|
@@ -67,6 +171,33 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
|
|
|
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 }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<View style={styles.container}>
|
|
|
<MapView
|
|
@@ -88,36 +219,79 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
|
|
|
{renderMapTiles(tilesBaseURL, 1)}
|
|
|
{type !== 1 && renderMapTiles(gridUrl, 2)}
|
|
|
{userId && renderMapTiles(visitedTiles, 2, 0.5)}
|
|
|
+ {location && (
|
|
|
+ <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
|
|
|
+ <Animation.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
|
|
|
+ </AnimatedMarker>
|
|
|
+ )}
|
|
|
</MapView>
|
|
|
|
|
|
- <TouchableOpacity
|
|
|
- style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
|
|
|
- onPress={handleGoBack}
|
|
|
- >
|
|
|
- <CloseSvg fill="white" width={13} height={13} />
|
|
|
- <Text style={styles.textClose}>Close</Text>
|
|
|
- </TouchableOpacity>
|
|
|
- <View style={[styles.cornerButton, styles.topRightButton]}>
|
|
|
- {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}
|
|
|
- />
|
|
|
- )}
|
|
|
- </View>
|
|
|
+ {!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}
|
|
|
+
|
|
|
<TouchableOpacity
|
|
|
style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
|
|
|
onPress={() => setIsFilterVisible(true)}
|
|
|
>
|
|
|
<FilterIcon />
|
|
|
</TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={handleGetLocation}
|
|
|
+ style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
|
|
|
+ >
|
|
|
+ <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}
|
|
@@ -130,6 +304,29 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
|
|
|
setVisitedTiles={setVisitedTiles}
|
|
|
isPublicView={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={() =>
|
|
|
+ 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}
|
|
|
+ />
|
|
|
</View>
|
|
|
);
|
|
|
};
|