Browse Source

friendship functionality

Viktoriia 11 months ago
parent
commit
5cd1565c15
26 changed files with 1127 additions and 54 deletions
  1. 11 0
      Route.tsx
  2. 2 0
      assets/icons/friends.svg
  3. 1 1
      assets/icons/user-plus.svg
  4. 32 1
      src/components/WarningModal/index.tsx
  5. 79 0
      src/modules/api/friends/friends-api.ts
  6. 7 0
      src/modules/api/friends/friends-query-keys.tsx
  7. 3 0
      src/modules/api/friends/index.ts
  8. 5 0
      src/modules/api/friends/queries/index.ts
  9. 26 0
      src/modules/api/friends/queries/use-post-hideShowRequest.tsx
  10. 4 4
      src/modules/api/friends/queries/use-post-load-friends-app.tsx
  11. 28 0
      src/modules/api/friends/queries/use-post-load-friends-settings.tsx
  12. 17 0
      src/modules/api/friends/queries/use-post-send-friend-request.tsx
  13. 26 0
      src/modules/api/friends/queries/use-post-update-friend-status.tsx
  14. 0 1
      src/modules/api/user/queries/index.ts
  15. 3 23
      src/modules/api/user/user-api.tsx
  16. 1 2
      src/modules/api/user/user-query-keys.tsx
  17. 3 3
      src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx
  18. 164 0
      src/screens/InAppScreens/ProfileScreen/Components/FriendStatus.tsx
  19. 89 14
      src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx
  20. 20 1
      src/screens/InAppScreens/ProfileScreen/Components/styles.tsx
  21. 257 0
      src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/FriendsProfile/index.tsx
  22. 322 0
      src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/index.tsx
  23. 9 2
      src/screens/InAppScreens/ProfileScreen/index.tsx
  24. 7 0
      src/screens/InAppScreens/TravellersScreen/index.tsx
  25. 10 2
      src/types/api.ts
  26. 1 0
      src/types/navigation.ts

+ 11 - 0
Route.tsx

@@ -74,6 +74,7 @@ import RegionViewScreen from 'src/screens/InAppScreens/MapScreen/RegionViewScree
 import { enableScreens } from 'react-native-screens';
 import UsersListScreen from 'src/screens/InAppScreens/MapScreen/UsersListScreen';
 import SuggestSeriesScreen from 'src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen';
+import MyFriendsScreen from 'src/screens/InAppScreens/ProfileScreen/MyFriendsScreen';
 
 enableScreens();
 
@@ -301,6 +302,11 @@ const Route = () => {
                     component={UsersListScreen}
                     options={regionViewScreenOptions}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.MY_FRIENDS}
+                    component={MyFriendsScreen}
+                    options={regionViewScreenOptions}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>
@@ -396,6 +402,11 @@ const Route = () => {
                     name={NAVIGATION_PAGES.USERS_MAP}
                     component={UsersMapScreen}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.MY_FRIENDS}
+                    component={MyFriendsScreen}
+                    options={regionViewScreenOptions}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

File diff suppressed because it is too large
+ 2 - 0
assets/icons/friends.svg


+ 1 - 1
assets/icons/user-plus.svg

@@ -1,6 +1,6 @@
 <svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
 <g clip-path="url(#clip0_648_3970)">
-<path d="M2.4 3.2C2.4 2.35131 2.73714 1.53737 3.33726 0.937258C3.93737 0.337142 4.75131 0 5.6 0C6.44869 0 7.26263 0.337142 7.86274 0.937258C8.46286 1.53737 8.8 2.35131 8.8 3.2C8.8 4.04869 8.46286 4.86263 7.86274 5.46274C7.26263 6.06286 6.44869 6.4 5.6 6.4C4.75131 6.4 3.93737 6.06286 3.33726 5.46274C2.73714 4.86263 2.4 4.04869 2.4 3.2ZM0 12.0575C0 9.595 1.995 7.6 4.4575 7.6H6.7425C9.205 7.6 11.2 9.595 11.2 12.0575C11.2 12.4675 10.8675 12.8 10.4575 12.8H0.7425C0.3325 12.8 0 12.4675 0 12.0575ZM12.6 7.8V6.2H11C10.6675 6.2 10.4 5.9325 10.4 5.6C10.4 5.2675 10.6675 5 11 5H12.6V3.4C12.6 3.0675 12.8675 2.8 13.2 2.8C13.5325 2.8 13.8 3.0675 13.8 3.4V5H15.4C15.7325 5 16 5.2675 16 5.6C16 5.9325 15.7325 6.2 15.4 6.2H13.8V7.8C13.8 8.1325 13.5325 8.4 13.2 8.4C12.8675 8.4 12.6 8.1325 12.6 7.8Z" fill="#0F3F4F"/>
+<path d="M2.4 3.2C2.4 2.35131 2.73714 1.53737 3.33726 0.937258C3.93737 0.337142 4.75131 0 5.6 0C6.44869 0 7.26263 0.337142 7.86274 0.937258C8.46286 1.53737 8.8 2.35131 8.8 3.2C8.8 4.04869 8.46286 4.86263 7.86274 5.46274C7.26263 6.06286 6.44869 6.4 5.6 6.4C4.75131 6.4 3.93737 6.06286 3.33726 5.46274C2.73714 4.86263 2.4 4.04869 2.4 3.2ZM0 12.0575C0 9.595 1.995 7.6 4.4575 7.6H6.7425C9.205 7.6 11.2 9.595 11.2 12.0575C11.2 12.4675 10.8675 12.8 10.4575 12.8H0.7425C0.3325 12.8 0 12.4675 0 12.0575ZM12.6 7.8V6.2H11C10.6675 6.2 10.4 5.9325 10.4 5.6C10.4 5.2675 10.6675 5 11 5H12.6V3.4C12.6 3.0675 12.8675 2.8 13.2 2.8C13.5325 2.8 13.8 3.0675 13.8 3.4V5H15.4C15.7325 5 16 5.2675 16 5.6C16 5.9325 15.7325 6.2 15.4 6.2H13.8V7.8C13.8 8.1325 13.5325 8.4 13.2 8.4C12.8675 8.4 12.6 8.1325 12.6 7.8Z"/>
 </g>
 <defs>
 <clipPath id="clip0_648_3970">

+ 32 - 1
src/components/WarningModal/index.tsx

