Browse Source

sharing filter

Viktoriia 1 day ago
parent
commit
176455998d

+ 90 - 0
src/components/MultiSelectorCountries/index.tsx

@@ -0,0 +1,90 @@
+import ReactModal from 'react-native-modal';
+import { ScrollView, Text, TouchableOpacity, View, Image } from 'react-native';
+import { API_HOST } from 'src/constants';
+import { useState, useEffect, useCallback } from 'react';
+import { styles } from './styles';
+
+import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
+import { Colors } from 'src/theme';
+
+const MultiSelectorCountries = ({
+  isVisible,
+  onClose,
+  onSelect,
+  data,
+  initialSelectedIds = []
+}: {
+  isVisible: boolean;
+  onClose: () => void;
+  onSelect: (
+    selectedItems: { country: number; name: string; count: number; flag: string | null }[]
+  ) => void;
+  data: { country: number; name: string; count: number; flag: string | null }[];
+  initialSelectedIds?: number[];
+}) => {
+  const [selectedIds, setSelectedIds] = useState<number[]>(initialSelectedIds);
+
+  useEffect(() => {
+    if (isVisible) {
+      setSelectedIds(initialSelectedIds);
+    }
+  }, [isVisible]);
+
+  const toggleSelection = (item: {
+    country: number;
+    name: string;
+    count: number;
+    flag: string | null;
+  }) => {
+    setSelectedIds((prev) => {
+      if (prev.includes(item.country)) {
+        return prev.filter((id) => id !== item.country);
+      } else {
+        return [...prev, item.country];
+      }
+    });
+  };
+
+  const handleClose = () => {
+    const selectedItems = data.filter((item) => selectedIds.includes(item.country));
+    onSelect(selectedItems);
+    onClose();
+  };
+
+  const isSelected = useCallback((id: number) => selectedIds.includes(id), [selectedIds]);
+
+  return (
+    <ReactModal
+      isVisible={isVisible}
+      onBackdropPress={handleClose}
+      style={styles.modal}
+      statusBarTranslucent={true}
+      presentationStyle="overFullScreen"
+    >
+      <View style={styles.wrapper}>
+        <ScrollView style={{ paddingBottom: 16 }} showsVerticalScrollIndicator={false}>
+          {data?.map((item) => (
+            <TouchableOpacity
+              key={item.country}
+              style={[styles.btnOption]}
+              onPress={() => toggleSelection(item)}
+            >
+              <View style={styles.row}>
+                <Image source={{ uri: `${API_HOST}${item.flag}` }} style={styles.flag} />
+                <Text style={[styles.btnOptionText]}>{item.name}</Text>
+                <Text style={[styles.btnOptionText]}>{item.count}</Text>
+              </View>
+              <View style={styles.unselectedCircle}>
+                {isSelected(item.country) && (
+                  <CheckSvg fill={Colors.DARK_BLUE} height={20} width={20} />
+                )}
+              </View>
+            </TouchableOpacity>
+          ))}
+        </ScrollView>
+      </View>
+    </ReactModal>
+  );
+};
+
+export default MultiSelectorCountries;

+ 46 - 0
src/components/MultiSelectorCountries/styles.tsx

@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  btnOption: {
+    paddingHorizontal: 12,
+    paddingVertical: 9,
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8
+  },
+  row: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+    flex: 1
+  },
+  btnOptionText: { fontSize: 16, fontWeight: '600', color: Colors.DARK_BLUE },
+  wrapper: {
+    backgroundColor: Colors.WHITE,
+    padding: 16,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    height: '86%'
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  flag: {
+    width: 30,
+    height: 30,
+    borderRadius: 15,
+    borderColor: Colors.BORDER_LIGHT,
+    borderWidth: 1
+  },
+  unselectedCircle: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    justifyContent: 'center',
+    alignItems: 'center'
+  }
+});

+ 1 - 0
src/components/index.ts

@@ -21,3 +21,4 @@ export * from './ErrorModal';
 export * from './MessagesDot';
 export * from './MapButton';
 export * from './ScaleBar';
+export * from './MultiSelectorCountries';

+ 20 - 1
src/modules/api/location/location-api.ts

