소스 검색

friends functionality fixes

Viktoriia 11 달 전
부모
커밋
a45ee9a709

+ 6 - 3
App.tsx

@@ -11,13 +11,16 @@ import { ErrorProvider, useError } from 'src/contexts/ErrorContext';
 import { useEffect } from 'react';
 import { setupInterceptors } from 'src/utils/request';
 import { ErrorModal } from 'src/components';
+import { NotificationProvider } from 'src/contexts/NotificationContext';
 
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
-      <ErrorProvider>
-        <InnerApp />
-      </ErrorProvider>
+      <NotificationProvider>
+        <ErrorProvider>
+          <InnerApp />
+        </ErrorProvider>
+      </NotificationProvider>
     </QueryClientProvider>
   );
 };

+ 50 - 0
src/components/BlinkingDot/index.tsx

@@ -0,0 +1,50 @@
+import React, { useEffect } from 'react';
+import { DimensionValue } from 'react-native';
+import Animated, {
+  useSharedValue,
+  useAnimatedStyle,
+  withRepeat,
+  withTiming,
+  Easing
+} from 'react-native-reanimated';
+
+const BlinkingDot = ({
+  diameter = 8,
+  right = 0,
+  top = 0
+}: {
+  diameter?: number;
+  right?: DimensionValue;
+  top?: DimensionValue;
+}) => {
+  const opacity = useSharedValue(1);
+
+  useEffect(() => {
+    opacity.value = withRepeat(withTiming(0, { duration: 600, easing: Easing.linear }), -1, true);
+  }, []);
+
+  const animatedStyle = useAnimatedStyle(() => {
+    return {
+      opacity: opacity.value
+    };
+  });
+
+  return (
+    <Animated.View
+      style={[
+        {
+          width: diameter,
+          height: diameter,
+          borderRadius: diameter / 2,
+          backgroundColor: 'red',
+          position: 'absolute',
+          right,
+          top
+        },
+        animatedStyle
+      ]}
+    />
+  );
+};
+
+export default BlinkingDot;

+ 18 - 12
src/components/HorizontalTabView/index.tsx

@@ -6,6 +6,7 @@ import { styles } from './styles';
 import { Colors } from 'src/theme';
 
 import MarkToUpIcon from '../../../assets/icons/mark-to-up.svg';