@@ -10,6 +10,7 @@ import { useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { ButtonVariants } from 'src/types/components';
 import { Button } from '../Button';
+import AddFriendSvg from '../../../assets/icons/user-plus.svg';
 
 export const WarningModal = ({
   isVisible,
@@ -132,6 +133,30 @@ export const WarningModal = ({
           borderColor: Colors.DARK_BLUE
         }
       ]
+    },
+    friend: {
+      message,
+      buttons: [
+        {
+          text: 'No',
+          textColor: Colors.DARK_BLUE,
+          color: Colors.WHITE,
+          action: () => {
+            onClose();
+          },
+          borderColor: Colors.DARK_BLUE
+        },
+        {
+          text: 'Yes',
+          textColor: Colors.WHITE,
+          color: Colors.DARK_BLUE,
+          action: () => {
+            onClose();
+            action && action();
+          },
+          borderColor: Colors.DARK_BLUE
+        }
+      ]
     }
   };
 
@@ -147,7 +172,13 @@ export const WarningModal = ({
             </TouchableOpacity>
           </View>
           <View style={styles.modalContent}>
-            <Text style={styles.modalTitle}>{title ?? 'Oops!'}</Text>
+            {type === 'friend' ? (
+              <View style={{ marginBottom: 16 }}>
+                <AddFriendSvg fill={Colors.ORANGE} width={40} height={40} />
+              </View>
+            ) : (
+              <Text style={styles.modalTitle}>{title ?? 'Oops!'}</Text>
+            )}
             <Text style={styles.modalText}>{modalContent.message}</Text>
             <View style={styles.buttonContainer}>
               {modalContent.buttons.map(

+ 79 - 0
src/modules/api/friends/friends-api.ts

@@ -0,0 +1,79 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+import { User } from '@api/regions';
+
+type FriendRequest = {
+  avatar: string | null;
+  first_name: string;
+  flag: string | null;
+  id: number;
+  last_name: string;
+  status: number;
+  user_id: number;
+};
+
+type UserData = {
+  max_pages: number;
+  countries: {
+    [key: string]: {
+      country: string;
+      flag: string;
+    };
+  };
+  users: User & { friend_db_id: number; friend_status: number }[];
+};
+
+export interface PostGetFriendsDataReturn extends ResponseType {
+  data: UserData;
+}
+
+export interface PostGetFriendsSettingsDataReturn extends ResponseType {
+  friends: UserData;
+  sent: UserData;
+  received: UserData;
+}
+
+export const friendsApi = {
+  getFriends: (uid: number, page: number, sort?: string, age?: number, country?: string) =>
+    request.postForm<PostGetFriendsDataReturn>(API.GET_FRIENDS, {
+      uid,
+      page,
+      sort,
+      age,
+      country
+    }),
+  sendFriendRequest: (token: string, uid: number) =>
+    request.postForm<ResponseType>(API.SEND_FRIEND_REQUEST, {
+      token,
+      uid
+    }),
+  loadFriendsSettings: (
+    token: string,
+    type: string,
+    page: number,
+    sort?: string,
+    age?: number,
+    country?: string
+  ) =>
+    request.postForm<PostGetFriendsSettingsDataReturn>(API.LOAD_FRIENDS_SETTINGS, {
+      token,
+      type,
+      page,
+      sort,
+      age,
+      country
+    }),
+  updateFriendStatus: (token: string, id: number, status: number) =>
+    request.postForm<ResponseType>(API.UPDATE_FRIEND_STATUS, {
+      token,
+      id,
+      status
+    }),
+  hideShowRequest: (token: string, id: number, show: 0 | 1) =>
+    request.postForm<ResponseType>(API.HIDE_SHOW_REQUEST, {
+      token,
+      id,
+      show
+    })
+};

+ 7 - 0
src/modules/api/friends/friends-query-keys.tsx

@@ -0,0 +1,7 @@
+export const friendsQueryKeys = {
+  getFriends: () => ['getFriends'] as const,
+  sendFriendRequest: () => ['sendFriendRequest'] as const,
+  loadFriendsSettings: () => ['loadFriendsSettings'] as const,
+  updateFriendStatus: () => ['updateFriendStatus'] as const,
+  hideShowRequest: () => ['hideShowRequest'] as const,
+};

+ 3 - 0
src/modules/api/friends/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './friends-api';
+export * from './friends-query-keys';

+ 5 - 0
src/modules/api/friends/queries/index.ts

@@ -0,0 +1,5 @@
+export * from './use-post-load-friends-app';
+export * from './use-post-send-friend-request';
+export * from './use-post-load-friends-settings';
+export * from './use-post-update-friend-status';
+export * from './use-post-hideShowRequest';

+ 26 - 0
src/modules/api/friends/queries/use-post-hideShowRequest.tsx

@@ -0,0 +1,26 @@
+import { friendsQueryKeys } from '../../friends/friends-query-keys';
+import { friendsApi } from '../../friends/friends-api';
+import { ResponseType } from '@api/response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const usePostHideShowRequestMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; id: number; show: 0 | 1 },
+    ResponseType
+  >({
+    mutationKey: friendsQueryKeys.updateFriendStatus(),
+    mutationFn: async (variables) => {
+      const response = await friendsApi.hideShowRequest(
+        variables.token,
+        variables.id,
+        variables.show
+      );
+      return response.data;
+    }
+  });
+};

+ 4 - 4
src/modules/api/user/queries/use-post-load-friends-app.tsx → src/modules/api/friends/queries/use-post-load-friends-app.tsx

@@ -1,5 +1,5 @@
-import { userQueryKeys } from '../user-query-keys';
-import { userApi, type PostGetFriendsDataReturn } from '../user-api';
+import { friendsQueryKeys } from '../../friends/friends-query-keys';
+import { friendsApi, type PostGetFriendsDataReturn } from '../../friends/friends-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
@@ -12,9 +12,9 @@ export const useGetFriendsMutation = () => {
     { id: number; page: number; sort?: string; age?: number; country?: string },
     PostGetFriendsDataReturn
   >({
-    mutationKey: userQueryKeys.getFriends(),
+    mutationKey: friendsQueryKeys.getFriends(),
     mutationFn: async (variables) => {
-      const response = await userApi.getFriends(
+      const response = await friendsApi.getFriends(
         variables.id,
         variables.page,
         variables.sort,

+ 28 - 0
src/modules/api/friends/queries/use-post-load-friends-settings.tsx

@@ -0,0 +1,28 @@
+import { friendsQueryKeys } from '../friends-query-keys';
+import { friendsApi, type PostGetFriendsSettingsDataReturn } from '../friends-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const useGetMyFriendsMutation = () => {
+  return useMutation<
+    PostGetFriendsSettingsDataReturn,
+    BaseAxiosError,
+    { token: string; type: string; page: number; sort?: string; age?: number; country?: string },
+    PostGetFriendsSettingsDataReturn
+  >({
+    mutationKey: friendsQueryKeys.loadFriendsSettings(),
+    mutationFn: async (variables) => {
+      const response = await friendsApi.loadFriendsSettings(
+        variables.token,
+        variables.type,
+        variables.page,
+        variables.sort,
+        variables.age,
+        variables.country
+      );
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/friends/queries/use-post-send-friend-request.tsx

@@ -0,0 +1,17 @@
+import { friendsQueryKeys } from '../../friends/friends-query-keys';
+import { friendsApi } from '../../friends/friends-api';
+import { ResponseType } from '@api/response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const usePostFriendRequestMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, { token: string; uid: number }, ResponseType>({
+    mutationKey: friendsQueryKeys.sendFriendRequest(),
+    mutationFn: async (variables) => {
+      const response = await friendsApi.sendFriendRequest(variables.token, variables.uid);
+      return response.data;
+    }
+  });
+};

+ 26 - 0
src/modules/api/friends/queries/use-post-update-friend-status.tsx

@@ -0,0 +1,26 @@
+import { friendsQueryKeys } from '../../friends/friends-query-keys';
+import { friendsApi } from '../../friends/friends-api';
+import { ResponseType } from '@api/response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const usePostUpdateFriendStatusMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; id: number; status: number },
+    ResponseType
+  >({
+    mutationKey: friendsQueryKeys.updateFriendStatus(),
+    mutationFn: async (variables) => {
+      const response = await friendsApi.updateFriendStatus(
+        variables.token,
+        variables.id,
+        variables.status
+      );
+      return response.data;
+    }
+  });
+};

+ 0 - 1
src/modules/api/user/queries/index.ts

@@ -5,4 +5,3 @@ export * from './use-post-get-profile-info-public';
 export * from './use-post-get-profile-regions';
 export * from './use-post-get-profile-data';
 export * from './use-post-get-profile-updates';
-export * from './use-post-load-friends-app';

+ 3 - 23
src/modules/api/user/user-api.tsx

@@ -197,8 +197,9 @@ export interface PostGetProfileDataReturn extends ResponseType {
     can_authenticate: 0 | 1;
     can_see_updates: 0 | 1;
     can_send_friend_request: 0 | 1;
-    friend_request_received: number;
-    friend_request_sent: number;
+    friend_request_received: 0 | 1;
+    friend_request_sent: 0 | 1;
+    friend_db_id: number;
     friends: {
       avatar: string | null;
       user_id: number;
@@ -280,19 +281,6 @@ export interface PostGetProfileUpdatesReturn extends ResponseType {
   };
 }
 
-export interface PostGetFriendsDataReturn extends ResponseType {
-  data: {
-    max_pages: number;
-    countries: {
-      [key: string]: {
-        country: string;
-        flag: string;
-      };
-    };
-    users: User[];
-  };
-}
-
 export const userApi = {
   getProfileData: (token: string) =>
     request.postForm<PostGetProfileData>(API.GET_USER_SETTINGS_DATA, { token }),
@@ -324,13 +312,5 @@ export const userApi = {
     request.postForm<PostGetProfileUpdatesReturn>(API.GET_PROFILE_UPDATES, {
       token,
       profile_id
-    }),
-  getFriends: (uid: number, page: number, sort?: string, age?: number, country?: string) =>
-    request.postForm<PostGetFriendsDataReturn>(API.GET_FRIENDS, {
-      uid,
-      page,
-      sort,
-      age,
-      country
     })
 };

+ 1 - 2
src/modules/api/user/user-query-keys.tsx

@@ -5,6 +5,5 @@ export const userQueryKeys = {
   getProfileInfoPublic: () => ['getProfileInfoPublic'] as const,
   getProfileRegions: (uid: number, type: string) => ['getProfileRegions', uid, type] as const,
   getProfileInfoData: (userId: number) => ['getProfileInfoData', userId] as const,
-  getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const,
-  getFriends: () => ['getFriends'] as const
+  getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const
 };

+ 3 - 3
src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx

@@ -16,7 +16,7 @@ import { Profile } from './Profile';
 import { RankingDropdown } from '../../TravellersScreen/utils/types';
 import { dataRanking } from '../../TravellersScreen/utils';
 import { Ranking } from '../../TravellersScreen';
-import { useGetFriendsMutation } from '@api/user';
+import { useGetFriendsMutation } from '@api/friends';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -31,9 +31,9 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
   const { mutateAsync: getUsersWhoVisitedDare } = useGetUsersWhoVisitedDareMutation();
   const { mutateAsync: getFriends } = useGetFriendsMutation();
   const token = storage.get('token', StoreType.STRING);
-  const [users, setUsers] = useState<Ranking[]>([]);
+  const [users, setUsers] = useState<any[]>([]);
   const [loading, setLoading] = useState(true);
-  const [selectedUsers, setSelectedUsers] = useState<Ranking[]>([]);
+  const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
   const [filteredUsers, setFilteredUsers] = useState<Ranking[]>([]);
   const isFromHere = route.params?.isFromHere;
   const [isModalVisible, setModalVisible] = useState(false);

+ 164 - 0
src/screens/InAppScreens/ProfileScreen/Components/FriendStatus.tsx

@@ -0,0 +1,164 @@
+import React, { FC } from 'react';
+import { View, TouchableOpacity, Text } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+import { styles } from './styles';
+
+type FriendStatusProps = {
+  status: number | null;
+  data: {
+    firstName: string;
+    lastName: string;
+  };
+  setModalInfo: (info: any) => void;
+  handleSendFriendRequest: () => Promise<void>;
+  handleUpdateFriendStatus: (status: number) => Promise<void>;
+};
+
+const FriendStatus: FC<FriendStatusProps> = ({
+  status,
+  data,
+  setModalInfo,
+  handleSendFriendRequest,
+  handleUpdateFriendStatus
+}) => {
+  switch (status) {
+    case 4:
+      return (
+        <View>
+          <TouchableOpacity
+            style={[
+              styles.statusBtn,
+              {
+                paddingVertical: 11,
+                paddingHorizontal: 12,
+                backgroundColor: Colors.ORANGE
+              }
+            ]}
+            onPress={() =>
+              setModalInfo({
+                isVisible: true,
+                type: 'friend',
+                message: `Are you sure you want to send a friend request to ${data.firstName} ${data.lastName}?`,
+                action: handleSendFriendRequest,
+                title: ''
+              })
+            }
+          >
+            <Text style={[styles.statusText, { fontSize: getFontSize(15) }]}>Add friend</Text>
+          </TouchableOpacity>
+        </View>
+      );
+    case 1:
+      return (
+        <View style={styles.statusContainer}>
+          <Text style={styles.statusTitle}>Friend</Text>
+          <TouchableOpacity
+            style={[
+              styles.statusBtn,
+              {
+                paddingVertical: 8,
+                paddingHorizontal: 12,
+                backgroundColor: Colors.RED,
+                flex: 1
+              }
+            ]}
+            onPress={() =>
+              setModalInfo({
+                isVisible: true,
+                type: 'confirm',
+                message: `Are you sure you want to unfriend ${data.firstName} ${data.lastName}?`,
+                action: () => handleUpdateFriendStatus(-1),
+                title: ''
+              })
+            }
+          >
+            <Text style={styles.statusText}>Unfriend</Text>
+          </TouchableOpacity>
+        </View>
+      );
+    case 2:
+      return (
+        <View style={styles.statusContainer}>
+          <Text style={styles.statusTitle}>Friendship pending</Text>
+          <TouchableOpacity
+            style={[
+              styles.statusBtn,
+              {
+                paddingVertical: 11,
+                paddingHorizontal: 12,
+                backgroundColor: Colors.ORANGE,
+                flex: 1
+              }
+            ]}
+            onPress={() =>
+              setModalInfo({
+                isVisible: true,
+                type: 'confirm',
+                message: `Are you sure you want to accept the friend request from ${data.firstName} ${data.lastName}?`,
+                action: () => handleUpdateFriendStatus(1),
+                title: ''
+              })
+            }
+          >
+            <Text style={[styles.statusText, { fontSize: getFontSize(15) }]}>Approve</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity
+            style={[
+              styles.statusBtn,
+              {
+                paddingVertical: 11,
+                paddingHorizontal: 12,
+                backgroundColor: Colors.RED,
+                flex: 1
+              }
+            ]}
+            onPress={() =>
+              setModalInfo({
+                isVisible: true,
+                type: 'confirm',
+                message: `Are you sure you want to reject the friend request from ${data.firstName} ${data.lastName}?`,
+                action: () => handleUpdateFriendStatus(2),
+                title: ''
+              })
+            }
+          >
+            <Text style={[styles.statusText, { fontSize: getFontSize(15) }]}>Deny</Text>
+          </TouchableOpacity>
+        </View>
+      );
+    case 3:
+      return (
+        <View style={styles.statusContainer}>
+          <Text style={styles.statusTitle}>Friendship pending</Text>
+          <TouchableOpacity
+            style={[
+              styles.statusBtn,
+              {
+                paddingVertical: 8,
+                paddingHorizontal: 12,
+                backgroundColor: Colors.RED,
+                flex: 1
+              }
+            ]}
+            onPress={() =>
+              setModalInfo({
+                isVisible: true,
+                type: 'confirm',
+                message: `Are you sure you want to cancel the friend request sent to ${data.firstName} ${data.lastName}?`,
+                action: () => handleUpdateFriendStatus(4),
+                title: ''
+              })
+            }
+          >
+            <Text style={styles.statusText}>Cancel</Text>
+          </TouchableOpacity>
+        </View>
+      );
+    default:
+      return null;
+  }
+};
+
+export default FriendStatus;

+ 89 - 14
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -1,4 +1,4 @@
-import { FC, useState } from 'react';
+import { FC, useCallback, useEffect, useState } from 'react';
 import { TouchableOpacity, View, Text, Image, Dimensions } from 'react-native';
 import { Series, usePostGetProfileRegions } from '@api/user';
 import { NavigationProp } from '@react-navigation/native';
@@ -20,7 +20,9 @@ import { InfoItem } from './InfoItem';
 import { Colors } from 'src/theme';
 import { API_HOST } from 'src/constants';
 import { NAVIGATION_PAGES } from 'src/types';
-import { AvatarWithInitials } from 'src/components';
+import { AvatarWithInitials, WarningModal } from 'src/components';
+import { usePostFriendRequestMutation, usePostUpdateFriendStatusMutation } from '@api/friends';
+import FriendStatus from './FriendStatus';
 
 type PersonalInfoProps = {
   data: {
@@ -35,6 +37,12 @@ type PersonalInfoProps = {
       last_name: string;
       flag: string;
     }[];
+    firstName: string;
+    lastName: string;
+    friendRequestSent: 0 | 1;
+    friendRequestReceived: 0 | 1;
+    isFriend: 0 | 1;
+    friendDbId: number;
   };
   updates: {
     countries: number;
@@ -48,7 +56,8 @@ type PersonalInfoProps = {
   };
   userId: number;
   navigation: NavigationProp<any>;
-  isFriend: 0 | 1;
+  isPublicView: boolean;
+  token: string | null;
 };
 
 const AVATAR_SIZE = 28;
@@ -60,16 +69,39 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
   userId,
   updates,
   navigation,
-  isFriend
+  isPublicView,
+  token
 }) => {
   const [showMoreSeries, setShowMoreSeries] = useState(false);
   const [type, setType] = useState<string>('nm');
   const [isModalVisible, setIsModalVisible] = useState(false);
   const [toolTipVisible, setToolTipVisible] = useState<number | null>(null);
   const [tooltipUser, setTooltipUser] = useState<number | null>(null);
+  const { mutateAsync: sendFriendRequest } = usePostFriendRequestMutation();
+  const { mutateAsync: updateFriendStatus } = usePostUpdateFriendStatusMutation();
+  const [friendStatus, setFriendStatus] = useState<number | null>(null);
+  const [modalInfo, setModalInfo] = useState({
+    type: 'friend',
+    message: '',
+    isVisible: false,
+    action: () => {},
+    title: ''
+  });
 
   const { data: regions } = usePostGetProfileRegions(userId, type);
 
+  useEffect(() => {
+    if (data.isFriend === 1) {
+      setFriendStatus(1);
+    } else if (data.friendRequestReceived === 1) {
+      setFriendStatus(2);
+    } else if (data.friendRequestSent === 1) {
+      setFriendStatus(3);
+    } else {
+      setFriendStatus(4);
+    }
+  }, [data]);
+
   const scores = [
     { name: 'score_nm', score: 'NM' },
     { name: 'score_mqp', score: 'DARE' },
@@ -130,6 +162,28 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
     );
   };
 
+  const handleSendFriendRequest = useCallback(async () => {
+    await sendFriendRequest(
+      { token: token as string, uid: userId },
+      {
+        onSuccess: () => {
+          setFriendStatus(3);
+        },
+      }
+    );
+  }, [sendFriendRequest, token, userId]);
+  
+  const handleUpdateFriendStatus = useCallback(async (status: number) => {
+    await updateFriendStatus(
+      { token: token as string, id: data.friendDbId, status },
+      {
+        onSuccess: () => {
+          status === -1 || status === 2 ? setFriendStatus(4) : setFriendStatus(status);
+        },
+      }
+    );
+  }, [updateFriendStatus, token, data.friendDbId]);
+
   const screenWidth = Dimensions.get('window').width;
   const availableWidth = screenWidth * (1 - 2 * SCREEN_PADDING_PERCENT);
   const maxAvatars = Math.floor(availableWidth / (AVATAR_SIZE - AVATAR_MARGIN)) - 2;
@@ -159,6 +213,16 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
           })}
         </View>
 
+        {isPublicView && token ? (
+          <FriendStatus
+            status={friendStatus}
+            data={data}
+            setModalInfo={setModalInfo}
+            handleSendFriendRequest={handleSendFriendRequest}
+            handleUpdateFriendStatus={handleUpdateFriendStatus}
+          />
+        ) : null}
+
         {data.friends.length > 0 ? (
           <InfoItem inline={true} title={'FRIENDS'}>
             <View style={{ flexDirection: 'row', flex: 1 }}>
@@ -213,18 +277,20 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
               <TouchableOpacity
                 style={[styles.avatar, styles.userShowMore]}
                 onPress={() => {
-                  if (isFriend === 0) {
+                  if (friendStatus !== 1 && isPublicView) {
                     setTooltipUser(-1);
                   } else {
-                    navigation.navigate(
-                      ...([
-                        NAVIGATION_PAGES.FRIENDS_LIST,
-                        {
-                          id: userId,
-                          type: 'friends'
-                        }
-                      ] as never)
-                    );
+                    isPublicView
+                      ? navigation.navigate(
+                          ...([
+                            NAVIGATION_PAGES.FRIENDS_LIST,
+                            {
+                              id: userId,
+                              type: 'friends'
+                            }
+                          ] as never)
+                        )
+                      : navigation.navigate(NAVIGATION_PAGES.MY_FRIENDS);
                   }
                 }}
               >
@@ -377,6 +443,15 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
       >
         <RegionsRenderer type={type} regions={regions} setIsModalVisible={setIsModalVisible} />
       </Modal>
+
+      <WarningModal
+        type={modalInfo.type}
+        isVisible={modalInfo.isVisible}
+        message={modalInfo.message}
+        onClose={() => setModalInfo({ ...modalInfo, isVisible: false })}
+        title=""
+        onModalHide={modalInfo.action}
+      />
     </>
   );
 };

+ 20 - 1
src/screens/InAppScreens/ProfileScreen/Components/styles.tsx

@@ -89,5 +89,24 @@ export const styles = StyleSheet.create({
     flexDirection: 'column',
     gap: 5,
     alignItems: 'center'
-  }
+  },
+  statusBtn: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+    borderRadius: 20,
+    gap: 4
+  },
+  statusText: {
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700',
+    color: Colors.WHITE
+  },
+  statusTextSmall: {
+    fontSize: getFontSize(10),
+    fontFamily: 'redhat-600'
+  },
+  statusContainer: { flexDirection: 'row', gap: 8, alignItems: 'center' },
+  statusTitle: { flex: 2, color: Colors.DARK_BLUE, fontWeight: '600' },
+  statusTitleSmall: { fontSize: 11, color: Colors.TEXT_GRAY, fontWeight: '500' }
 });

+ 257 - 0
src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/FriendsProfile/index.tsx

@@ -0,0 +1,257 @@
+import React, { FC } from 'react';
+import { Text, TouchableOpacity, View } from 'react-native';
+import { Image } from 'expo-image';
+import { useNavigation } from '@react-navigation/native';
+
+import { AvatarWithInitials } from 'src/components';
+import { NAVIGATION_PAGES } from 'src/types';
+import { API_HOST } from 'src/constants';
+import { adaptiveStyle, Colors } from 'src/theme';
+import {
+  ProfileStyles,
+  ScoreStyles
+} from 'src/screens/InAppScreens/TravellersScreen/Components/styles';
+import { getFontSize } from 'src/utils';
+
+import TickIcon from 'assets/icons/tick.svg';
+import UNIcon from 'assets/icons/un_icon.svg';
+import NMIcon from 'assets/icons/nm_icon.svg';
+import { styles } from '../../Components/styles';
+
+type Props = {
+  avatar: string | null;
+  first_name: string;
+  last_name: string;
+  date_of_birth: number;
+  homebase_flag: string;
+  homebase2_flag: string | null;
+  index: number;
+  score: any[];
+  active_score: number;
+  tbt_score?: number;
+  tbt_rank?: number;
+  badge_tbt?: number;
+  badge_1281: number;
+  badge_un: number;
+  auth: number;
+  userId: number;
+  type: 'friends' | 'sent' | 'received';
+  updateFriendStatus: (status: number, id: number) => void;
+  hideRequest: (id: number) => void;
+  friendDbId: number;
+  status: number;
+};
+
+export const FriendsProfile: FC<Props> = ({
+  avatar,
+  first_name,
+  last_name,
+  date_of_birth,
+  homebase_flag,
+  homebase2_flag,
+  score,
+  active_score,
+  badge_1281,
+  badge_un,
+  auth,
+  userId,
+  type,
+  updateFriendStatus,
+  hideRequest,
+  friendDbId,
+  status
+}) => {
+  const navigation = useNavigation();
+
+  const scoreNames = ['NM', 'DARE', 'UN', 'UN+', 'TCC', 'DEEP', 'YES', 'SLOW', 'WHS', 'KYE', 'TBT'];
+
+  const handlePress = () => {
+    navigation.navigate(...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId }] as never));
+  };
+
+  const renderStatus = () => {
+    switch (type) {
+      case 'sent':
+        return (
+          <View style={{ alignItems: 'center', flexDirection: 'row', gap: 12, marginTop: 4 }}>
+            <Text style={styles.statusTitleSmall}>
+              {status === 4 ? 'Canceled' : status === 2 ? 'Denied' : 'Pending'}
+            </Text>
+            <TouchableOpacity
+              style={[
+                styles.statusBtn,
+                {
+                  borderWidth: 1,
+                  borderColor: status === 3 ? Colors.RED : Colors.TEXT_GRAY,
+                  paddingVertical: 6,
+                  paddingHorizontal: 10
+                }
+              ]}
+              onPress={() =>
+                status === 3 ? updateFriendStatus(4, friendDbId) : hideRequest(friendDbId)
+              }
+            >
+              <Text
+                style={{
+                  fontSize: getFontSize(10),
+                  fontFamily: 'redhat-600',
+                  color: status === 3 ? Colors.RED : Colors.TEXT_GRAY
+                }}
+              >
+                {status === 3 ? 'Cancel' : 'Hide'}
+              </Text>
+            </TouchableOpacity>
+          </View>
+        );
+      case 'received':
+        return (
+          <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginTop: 4 }}>
+            <Text style={styles.statusTitleSmall}>{status === 3 ? 'Pending' : 'Denied'}</Text>
+            {status === 3 ? (
+              <>
+                <TouchableOpacity
+                  style={[
+                    styles.statusBtn,
+                    {
+                      borderWidth: 1,
+                      borderColor: Colors.ORANGE,
+                      paddingVertical: 6,
+                      paddingHorizontal: 10
+                    }
+                  ]}
+                  onPress={() => updateFriendStatus(1, friendDbId)}
+                >
+                  <Text
+                    style={[
+                      styles.statusTextSmall,
+                      {
+                        color: Colors.ORANGE
+                      }
+                    ]}
+                  >
+                    Approve
+                  </Text>
+                </TouchableOpacity>
+                <TouchableOpacity
+                  style={[
+                    styles.statusBtn,
+                    {
+                      borderWidth: 1,
+                      borderColor: Colors.RED,
+                      paddingVertical: 6,
+                      paddingHorizontal: 10
+                    }
+                  ]}
+                  onPress={() => updateFriendStatus(2, friendDbId)}
+                >
+                  <Text
+                    style={[
+                      styles.statusTextSmall,
+                      {
+                        color: Colors.RED
+                      }
+                    ]}
+                  >
+                    Deny
+                  </Text>
+                </TouchableOpacity>
+              </>
+            ) : (
+              <TouchableOpacity
+                style={[
+                  styles.statusBtn,
+                  {
+                    borderWidth: 1,
+                    borderColor: Colors.TEXT_GRAY,
+                    paddingVertical: 6,
+                    paddingHorizontal: 10
+                  }
+                ]}
+                onPress={() => hideRequest(friendDbId)}
+              >
+                <Text
+                  style={[
+                    styles.statusTextSmall,
+                    {
+                      color: Colors.TEXT_GRAY
+                    }
+                  ]}
+                >
+                  Hide
+                </Text>
+              </TouchableOpacity>
+            )}
+          </View>
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <View style={ProfileStyles.wrapper}>
+      <TouchableOpacity
+        onPress={() => handlePress()}
+        style={{ flex: 1, paddingHorizontal: 8, paddingVertical: 3 }}
+      >
+        <View style={adaptiveStyle(ProfileStyles.profileRoot, {})}>
+          {avatar ? (
+            <Image
+              style={adaptiveStyle(ProfileStyles.profileAvatar, {})}
+              source={{ uri: API_HOST + avatar }}
+            />
+          ) : homebase_flag ? (
+            <AvatarWithInitials
+              text={`${first_name[0] ?? ''}${last_name[0] ?? ''}`}
+              flag={API_HOST + homebase_flag}
+              size={48}
+            />
+          ) : null}
+          <View style={adaptiveStyle(ProfileStyles.profileDataRoot, {})}>
+            <Text
+              style={adaptiveStyle(
+                [ProfileStyles.profileFirstLastName, { fontSize: getFontSize(14), flex: 0 }],
+                {}
+              )}
+            >
+              {first_name ?? ''} {last_name ?? ''}
+            </Text>
+            <View style={adaptiveStyle(ProfileStyles.profileDataContainer, {})}>
+              <View style={adaptiveStyle(ProfileStyles.profileDataWrapper, {})}>
+                <Text style={adaptiveStyle(ProfileStyles.profileAge, {})}>
+                  Age: {date_of_birth ?? ''}
+                </Text>
+                {homebase_flag && (
+                  <Image
+                    source={{ uri: API_HOST + homebase_flag }}
+                    style={adaptiveStyle(ProfileStyles.countryFlag, {})}
+                  />
+                )}
+                {homebase2_flag && homebase2_flag !== homebase_flag ? (
+                  <Image
+                    source={{ uri: API_HOST + homebase2_flag }}
+                    style={adaptiveStyle([ProfileStyles.countryFlag, { marginLeft: -15 }], {})}
+                  />
+                ) : null}
+                <View style={adaptiveStyle(ProfileStyles.badgesWrapper, {})}>
+                  {auth ? <TickIcon /> : null}
+                  {badge_un ? <UNIcon /> : null}
+                  {badge_1281 ? <NMIcon /> : null}
+                </View>
+              </View>
+            </View>
+            {renderStatus()}
+          </View>
+          <View style={{ alignItems: 'center' }}>
+            <Text style={adaptiveStyle(ScoreStyles.activeScoreRanking, {})}>
+              {score[active_score]}
+            </Text>
+            <Text style={adaptiveStyle(ScoreStyles.activeScoreName, {})}>
+              {scoreNames[active_score]}
+            </Text>
+          </View>
+        </View>
+      </TouchableOpacity>
+    </View>
+  );
+};

+ 322 - 0
src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/index.tsx

@@ -0,0 +1,322 @@
+import { NavigationProp, useFocusEffect } from '@react-navigation/native';
+import React, { FC, useCallback, useEffect, useState } from 'react';
+import { ActivityIndicator } from 'react-native';
+import { Header, HorizontalTabView, Loading, PageWrapper, WarningModal } from 'src/components';
+
+import { Colors } from 'src/theme';
+import { FlashList } from '@shopify/flash-list';
+import { StoreType, storage } from 'src/storage';
+import { FilterButton, FilterModal } from '../../TravellersScreen/Components/FilterModal';
+import { RankingDropdown } from '../../TravellersScreen/utils/types';
+import { dataRanking } from '../../TravellersScreen/utils';
+import {
+  useGetMyFriendsMutation,
+  usePostHideShowRequestMutation,
+  usePostUpdateFriendStatusMutation
+} from '@api/friends';
+import { FriendsProfile } from './FriendsProfile';
+
+type Props = {
+  navigation: NavigationProp<any>;
+  route: any;
+};
+
+type Routes = {
+  key: 'friends' | 'received' | 'sent';
+  title: string;
+};
+
+const MyFriendsScreen: FC<Props> = ({ navigation }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+
+  const { mutateAsync: getMyFriends } = useGetMyFriendsMutation();
+  const [loading, setLoading] = useState(true);
+  const [isModalVisible, setModalVisible] = useState(false);
+  const [confirmedValueRanking, setConfirmedValueRanking] = useState<RankingDropdown | null>();
+  const [masterCountries, setMasterCountries] = useState<any>([]);
+  const [maxPages, setMaxPages] = useState(1);
+  const [page, setPage] = useState(0);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+  const [filter, setFilter] = useState<{
+    age: number | undefined;
+    ranking: string | undefined;
+    country: string | undefined;
+  }>({ age: undefined, ranking: undefined, country: undefined });
+  const [index, setIndex] = useState(0);
+  const routes: Routes[] = [
+    { key: 'friends', title: 'Friends' },
+    { key: 'received', title: 'Requests received' },
+    { key: 'sent', title: 'Requests sent' }
+  ];
+  const [users, setUsers] = useState<{ [key in 'friends' | 'received' | 'sent']: any[] }>({
+    friends: [],
+    received: [],
+    sent: []
+  });
+  const { mutateAsync: updateFriendStatus } = usePostUpdateFriendStatusMutation();
+  const { mutateAsync: hideShowRequest } = usePostHideShowRequestMutation();
+  const [modalInfo, setModalInfo] = useState({
+    type: 'friend',
+    message: '',
+    isVisible: false,
+    action: () => {},
+    title: ''
+  });
+
+  useEffect(() => {
+    if (filter.age || filter.ranking || filter.country) {
+      applySort();
+    }
+  }, [filter]);
+
+  useEffect(() => {
+    fetchUsers();
+    setFilter({
+      age: undefined,
+      ranking: undefined,
+      country: undefined
+    });
+  }, [index]);
+
+  useFocusEffect(
+    useCallback(() => {
+      fetchUsers();
+    }, [])
+  );
+
+  useEffect(() => {
+    const getNextPage = async () => {
+      await getMyFriends(
+        {
+          token,
+          type: routes[index].key,
+          page,
+          sort: filter.ranking,
+          age: filter.age,
+          country: filter.country
+        },
+        {
+          onSuccess: (data) => {
+            setIsLoadingMore(false);
+            setUsers((prevState) => {
+              return {
+                ...prevState,
+                [routes[index].key]: [
+                  ...prevState[routes[index].key],
+                  ...data[routes[index].key].users
+                ]
+              };
+            });
+          }
+        }
+      );
+    };
+    if (page !== 0) {
+      getNextPage();
+    }
+  }, [page]);
+
+  const fetchUsers = async () => {
+    await getMyFriends(
+      {
+        token,
+        type: '',
+        page: 0,
+        sort: undefined,
+        age: undefined,
+        country: undefined
+      },
+      {
+        onSuccess: (data) => {
+          setUsers({
+            friends: data.friends.users,
+            received: data.received.users,
+            sent: data.sent.users
+          });
+          setMaxPages(data[routes[index].key].max_pages);
+          setMasterCountries(convertData(data[routes[index].key].countries) ?? []);
+          setLoading(false);
+        }
+      }
+    );
+  };
+
+  const applySort = async () => {
+    await getMyFriends(
+      {
+        token,
+        type: routes[index].key,
+        page,
+        sort: filter.ranking,
+        age: filter.age,
+        country: filter.country
+      },
+      {
+        onSuccess: (data) => {
+          setUsers((prevState) => {
+            return {
+              ...prevState,
+              [routes[index].key]: data[routes[index].key].users
+            };
+          });
+          setMaxPages(data[routes[index].key].max_pages);
+          setMasterCountries(convertData(data[routes[index].key].countries) ?? []);
+          setLoading(false);
+        }
+      }
+    );
+  };
+
+  const convertData = (data: { [key: string]: { country: string; flag: string } }) => {
+    let formatedCountries = [{ two: 'all', name: 'ALL', flag: '' }];
+    formatedCountries.push(
+      ...Object.entries(data).map(([key, value]) => ({
+        two: key,
+        name: value.country,
+        flag: value.flag
+      }))
+    );
+
+    return formatedCountries;
+  };
+
+  const handleEndReached = useCallback(() => {
+    if (users && page < maxPages && !isLoadingMore && maxPages > 1) {
+      setIsLoadingMore(true);
+      setPage((prevPage) => prevPage + 1);
+    }
+  }, [users, page]);
+
+  const handleUpdateFriendStatus = async (status: number, id: number) => {
+    await updateFriendStatus(
+      { token, id, status },
+      {
+        onSuccess: () => {
+          applySort();
+        }
+      }
+    );
+  };
+
+  const handleHideRequest = async (id: number) => {
+    await hideShowRequest(
+      { token, id, show: 0 },
+      {
+        onSuccess: () => {
+          applySort();
+        }
+      }
+    );
+  };
+
+  if (loading) return <Loading />;
+
+  const ListFooter = ({ isLoading }: { isLoading: boolean }) =>
+    isLoading ? <ActivityIndicator size="large" color={Colors.DARK_BLUE} /> : null;
+
+  return (
+    <PageWrapper>
+      <Header
+        label={'Friends'}
+        rightElement={<FilterButton onPress={() => setModalVisible(!isModalVisible)} />}
+      />
+      <HorizontalTabView
+        index={index}
+        setIndex={setIndex}
+        routes={routes}
+        renderScene={({ route }: { route: Routes }) => (
+          <>
+            <FlashList
+              viewabilityConfig={{
+                waitForInteraction: true,
+                itemVisiblePercentThreshold: 50,
+                minimumViewTime: 1000
+              }}
+              estimatedItemSize={50}
+              data={users[route.key]}
+              renderItem={({ item, index }) => (
+                <FriendsProfile
+                  userId={item.user_id}
+                  key={index}
+                  index={index}
+                  first_name={item.first_name}
+                  last_name={item.last_name}
+                  avatar={item.avatar}
+                  date_of_birth={item.age}
+                  homebase_flag={item.flag1}
+                  homebase2_flag={item.flag2}
+                  score={[
+                    item.score_nm,
+                    item.score_dare,
+                    item.score_un,
+                    item.score_unp,
+                    item.score_tcc,
+                    item.score_deep,
+                    item.score_yes,
+                    item.score_slow,
+                    item.score_whs,
+                    item.score_kye,
+                    item.score_tbt
+                  ]}
+                  active_score={
+                    confirmedValueRanking
+                      ? confirmedValueRanking.value - 1
+                      : dataRanking[0].value - 1
+                  }
+                  tbt_score={item.score_tbt}
+                  tbt_rank={item.rank_tbt}
+                  badge_tbt={item.badge_tbt}
+                  badge_1281={item.badge_1281}
+                  badge_un={item.badge_un}
+                  auth={item.auth}
+                  type={route.key}
+                  updateFriendStatus={handleUpdateFriendStatus}
+                  status={item.friend_status}
+                  friendDbId={item.friend_db_id}
+                  hideRequest={handleHideRequest}
+                />
+              )}
+              keyExtractor={(item) => item.user_id.toString()}
+              contentContainerStyle={{ paddingBottom: 52, paddingTop: 8 }}
+              showsVerticalScrollIndicator={false}
+              onEndReached={handleEndReached}
+              onEndReachedThreshold={0.2}
+              ListFooterComponent={<ListFooter isLoading={isLoadingMore} />}
+            />
+          </>
+        )}
+      />
+
+      <FilterModal
+        isModalVisible={isModalVisible}
+        setModalVisible={(value) => setModalVisible(value)}
+        applyFilter={(filterAge, filterRanking, filterCountry) => {
+          setConfirmedValueRanking(filterRanking);
+          setPage(0);
+          setFilter({
+            age: filterAge?.value ? +filterAge?.value : undefined,
+            ranking: filterRanking?.name,
+            country:
+              filterCountry?.two && filterCountry?.two !== 'all' ? filterCountry?.two : undefined
+          });
+          if (!filterAge && !filterRanking && !filterCountry) {
+            fetchUsers();
+          }
+          setModalVisible(false);
+        }}
+        countriesData={masterCountries}
+      />
+      <WarningModal
+        type={modalInfo.type}
+        isVisible={modalInfo.isVisible}
+        message={modalInfo.message}
+        action={modalInfo.action}
+        onClose={() => setModalInfo({ ...modalInfo, isVisible: false })}
+        title=""
+        onModalHide={() => setModalInfo({ ...modalInfo, isVisible: false })}
+      />
+    </PageWrapper>
+  );
+};
+
+export default MyFriendsScreen;

+ 9 - 2
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -258,12 +258,19 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
             scores: data.scores,
             homebase: data.user_data.homeregion,
             series: data.series,
-            friends: data.friends
+            friends: data.friends,
+            firstName: data.user_data.first_name,
+            lastName: data.user_data.last_name,
+            friendRequestSent: data.friend_request_sent,
+            friendRequestReceived: data.friend_request_received,
+            isFriend: data.is_friend,
+            friendDbId: data.friend_db_id
           }}
           updates={lastUpdates.data.updates}
           userId={isPublicView ? route.params?.userId : +currentUserId}
           navigation={navigation}