@@ -15,6 +15,16 @@ export interface PostGetUsersLocationReturn extends ResponseType {
 
 export interface PostGetUserCountReturn extends ResponseType {
   count: number;
+  friends_count: number;
+}
+
+export interface PostGetCountriesReturn extends ResponseType {
+  countries: {
+    count: number;
+    country: number;
+    flag: string | null;
+    name: string;
+  }[];
 }
 
 export const locationApi = {
@@ -29,5 +39,14 @@ export const locationApi = {
   getUsersCount: (token: string) =>
     request.postForm<PostGetUserCountReturn>(API.GET_USERS_COUNT, { token }),
   setSettingsRegions: (token: string, sharing: 0 | 1) =>
-    request.postForm<ResponseType>(API.SET_LOCATION_REGIONS, { token, sharing })
+    request.postForm<ResponseType>(API.SET_LOCATION_REGIONS, { token, sharing }),
+  getCountries: (token: string) =>
+    request.postForm<PostGetCountriesReturn>(API.GET_COUNTRIES_FOR_LOCATION_DATA, { token }),
+  getLocationFiltered: (token: string, friends: 0 | 1, trusted: 0 | 1, countries?: string) =>
+    request.postForm<ResponseType>(API.GET_USERS_LOCATION_FILTERED, {
+      token,
+      friends,
+      trusted,
+      countries
+    })
 };

+ 3 - 1
src/modules/api/location/location-query-keys.tsx

@@ -4,5 +4,7 @@ export const locationQueryKeys = {
   updateLocation: () => ['location', 'updateLocation'],
   getUsersLocation: (token: string) => ['location', 'getUsersLocation', token],
   getUsersCount: (token: string) => ['location', 'getUsersCount', token],
-  setSettingsRegions: () => ['location', 'setSettingsRegions']
+  setSettingsRegions: () => ['location', 'setSettingsRegions'],
+  getCountries: (token: string) => ['location', 'getCountries', token],
+  getLocationFiltered: () => ['location', 'getLocationFiltered']
 };

+ 2 - 0
src/modules/api/location/queries/index.ts

@@ -4,3 +4,5 @@ export * from './use-post-update-location';
 export * from './use-post-get-users-location';
 export * from './use-post-get-users-on-map-count';
 export * from './use-post-set-settings-regions';
+export * from './use-post-get-list-of-countries-for-location-data';
+export * from './use-post-get-users-location-filtered';

+ 17 - 0
src/modules/api/location/queries/use-post-get-list-of-countries-for-location-data.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi, type PostGetCountriesReturn } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetListOfCountriesForLocationDataQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetCountriesReturn, BaseAxiosError>({
+    queryKey: locationQueryKeys.getCountries(token),
+    queryFn: async () => {
+      const response = await locationApi.getCountries(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 27 - 0
src/modules/api/location/queries/use-post-get-users-location-filtered.tsx

@@ -0,0 +1,27 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { locationQueryKeys } from '../location-query-keys';
+import { locationApi } from '../location-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostGetUsersLocationFilteredMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; friends: 0 | 1; trusted: 0 | 1; countries?: string },
+    ResponseType
+  >({
+    mutationKey: locationQueryKeys.getLocationFiltered(),
+    mutationFn: async (variables) => {
+      const response = await locationApi.getLocationFiltered(
+        variables.token,
+        variables.friends,
+        variables.trusted,
+        variables.countries
+      );
+      return response.data;
+    }
+  });
+};

+ 307 - 43
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -24,6 +24,7 @@ import { RadioButton } from 'react-native-paper';
 import { storage, StoreType } from 'src/storage';
 import moment from 'moment';
 import {
+  usePostGetListOfCountriesForLocationDataQuery,
   usePostGetSettingsQuery,
   usePostSetSettingsMutation,
   usePostUpdateLocationMutation
@@ -38,6 +39,11 @@ import {
   startBackgroundLocationUpdates,
   stopBackgroundLocationUpdates
 } from 'src/utils/backgroundLocation';
+import MultiSelectorCountries from 'src/components/MultiSelectorCountries';
+import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
+import { getFontSize } from 'src/utils';
+import InfoIcon from 'assets/icons/info-solid.svg';
+import Tooltip from 'react-native-walkthrough-tooltip';
 
 const FilterModal = ({
   isFilterVisible,
@@ -49,12 +55,15 @@ const FilterModal = ({
   userId,
   setRegionsFilter,
   setSeriesFilter,
+  setNomadsFilter,
   isPublicView,
   isLogged = true,
   showNomads,
   setShowNomads,
   usersOnMapCount,
-  isConnected = true
+  friendsOnTheMapCount,
+  isConnected = true,
+  isPremium
 }: {
   isFilterVisible: string | null;
   setIsFilterVisible: (filterType: string | null) => void;
@@ -65,18 +74,25 @@ const FilterModal = ({
   userId: number;
   setRegionsFilter: (data: { visitedLabel: 'in' | 'by'; year: number }) => void;
   setSeriesFilter?: (filter: any) => void;
+  setNomadsFilter?: (filter: any) => void;
   isPublicView: boolean;
   isLogged: boolean;
   showNomads?: boolean;
   setShowNomads?: (showNomads: boolean) => void;
   usersOnMapCount?: number | null;
+  friendsOnTheMapCount?: number | null;
   isConnected?: boolean;
+  isPremium?: boolean;
 }) => {
   const token = storage.get('token', StoreType.STRING) as string;
   const { data: locationSettings } = usePostGetSettingsQuery(
     token,
     isLogged && !isPublicView && isConnected
   );
+  const { data: countriesData } = usePostGetListOfCountriesForLocationDataQuery(
+    token,
+    isLogged && !isPublicView && isConnected
+  );
   const { mutateAsync: setSettings } = usePostSetSettingsMutation();
   const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
   const [selectedYear, setSelectedYear] = useState<{ label: string; value: number } | null>(null);
@@ -96,6 +112,9 @@ const FilterModal = ({
   const [selectedSeries, setSelectedSeries] = useState<number[]>([]);
   const [seriesVisible, setSeriesVisible] = useState(true);
   const [selectedSeriesFilter, setSelectedSeriesFilter] = useState(-1);
+  const [selectedCountries, setSelectedCountries] = useState<any[]>([]);
+  const [friendsVisible, setFriendsVisible] = useState(false);
+  const [trustedVisible, setTrustedVisible] = useState(false);
   const savedFilterSettings = !isPublicView
     ? (storage.get('filterSettings', StoreType.STRING) as string)
     : null;
@@ -105,6 +124,12 @@ const FilterModal = ({
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
   const [openSettingsBackgroundVisible, setOpenSettingsBackgroundVisible] =
     useState<boolean>(false);
+  const [countrySelectorVisible, setCountrySelectorVisible] = useState(false);
+  const [countriesForSelector, setCountriesForSelector] = useState<
+    { country: number; name: string; count: number; flag: string | null }[]
+  >([]);
+  const [premiumModalVisible, setPremiumModalVisible] = useState(false);
+  const [toolTipVisible, setToolTipVisible] = useState<boolean>(false);
 
   useEffect(() => {
     const syncSettings = async () => {
@@ -119,6 +144,12 @@ const FilterModal = ({
     syncSettings();
   }, [locationSettings]);
 
+  useEffect(() => {
+    if (countriesData && countriesData.countries) {
+      setCountriesForSelector(countriesData.countries.filter((country: any) => country.count > 0));
+    }
+  }, [countriesData]);
+
   useEffect(() => {
     const loadFilterSettings = () => {
       try {
@@ -130,6 +161,9 @@ const FilterModal = ({
           setSelectedSeries(filterSettings.seriesFilter.groups);
           setSeriesVisible(filterSettings.seriesFilter.visible);
           setSelectedSeriesFilter(filterSettings.seriesFilter.status);
+          setSelectedCountries(filterSettings.nomadsFilter.countries || []);
+          setFriendsVisible(filterSettings.nomadsFilter.friends === 1);
+          setTrustedVisible(filterSettings.nomadsFilter.trusted === 1);
         }
       } catch (error) {
         console.error('Failed to load filter settings', error);
@@ -141,6 +175,17 @@ const FilterModal = ({
     }
   }, [savedFilterSettings, isFilterVisible]);
 
+  const handleCountrySelect = (
+    selectedItems: { country: number; name: string; count: number; flag: string | null }[]
+  ) => {
+    if (selectedItems.length > 0) {
+      setShowNomads && setShowNomads(false);
+      storage.set('showNomads', false);
+    }
+    setSelectedCountries(selectedItems);
+    setCountrySelectorVisible(false);
+  };
+
   const saveFilterSettings = async () => {
     if (isLogged && !isPublicView) {
       try {
@@ -161,7 +206,13 @@ const FilterModal = ({
             groups: selectedSeries,
             applied: true,
             status: selectedSeriesFilter
-          }
+          },
+          nomadsFilter: {
+            friends: friendsVisible ? 1 : 0,
+            trusted: trustedVisible ? 1 : 0,
+            countries: selectedCountries
+          },
+          showNomads: showNomads
         };
         storage.set('filterSettings', JSON.stringify(filterSettings));
       } catch (error) {
@@ -207,6 +258,18 @@ const FilterModal = ({
     }
   }, [selectedSeries, seriesVisible, selectedSeriesFilter]);
 
+  useEffect(() => {
+    if (isFilterVisible && isFilterVisible === 'nomads') {
+      setNomadsFilter &&
+        setNomadsFilter({
+          friends: friendsVisible ? 1 : 0,
+          trusted: trustedVisible ? 1 : 0,
+          countries: selectedCountries
+        });
+      saveFilterSettings();
+    }
+  }, [friendsVisible, trustedVisible, selectedCountries]);
+
   useEffect(() => {
     if (isFilterVisible && isFilterVisible === 'regions') {
       try {
@@ -430,9 +493,32 @@ const FilterModal = ({
       setSettings({ token, sharing: 0 });
       setShowNomads && setShowNomads(false);
       storage.set('showNomads', false);
+      setTrustedVisible(false);
+      setFriendsVisible(false);
+      setSelectedCountries([]);
     }
   };
 
+  const toggleTrustedSwitch = async () => {
+    if (!isPremium) {
+      setPremiumModalVisible(true);
+      return;
+    }
+    setShowNomads && setShowNomads(false);
+    storage.set('showNomads', false);
+    setTrustedVisible(!trustedVisible);
+  };
+
+  const toggleFriendsSwitch = async () => {
+    if (!isPremium) {
+      setPremiumModalVisible(true);
+      return;
+    }
+    setShowNomads && setShowNomads(false);
+    storage.set('showNomads', false);
+    setFriendsVisible(!friendsVisible);
+  };
+
   const requestBackgroundPermissionSafe = async () => {
     await new Promise((resolve) => setTimeout(resolve, 300));
     return await Location.requestBackgroundPermissionsAsync();
@@ -510,6 +596,11 @@ const FilterModal = ({
   };
 
   const toggleNomadsSwitch = () => {
+    if (!showNomads) {
+      setSelectedCountries([]);
+      setTrustedVisible(false);
+      setFriendsVisible(false);
+    }
     setShowNomads && setShowNomads(!showNomads);
     storage.set('showNomads', !showNomads);
   };
@@ -519,7 +610,92 @@ const FilterModal = ({
       <View style={[styles.sceneContainer, { flex: 0 }]}>
         <View style={styles.textContainer}>
           <Text style={styles.text}>The location is shared with 1 km radius precision.</Text>
+          <Tooltip
+            isVisible={toolTipVisible}
+            content={
+              <View style={{ gap: 6 }}>
+                <Text style={[styles.text, styles.boldText]}>
+                  At NomadMania, we respect your privacy.
+                </Text>
+                <Text style={[styles.text]}>
+                  If you choose to share your location, it will be used to show your current
+                  location to other users who also share theirs.
+                </Text>
+                <Text style={[styles.text]}>
+                  You can choose how you want to share your location:
+                </Text>
+                <View style={[styles.bulletItem]}>
+                  <Text style={styles.bulletIcon}>{'\u2022'}</Text>
+                  <Text style={[styles.text, { flex: 1 }]}>
+                    <Text style={styles.boldText}>Only when the app is open</Text> – Your location
+                    updates only when you open the app. This uses less battery but may be less
+                    accurate.
+                  </Text>
+                </View>
+                <View style={styles.bulletItem}>
+                  <Text style={styles.bulletIcon}>{'\u2022'}</Text>
+                  <Text style={[styles.text, { flex: 1 }]}>
+                    <Text style={styles.boldText}>In the background</Text> – Your location stays up
+                    to date even when the app is closed. Other users see your latest location.
+                  </Text>
+                </View>
+                <Text style={[styles.text]}>
+                  You’re always in control, and you can change these settings anytime in the app{' '}
+                  <Text
+                    style={{ color: Colors.ORANGE }}
+                    onPress={() =>
+                      Platform.OS === 'ios'
+                        ? Linking.openURL('app-settings:')
+                        : Linking.openSettings()
+                    }
+                  >
+                    Settings
+                  </Text>{' '}
+                  section.
+                </Text>
+              </View>
+            }
+            contentStyle={{ backgroundColor: Colors.WHITE }}
+            placement="top"
+            onClose={() => setToolTipVisible(false)}
+            backgroundColor="transparent"
+            allowChildInteraction={false}
+          >
+            <TouchableOpacity
+              style={{ paddingHorizontal: 5 }}
+              onPress={() => setToolTipVisible(true)}
+              hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+            >
+              <InfoIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+            </TouchableOpacity>
+          </Tooltip>
         </View>
+
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleSettingsSwitch}
+        >
+          <View style={styles.alignStyle}>
+            <SharingIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+            <Text style={styles.buttonLabel}>Share my location to see others</Text>
+          </View>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleSettingsSwitch}
+              value={isSharing}
+              style={{ transform: 'scale(0.8)' }}
+            />
+          </View>
+        </TouchableOpacity>
+
         <TouchableOpacity
           style={[
             styles.alignStyle,
@@ -538,7 +714,7 @@ const FilterModal = ({
               height={20}
             />
             <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
-              Show Nomads - {usersOnMapCount ? formatNumber(usersOnMapCount) : null} online
+              Show all Nomads - {usersOnMapCount ? formatNumber(usersOnMapCount) : null} online
             </Text>
           </View>
           <View>
@@ -553,7 +729,22 @@ const FilterModal = ({
           </View>
         </TouchableOpacity>
 
-        <TouchableOpacity
+        <View style={{ flexDirection: 'row', alignItems: 'center', marginVertical: 12 }}>
+          <View style={styles.divider} />
+          <Text
+            style={{
+              color: Colors.DARK_BLUE,
+              fontSize: getFontSize(12),
+              fontWeight: '500',
+              paddingHorizontal: 4
+            }}
+          >
+            Premium Features
+          </Text>
+          <View style={styles.divider} />
+        </View>
+
+        {/* <TouchableOpacity
           style={[
             styles.alignStyle,
             styles.buttonWrapper,
@@ -561,58 +752,131 @@ const FilterModal = ({
               justifyContent: 'space-between'
             }
           ]}
-          onPress={toggleSettingsSwitch}
+          onPress={toggleTrustedSwitch}
+          disabled={!isSharing}
         >
           <View style={styles.alignStyle}>
-            <SharingIcon fill={Colors.DARK_BLUE} width={20} height={20} />
-            <Text style={styles.buttonLabel}>Share location to see others</Text>
+            <UsersIcon
+              fill={isSharing ? Colors.DARK_BLUE : Colors.LIGHT_GRAY}
+              width={20}
+              height={20}
+            />
+            <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
+              Show trusted Nomads
+            </Text>
           </View>
           <View>
             <Switch
               trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
               thumbColor={Colors.WHITE}
-              onValueChange={toggleSettingsSwitch}
-              value={isSharing}
+              onValueChange={toggleTrustedSwitch}
+              value={trustedVisible}
               style={{ transform: 'scale(0.8)' }}
+              disabled={!isSharing}
             />
           </View>
-        </TouchableOpacity>
-        <View style={{ marginVertical: 12, gap: 6 }}>
-          <Text style={[styles.text, styles.boldText]}>
-            At NomadMania, we respect your privacy.
-          </Text>
-          <Text style={[styles.text]}>
-            If you choose to share your location, it will be used to show your current location to
-            other users who also share theirs.
-          </Text>
-          <Text style={[styles.text]}>You can choose how you want to share your location:</Text>
-          <View style={[styles.bulletItem]}>
-            <Text style={styles.bulletIcon}>{'\u2022'}</Text>
-            <Text style={[styles.text, { flex: 1 }]}>
-              <Text style={styles.boldText}>Only when the app is open</Text> – Your location updates
-              only when you open the app. This uses less battery but may be less accurate.
+        </TouchableOpacity> */}
+
+        <TouchableOpacity
+          style={[
+            styles.alignStyle,
+            styles.buttonWrapper,
+            {
+              justifyContent: 'space-between'
+            }
+          ]}
+          onPress={toggleFriendsSwitch}
+          disabled={!isSharing}
+        >
+          <View style={styles.alignStyle}>
+            <UsersIcon
+              fill={isSharing ? Colors.DARK_BLUE : Colors.LIGHT_GRAY}
+              width={20}
+              height={20}
+            />
+            <Text style={[styles.buttonLabel, !isSharing ? { color: Colors.LIGHT_GRAY } : {}]}>
+              Show my friends - {friendsOnTheMapCount ? formatNumber(friendsOnTheMapCount) : null}{' '}
+              online
             </Text>
           </View>
-          <View style={styles.bulletItem}>
-            <Text style={styles.bulletIcon}>{'\u2022'}</Text>
-            <Text style={[styles.text, { flex: 1 }]}>
-              <Text style={styles.boldText}>In the background</Text> – Your location stays up to
-              date even when the app is closed. Other users see your latest location.
-            </Text>
+          <View>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={toggleFriendsSwitch}
+              value={friendsVisible}
+              style={{ transform: 'scale(0.8)' }}
+              disabled={!isSharing}
+            />
           </View>
-          <Text style={[styles.text]}>
-            You’re always in control, and you can change these settings anytime in the app{' '}
-            <Text
-              style={{ color: Colors.ORANGE }}
-              onPress={() =>
-                Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
-              }
-            >
-              Settings
-            </Text>{' '}
-            section.
-          </Text>
-        </View>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={styles.megaSelector}
+          onPress={() => {
+            if (!isPremium) {
+              setPremiumModalVisible(true);
+              return;
+            }
+            setCountrySelectorVisible(true);
+          }}
+          disabled={!isSharing}
+        >
+          <Text style={styles.megaButtonText}>Show Nomads from</Text>
+          <ChevronIcon width={18} height={18} />
+        </TouchableOpacity>
+
+        {selectedCountries.length > 0 && (
+          <View
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              flexWrap: 'wrap',
+              marginLeft: 12,
+              gap: 6,
+              marginVertical: 8
+            }}
+          >
+            {selectedCountries.map((item) => (
+              <View
+                key={item.country}
+                style={{
+                  marginLeft: -12
+                }}
+              >
+                <Image
+                  source={{ uri: API_HOST + item.flag }}
+                  style={{
+                    width: 28,
+                    height: 28,
+                    borderRadius: 14,
+                    borderWidth: 0.5,
+                    borderColor: Colors.BORDER_LIGHT
+                  }}
+                />
+              </View>
+            ))}
+          </View>
+        )}
+
+        <MultiSelectorCountries
+          isVisible={countrySelectorVisible}
+          onClose={() => setCountrySelectorVisible(false)}
+          onSelect={handleCountrySelect}
+          data={countriesForSelector ?? []}
+          initialSelectedIds={selectedCountries.map((item) => parseInt(item.country))}
+        />
+
+        <WarningModal
+          type={'success'}
+          isVisible={premiumModalVisible}
+          message={
+            'This feature is available to Premium users. Premium account settings can be managed on our website.'
+          }
+          action={() => {}}
+          onClose={() => setPremiumModalVisible(false)}
+          title={'Premium Feature'}
+        />
 
         <WarningModal
           type={'success'}

+ 29 - 2
src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx

@@ -54,7 +54,8 @@ export const styles = StyleSheet.create({
   closeBtn: { backgroundColor: Colors.WHITE, borderColor: '#B7C6CB' },
   sceneContainer: {
     flex: 1,
-    paddingVertical: 24,
+    paddingVertical: 12,
+    marginBottom: 24,
     justifyContent: 'space-between'
   },
   row: { flexDirection: 'row', alignItems: 'center' },
@@ -82,7 +83,12 @@ export const styles = StyleSheet.create({
     fontWeight: '700',
     marginLeft: 15
   },
-  textContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', marginBottom: 12 },
+  textContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 12,
+    justifyContent: 'space-between'
+  },
   textWithIcon: { lineHeight: 26, fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
   text: { fontSize: 12, color: Colors.DARK_BLUE, fontStyle: 'italic' },
   boldText: { fontWeight: '700' },
@@ -109,5 +115,26 @@ export const styles = StyleSheet.create({
     shadowRadius: 4,
     shadowOpacity: 1,
     elevation: 8
+  },
+  megaSelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingVertical: 10,
+    borderRadius: 6,
+    backgroundColor: Colors.FILL_LIGHT,
+    justifyContent: 'space-between',
+    marginTop: 12,
+    marginBottom: 8
+  },
+  megaButtonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  divider: {
+    height: 1,
+    flex: 1,
+    backgroundColor: Colors.DARK_LIGHT
   }
 });

+ 69 - 4
src/screens/InAppScreens/MapScreen/index.tsx

@@ -70,6 +70,7 @@ import MarkerItem from './MarkerItem';
 import {
   usePostGetSettingsQuery,
   usePostGetUsersCountQuery,
+  usePostGetUsersLocationFilteredMutation,
   usePostGetUsersLocationQuery,
   usePostUpdateLocationMutation
 } from '@api/location';
@@ -92,6 +93,7 @@ import {
 } from 'src/utils/backgroundLocation';
 import { SheetManager } from 'react-native-actions-sheet';
 import MultipleSeriesModal from './MultipleSeriesModal';
+import { useSubscription } from 'src/screens/OfflineMapsScreen/useSubscription';
 
 const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png');
 const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
@@ -319,6 +321,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     visitedLabel: 'by',
     year: moment().year()
   });
+  const [nomadsFilter, setNomadsFilter] = useState<any>({
+    friends: 0,
+    trusted: 0,
+    countries: []
+  });
   const [showNomads, setShowNomads] = useState(
     (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false
   );
@@ -415,6 +422,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     token,
     !!token && showNomads && Boolean(location) && isConnected
   );
+  const { mutateAsync: getLocationFiltered } = usePostGetUsersLocationFilteredMutation();
   const [selectedUser, setSelectedUser] = useState<any>(null);
   const [zoom, setZoom] = useState(0);
   const [center, setCenter] = useState<number[] | null>(null);
@@ -429,6 +437,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   const [didFinishLoadingStyle, setDidFinishLoadingStyle] = useState(false);
 
+  const { isPremium } = useSubscription();
+
   useEffect(() => {
     if (netInfo && netInfo.isConnected !== null) {
       setIsConnected(netInfo.isConnected);
@@ -566,6 +576,12 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             : 'by',
         year: filterSettings.selectedYear?.value ?? moment().year()
       });
+      setShowNomads(filterSettings.showNomads ?? false);
+      setNomadsFilter({
+        friends: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.friends : 0,
+        trusted: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.trusted : 0,
+        countries: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.countries : undefined
+      });
       setSeriesFilter(filterSettings.seriesFilter);
     }
   }, []);
@@ -645,6 +661,43 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   }, [dareVisited]);
 
+  useEffect(() => {
+    if (!isPremium || !token || showNomads) return;
+    const countriesFilter = nomadsFilter.countries
+      ? nomadsFilter.countries.map((country: any) => country.country)
+      : [];
+
+    getLocationFiltered(
+      {
+        token,
+        friends: nomadsFilter.friends,
+        trusted: nomadsFilter.trusted,
+        countries: nomadsFilter.countries ? JSON.stringify(countriesFilter) : undefined
+      },
+      {
+        onSuccess: (data) => {
+          if (data && data?.geojson) {
+            const filteredNomads: GeoJSON.FeatureCollection = {
+              type: 'FeatureCollection',
+              features: data.geojson.features.filter(
+                (feature: GeoJSON.Feature) => feature.properties?.id !== +userId
+              )
+            };
+
+            if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) {
+              setNomads(filteredNomads);
+            }
+          } else {
+            setNomads(null);
+          }
+        },
+        onError: (error) => {
+          console.error('Error fetching filtered users location:', error);
+        }
+      }
+    );
+  }, [nomadsFilter]);
+
   useEffect(() => {
     if (!seriesFilter.visible) {
       setSeriesVisitedFilter(generateFilter([]));
@@ -1584,7 +1637,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             : null}
         </MapLibreRN.VectorSource>
 
-        {nomads && showNomads && (
+        {nomads && (showNomads || nomadsFilter.friends || nomadsFilter.countries?.length) ? (
           <MapLibreRN.ShapeSource
             ref={shapeSourceRef}
             tolerance={20}
@@ -1686,7 +1739,14 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               filter={['!', ['has', 'point_count']]}
               aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
               style={{
-                iconImage: '00',
+                iconImage: [
+                  'case',
+                  ['==', ['get', 'friend'], 1],
+                  '02',
+                  // ['==', ['get', 'trusted'], 1],
+                  // '01',
+                  '00'
+                ],
                 // iconSize: [
                 //   'interpolate',
                 //   ['linear'],
@@ -1706,7 +1766,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               }}
             ></MapLibreRN.SymbolLayer>
           </MapLibreRN.ShapeSource>
-        )}
+        ) : null}
 
         {selectedUser && <UserItem marker={selectedUser} />}
 
@@ -1927,7 +1987,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
                   }}
                   icon={NomadsIcon}
                   text="Nomads"
-                  active={showNomads}
+                  active={showNomads || nomadsFilter.friends || nomadsFilter.countries?.length}
                 >
                   {usersOnMapCount && usersOnMapCount?.count > 0 ? (
                     <MessagesDot
@@ -1992,11 +2052,16 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         setRegionsFilter={setRegionsFilter}
         setSeriesFilter={setSeriesFilter}
         setShowNomads={setShowNomads}
+        setNomadsFilter={setNomadsFilter}
         showNomads={showNomads}
         isPublicView={false}
         isLogged={token ? true : false}
         usersOnMapCount={token && usersOnMapCount?.count ? usersOnMapCount.count : null}
+        friendsOnTheMapCount={
+          token && usersOnMapCount?.friends_count ? usersOnMapCount.friends_count : null
+        }
         isConnected={isConnected}
+        isPremium={isPremium}
       />
       <EditModal
         isVisible={isEditSlowModalVisible}

+ 3 - 2
src/screens/InAppScreens/TravelsScreen/CreateSharedTrip/index.tsx

@@ -55,6 +55,7 @@ const EventSchema = yup.object({
 const CreateSharedTripScreen = ({ route }: { route: any }) => {
   const eventId = route.params?.eventId;
   const eventData = route.params?.event;
+  const full = route.params?.full;
   const token = (storage.get('token', StoreType.STRING) as string) ?? null;
   const navigation = useNavigation();
   const scrollRef = useRef<ScrollView>(null);
@@ -580,11 +581,11 @@ const CreateSharedTripScreen = ({ route }: { route: any }) => {
                         }}
                         textStyles={{ color: Colors.DARK_BLUE }}
                         onPress={async () => {
-                          await setFull({ token, id: eventId, full: 1 });
+                          await setFull({ token, id: eventId, full: full ? 0 : 1 });
                           navigation.goBack();
                         }}
                       >
-                        Set Full
+                        {full ? 'Set not full' : 'Set full'}
                       </Button>
 
                       <Button

+ 2 - 1
src/screens/InAppScreens/TravelsScreen/EventScreen/index.tsx

@@ -1522,7 +1522,8 @@ const EventScreen = ({ route }: { route: any }) => {
                                 NAVIGATION_PAGES.CREATE_SHARED_TRIP,
                                 {
                                   eventId: event.id,
-                                  event: res
+                                  event: res,
+                                  full: event.full
                                 }
                               ] as never)
                             )

+ 6 - 2
src/types/api.ts

@@ -213,7 +213,9 @@ export enum API_ENDPOINT {
   ADD_SHARED_TRIP = 'add-shared-trip',
   UPDATE_SHARED_TRIP = 'update-shared-trip',
   SHARED_TRIP_ADD_PARTICIPANT = 'add-participant',
-  SET_FULL = 'set-event-full'
+  SET_FULL = 'set-event-full',
+  GET_COUNTRIES_FOR_LOCATION_DATA = 'get-list-of-countries-for-location-data',
+  GET_USERS_LOCATION_FILTERED = 'get-users-location-filtered'
 }
 
 export enum API {
@@ -399,7 +401,9 @@ export enum API {
   ADD_SHARED_TRIP = `${API_ROUTE.EVENTS}/${API_ENDPOINT.ADD_SHARED_TRIP}`,
   UPDATE_SHARED_TRIP = `${API_ROUTE.EVENTS}/${API_ENDPOINT.UPDATE_SHARED_TRIP}`,
   SHARED_TRIP_ADD_PARTICIPANT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.SHARED_TRIP_ADD_PARTICIPANT}`,
-  SET_FULL = `${API_ROUTE.EVENTS}/${API_ENDPOINT.SET_FULL}`
+  SET_FULL = `${API_ROUTE.EVENTS}/${API_ENDPOINT.SET_FULL}`,
+  GET_COUNTRIES_FOR_LOCATION_DATA = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_COUNTRIES_FOR_LOCATION_DATA}`,
+  GET_USERS_LOCATION_FILTERED = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_USERS_LOCATION_FILTERED}`
 }
 
 export type BaseAxiosError = AxiosError;