+import BlinkingDot from '../BlinkingDot';
 
 export const HorizontalTabView = ({
   index,
@@ -14,7 +15,8 @@ export const HorizontalTabView = ({
   renderScene,
   withMark,
   onDoubleClick,
-  lazy = false
+  lazy = false,
+  withNotification = false
 }: {
   index: number;
   setIndex: React.Dispatch<React.SetStateAction<number>>;
@@ -23,22 +25,26 @@ export const HorizontalTabView = ({
   withMark?: boolean;
   onDoubleClick?: () => void;
   lazy?: boolean;
+  withNotification?: boolean;
 }) => {
   const renderTabBar = (props: any) => (
     <TabBar
       {...props}
       renderLabel={({ route, focused }) => (
-        <View style={[styles.tabLabelContainer, focused ? styles.tabLabelFocused : null]}>
-          <Text style={[styles.label, focused ? styles.labelFocused : null]}>{route.title}</Text>
-          {withMark ? (
-            <MarkToUpIcon
-              height={16}
-              width={16}
-              style={styles.icon}
-              stroke={focused ? Colors.WHITE : Colors.DARK_BLUE}
-            />
-          ) : null}
-        </View>
+        <>
+          <View style={[styles.tabLabelContainer, focused ? styles.tabLabelFocused : null]}>
+            <Text style={[styles.label, focused ? styles.labelFocused : null]}>{route.title}</Text>
+            {withMark ? (
+              <MarkToUpIcon
+                height={16}
+                width={16}
+                style={styles.icon}
+                stroke={focused ? Colors.WHITE : Colors.DARK_BLUE}
+              />
+            ) : null}
+          </View>
+          {withNotification && route.key === 'received' ? <BlinkingDot diameter={10} /> : null}
+        </>
       )}
       scrollEnabled={true}
       indicatorStyle={styles.indicator}

+ 10 - 2
src/components/TabBarButton/index.tsx

@@ -10,6 +10,8 @@ import ProfileIcon from '../../../assets/icons/bottom-navigation/profile.svg';
 
 import { Colors } from '../../theme';
 import { styles } from './style';
+import BlinkingDot from '../BlinkingDot';
+import { useNotification } from 'src/contexts/NotificationContext';
 
 const getTabIcon = (routeName: string) => {
   switch (routeName) {
@@ -36,14 +38,20 @@ const TabBarButton = ({
   focused: boolean;
 }) => {
   const IconComponent: React.FC<SvgProps> | null = getTabIcon(label);
+  const { isNotificationActive } = useNotification();
 
   let currentColor = focused ? Colors.DARK_BLUE : Colors.LIGHT_GRAY;
 
   return (
     <TouchableWithoutFeedback onPress={onPress}>
       <View style={styles.buttonStyle}>
-        {IconComponent && <IconComponent width={24} height={24} fill={currentColor} />}
-        <Text style={[styles.labelStyle, { color: currentColor }]}>{label}</Text>
+        <View style={{ alignItems: 'center' }}>
+          {IconComponent && <IconComponent width={24} height={24} fill={currentColor} />}
+          {label === NAVIGATION_PAGES.IN_APP_TRAVELLERS_TAB && isNotificationActive && (
+            <BlinkingDot diameter={8} />
+          )}
+          <Text style={[styles.labelStyle, { color: currentColor }]}>{label}</Text>
+        </View>
       </View>
     </TouchableWithoutFeedback>
   );

+ 1 - 1
src/components/TabBarButton/style.tsx

@@ -6,7 +6,7 @@ export const styles = StyleSheet.create({
     alignItems: 'center',
     justifyContent: 'center',
     overflow: 'hidden',
-    marginTop: Platform.OS === 'ios' ? 4 : 0,
+    paddingTop: Platform.OS === 'ios' ? 4 : 0,
   },
   labelStyle: {
     marginTop: 4,

+ 1 - 0
src/components/index.ts

@@ -18,3 +18,4 @@ export * from './HorizontalTabView';
 export * from './EditNmModal';
 export * from './MenuDrawer';
 export * from './ErrorModal';
+export * from './BlinkingDot';

+ 46 - 0
src/contexts/NotificationContext.tsx

@@ -0,0 +1,46 @@
+import React, { createContext, useState, useContext, useEffect, useCallback } from 'react';
+import { fetchFriendsNotification } from '@api/friends';
+import { StoreType, storage } from 'src/storage';
+
+const NotificationContext = createContext({
+  isNotificationActive: false,
+  updateNotificationStatus: () => {}
+});
+
+export const NotificationProvider = ({ children }: { children: React.ReactNode }) => {
+  const [isNotificationActive, setIsNotificationActive] = useState(
+    storage.get('friendsNotification', StoreType.BOOLEAN) as boolean
+  );
+  const token = storage.get('token', StoreType.STRING) as string;
+
+  const updateNotificationStatus = useCallback(async () => {
+    try {
+      const data = await fetchFriendsNotification(token);
+      const isActive = data && data.active;
+      setIsNotificationActive(isActive as boolean);
+      storage.set('friendsNotification', isActive as boolean);
+    } catch (error) {
+      console.error('Failed to fetch notifications', error);
+    }
+  }, [token]);
+
+  useEffect(() => {
+    updateNotificationStatus();
+  }, [token]);
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      updateNotificationStatus();
+    }, 15000);
+
+    return () => clearInterval(interval);
+  }, [updateNotificationStatus]);
+
+  return (
+    <NotificationContext.Provider value={{ isNotificationActive, updateNotificationStatus }}>
+      {children}
+    </NotificationContext.Provider>
+  );
+};
+
+export const useNotification = () => useContext(NotificationContext);

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

@@ -34,6 +34,10 @@ export interface PostGetFriendsSettingsDataReturn extends ResponseType {
   received: UserData;
 }
 
+export interface PostGetFriendsNotificationReturn extends ResponseType {
+  active: boolean;
+}
+
 export const friendsApi = {
   getFriends: (uid: number, page: number, sort?: string, age?: number, country?: string) =>
     request.postForm<PostGetFriendsDataReturn>(API.GET_FRIENDS, {
@@ -75,5 +79,9 @@ export const friendsApi = {
       token,
       id,
       show
+    }),
+  getNotification: (token: string) =>
+    request.postForm<PostGetFriendsNotificationReturn>(API.GET_FRIENDS_NOTIFICATION, {
+      token
     })
 };

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

@@ -4,4 +4,5 @@ export const friendsQueryKeys = {
   loadFriendsSettings: () => ['loadFriendsSettings'] as const,
   updateFriendStatus: () => ['updateFriendStatus'] as const,
   hideShowRequest: () => ['hideShowRequest'] as const,
+  getNotification: (token: string) => ['getNotification', token ] as const
 };

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

@@ -3,3 +3,4 @@ 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';
+export * from './use-post-is-notification-active';

+ 18 - 0
src/modules/api/friends/queries/use-post-is-notification-active.tsx

@@ -0,0 +1,18 @@
+import { friendsQueryKeys } from '../friends-query-keys';
+import { type PostGetFriendsNotificationReturn, friendsApi } from '../friends-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchFriendsNotification = async (token: string) => {
+  try {
+    const data: PostGetFriendsNotificationReturn = await queryClient.fetchQuery({
+      queryKey: friendsQueryKeys.getNotification(token),
+      queryFn: () => friendsApi.getNotification(token).then((res) => res.data),
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch friends notification:', error);
+  }
+};

+ 31 - 17
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -43,6 +43,7 @@ type PersonalInfoProps = {
     friendRequestReceived: 0 | 1;
     isFriend: 0 | 1;
     friendDbId: number;
+    ownProfile: 0 | 1;
   };
   updates: {
     countries: number;
@@ -168,21 +169,24 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
       {
         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 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);
@@ -230,16 +234,26 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                 <Tooltip
                   isVisible={tooltipUser === index}
                   content={
-                    <Text style={{}}>
-                      {friend.first_name} {friend.last_name}
-                    </Text>
+                    <TouchableOpacity
+                      onPress={() =>
+                        navigation.navigate(
+                          ...([
+                            NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
+                            { userId: friend.user_id }
+                          ] as never)
+                        )
+                      }
+                    >
+                      <Text style={{}}>
+                        {friend.first_name} {friend.last_name}
+                      </Text>
+                    </TouchableOpacity>
                   }
                   contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
                   placement="top"
                   onClose={() => setTooltipUser(null)}
                   key={index}
                   backgroundColor="transparent"
-                  allowChildInteraction={false}
                 >
                   <TouchableOpacity
                     onPress={() => setTooltipUser(index)}
@@ -280,7 +294,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                   if (friendStatus !== 1 && isPublicView) {
                     setTooltipUser(-1);
                   } else {
-                    isPublicView
+                    data.ownProfile === 0
                       ? navigation.navigate(
                           ...([
                             NAVIGATION_PAGES.FRIENDS_LIST,

+ 7 - 3
src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/index.tsx

@@ -15,6 +15,7 @@ import {
   usePostUpdateFriendStatusMutation
 } from '@api/friends';
 import { FriendsProfile } from './FriendsProfile';
+import { useNotification } from 'src/contexts/NotificationContext';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -28,6 +29,7 @@ type Routes = {
 
 const MyFriendsScreen: FC<Props> = ({ navigation }) => {
   const token = storage.get('token', StoreType.STRING) as string;
+  const { isNotificationActive, updateNotificationStatus } = useNotification();
 
   const { mutateAsync: getMyFriends } = useGetMyFriendsMutation();
   const [loading, setLoading] = useState(true);
@@ -44,9 +46,9 @@ const MyFriendsScreen: FC<Props> = ({ navigation }) => {
   }>({ 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' }
+    { key: 'friends', title: 'My friends' },
+    { key: 'received', title: 'Received requests' },
+    { key: 'sent', title: 'Sent requests' }
   ];
   const [users, setUsers] = useState<{ [key in 'friends' | 'received' | 'sent']: any[] }>({
     friends: [],
@@ -189,6 +191,7 @@ const MyFriendsScreen: FC<Props> = ({ navigation }) => {
         }
       }
     );
+    updateNotificationStatus();
   };
 
   const handleHideRequest = async (id: number) => {
@@ -217,6 +220,7 @@ const MyFriendsScreen: FC<Props> = ({ navigation }) => {
         index={index}
         setIndex={setIndex}
         routes={routes}
+        withNotification={isNotificationActive}
         renderScene={({ route }: { route: Routes }) => (
           <>
             <FlashList

+ 55 - 13
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -1,6 +1,7 @@
-import React, { FC, useCallback, useEffect } from 'react';
+import React, { FC, useCallback, useEffect, useState } from 'react';
 import { Linking, ScrollView, Text, TouchableOpacity, View, Image, Platform } from 'react-native';
 import { CommonActions, NavigationProp, useFocusEffect } from '@react-navigation/native';
+import ReactModal from 'react-native-modal';
 
 import { usePostGetProfileInfoDataQuery, usePostGetProfileUpdatesQuery } from '@api/user';
 
@@ -61,8 +62,12 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
     true
   );
   const { mutateAsync: updateFriendStatus } = usePostUpdateFriendStatusMutation();
-  const [isModalVisible, setIsModalVisible] = React.useState(false);
   const [isFriend, setIsFriend] = React.useState<0 | 1>(0);
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+  const [modalState, setModalState] = useState({
+    isModalVisible: false,
+    isWarningVisible: false
+  });
 
   useFocusEffect(
     useCallback(() => {
@@ -102,6 +107,14 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
         );
   };
 
+  const closeModal = (modalName: string) => {
+    setModalState((prevState) => ({ ...prevState, [modalName]: false }));
+  };
+
+  const openModal = (modalName: string) => {
+    setModalState((prevState) => ({ ...prevState, [modalName]: true }));
+  };
+
   const TBRanking = () => {
     const colors = [
       'rgba(237, 147, 52, 1)',
@@ -199,6 +212,14 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
               />
             )}
             {data.scores.rank_tbt && data.scores.rank_tbt >= 1 ? <TBRanking /> : null}
+            {isFriend === 1 && token && data.own_profile === 0 ? (
+              <TouchableOpacity style={styles.friend} onPress={() => openModal('isModalVisible')}>
+                <Text style={styles.friendText}>Friend</Text>
+                <View style={{ transform: 'rotate(180deg)' }}>
+                  <ChevronIcon fill={Colors.WHITE} height={8} />
+                </View>
+              </TouchableOpacity>
+            ) : null}
           </View>
           <View style={{ gap: 5, flex: 1 }}>
             <View style={{ height: 34 }}></View>
@@ -206,14 +227,6 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
               <Text style={[styles.headerText, { fontSize: getFontSize(18) }]}>
                 {data.user_data.first_name} {data.user_data.last_name}
               </Text>
-              {isFriend === 1 && token && data.own_profile === 0 ? (
-                <TouchableOpacity style={styles.friend} onPress={() => setIsModalVisible(true)}>
-                  <Text style={styles.friendText}>Friend</Text>
-                  <View style={{ transform: 'rotate(180deg)' }}>
-                    <ChevronIcon fill={Colors.WHITE} height={8} />
-                  </View>
-                </TouchableOpacity>
-              ) : null}
             </View>
 
             <View style={styles.userInfoContainer}>
@@ -300,7 +313,8 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
             friendRequestSent: data.friend_request_sent,
             friendRequestReceived: data.friend_request_received,
             isFriend,
-            friendDbId: data.friend_db_id
+            friendDbId: data.friend_db_id,
+            ownProfile: data.own_profile
           }}
           updates={lastUpdates.data.updates}
           userId={isPublicView ? route.params?.userId : +currentUserId}
@@ -310,12 +324,40 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
         />
       </ScrollView>
 
+      <ReactModal
+        isVisible={modalState.isModalVisible}
+        onBackdropPress={() => closeModal('isModalVisible')}
+        style={styles.modal}
+        statusBarTranslucent={true}
+        presentationStyle="overFullScreen"
+        onModalHide={() => {
+          if (shouldOpenWarningModal) {
+            openModal('isWarningVisible');
+            setShouldOpenWarningModal(false);
+          }
+        }}
+      >
+        <View style={styles.wrapper}>
+          <TouchableOpacity
+            style={styles.btnModalEdit}
+            onPress={() => {
+              closeModal('isModalVisible');
+              setShouldOpenWarningModal(true);
+            }}
+          >
+            <Text style={styles.btnModalEditText}>Unfriend</Text>
+            <View style={{ transform: 'rotate(180deg)' }}>
+              <ChevronIcon fill={Colors.DARK_BLUE} height={11} />
+            </View>
+          </TouchableOpacity>
+        </View>
+      </ReactModal>
       <WarningModal
         type={'confirm'}
-        isVisible={isModalVisible}
+        isVisible={modalState.isWarningVisible}
         message={`Are you sure you want to unfriend ${data.user_data.first_name} ${data.user_data.last_name}?`}
         action={handleUpdateFriendStatus}
-        onClose={() => setIsModalVisible(false)}
+        onClose={() => closeModal('isWarningVisible')}
         title=""
       />
     </PageWrapper>

+ 26 - 3
src/screens/InAppScreens/ProfileScreen/styles.ts

@@ -76,8 +76,8 @@ export const styles = StyleSheet.create({
     borderRadius: 20,
     gap: 4,
     paddingVertical: 4,
-    paddingHorizontal: 10,
-    backgroundColor: Colors.ORANGE,
+    paddingHorizontal: 8,
+    backgroundColor: Colors.ORANGE
   },
   friendText: {
     fontSize: getFontSize(10),
@@ -88,5 +88,28 @@ export const styles = StyleSheet.create({
     flexDirection: 'row',
     justifyContent: 'space-between',
     alignItems: 'center'
-  }
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  wrapper: {
+    backgroundColor: Colors.WHITE,
+    paddingLeft: 15,
+    paddingRight: 15,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    height: 'auto',
+    paddingBottom: 36,
+    paddingTop: 24
+  },
+  btnModalEdit: {
+    paddingHorizontal: 16,
+    paddingVertical: 8,
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 12,
+    justifyContent: 'space-between'
+  },
+  btnModalEditText: { fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE }
 });

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

@@ -17,10 +17,13 @@ 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';
+import BlinkingDot from 'src/components/BlinkingDot';
+import { useNotification } from 'src/contexts/NotificationContext';
 
 const TravellersScreen = () => {
   const navigation = useNavigation();
   const token = storage.get('token', StoreType.STRING);
+  const { isNotificationActive } = useNotification();
 
   const buttons = [
     { label: 'Master Ranking', icon: UserGroupIcon, page: NAVIGATION_PAGES.MASTER_RANKING },
@@ -50,6 +53,9 @@ const TravellersScreen = () => {
         />
         <Text style={styles.label}>{item.label}</Text>
       </View>
+      {item.label === 'Friends' && isNotificationActive && (
+        <BlinkingDot diameter={10} right={'15%'} top={'15%'} />
+      )}
     </TouchableOpacity>
   );
 

+ 4 - 2
src/types/api.ts

@@ -102,7 +102,8 @@ export enum API_ENDPOINT {
   SEND_FRIEND_REQUEST = 'send-friend-request',
   LOAD_FRIENDS_SETTINGS = 'load-friends-settings-app',
   UPDATE_FRIEND_STATUS = 'update-friend-status',
-  HIDE_SHOW_REQUEST = 'hideShowRequest'
+  HIDE_SHOW_REQUEST = 'hideShowRequest',
+  GET_FRIENDS_NOTIFICATION = 'is-notification-active'
 }
 
 export enum API {
@@ -185,7 +186,8 @@ export enum API {
   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}`
+  HIDE_SHOW_REQUEST = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.HIDE_SHOW_REQUEST}`,
+  GET_FRIENDS_NOTIFICATION = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.GET_FRIENDS_NOTIFICATION}`
 }
 
 export type BaseAxiosError = AxiosError;