-          isFriend={data.is_friend}
+          isPublicView={isPublicView}
+          token={token ? token : null}
         />
       </ScrollView>
     </PageWrapper>

+ 7 - 0
src/screens/InAppScreens/TravellersScreen/index.tsx

@@ -5,6 +5,7 @@ import { useNavigation } from '@react-navigation/native';
 import { Colors } from '../../../theme';
 import { PageWrapper } from '../../../components';
 import { NAVIGATION_PAGES } from 'src/types';
+import { StoreType, storage } from 'src/storage';
 
 import UserGroupIcon from '../../../../assets/icons/user-group.svg';
 import CrownIcon from '../../../../assets/icons/crown.svg';
@@ -15,9 +16,11 @@ import MemoriamIcon from '../../../../assets/icons/monument.svg';
 import ClockIcon from '../../../../assets/icons/clock.svg';
 import TrophyIcon from '../../../../assets/icons/trophy.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
+import FriendsIcon from 'assets/icons/friends.svg';
 
 const TravellersScreen = () => {
   const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING);
 
   const buttons = [
     { label: 'Master Ranking', icon: UserGroupIcon, page: NAVIGATION_PAGES.MASTER_RANKING },
@@ -30,6 +33,10 @@ const TravellersScreen = () => {
     { label: 'Triumphs', icon: TrophyIcon, page: NAVIGATION_PAGES.TRIUMPHS }
   ];
 
+  if (token) {
+    buttons.push({ label: 'Friends', icon: FriendsIcon, page: NAVIGATION_PAGES.MY_FRIENDS });
+  }
+
   const renderItem = ({ item }: { item: { label: string; icon: any; page: string } }) => (
     <TouchableOpacity
       style={{ alignItems: 'center' }}

+ 10 - 2
src/types/api.ts

@@ -98,7 +98,11 @@ export enum API_ENDPOINT {
   SUBMIT_SUGGESTION = 'submit-suggestion',
   GET_PROGILE_DATA = 'get-profile-data',
   GET_PROFILE_UPDATES = 'get-profile-updates',
-  GET_FRIENDS = 'load-friends-app'
+  GET_FRIENDS = 'load-friends-app',
+  SEND_FRIEND_REQUEST = 'send-friend-request',
+  LOAD_FRIENDS_SETTINGS = 'load-friends-settings-app',
+  UPDATE_FRIEND_STATUS = 'update-friend-status',
+  HIDE_SHOW_REQUEST = 'hideShowRequest'
 }
 
 export enum API {
@@ -177,7 +181,11 @@ export enum API {
   SUBMIT_SUGGESTION = `${API_ROUTE.SERIES}/${API_ENDPOINT.SUBMIT_SUGGESTION}`,
   GET_PROGILE_DATA = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_PROGILE_DATA}`,
   GET_PROFILE_UPDATES = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_PROFILE_UPDATES}`,
-  GET_FRIENDS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.GET_FRIENDS}`
+  GET_FRIENDS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.GET_FRIENDS}`,
+  SEND_FRIEND_REQUEST = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.SEND_FRIEND_REQUEST}`,
+  LOAD_FRIENDS_SETTINGS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.LOAD_FRIENDS_SETTINGS}`,
+  UPDATE_FRIEND_STATUS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.UPDATE_FRIEND_STATUS}`,
+  HIDE_SHOW_REQUEST = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.HIDE_SHOW_REQUEST}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 1 - 0
src/types/navigation.ts

@@ -54,4 +54,5 @@ export enum NAVIGATION_PAGES {
   USERS_LIST = 'inAppUsersList',
   SUGGEST_SERIES = 'inAppSuggestSeries',
   FRIENDS_LIST = 'inAppFriendsList',
+  MY_FRIENDS = 'inAppMyFriends',
 }

Some files were not shown because too many files changed in this diff