Browse Source

chats badge

Viktoriia 8 months ago
parent
commit
2b2b7b8bb2

+ 2 - 2
App.tsx

@@ -16,13 +16,13 @@ import { ErrorModal } from 'src/components';
 import { NotificationProvider } from 'src/contexts/NotificationContext';
 import React from 'react';
 
-const routingInstrumentation = new Sentry.ReactNavigationInstrumentation({
+const routingInstrumentation = Sentry.reactNavigationIntegration({
   enableTimeToInitialDisplay: true
 });
 
 Sentry.init({
   dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
-  integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })],
+  integrations: [Sentry.reactNativeTracingIntegration({ routingInstrumentation })],
   debug: false,
   ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded']
 });

+ 4 - 0
Route.tsx

@@ -93,6 +93,7 @@ import SystemNotificationsScreen from 'src/screens/NotificationsScreen/SystemNot
 import MessagesScreen from 'src/screens/InAppScreens/MessagesScreen';
 import ChatScreen from 'src/screens/InAppScreens/MessagesScreen/ChatScreen';
 import { Splash } from 'src/components/SplashSpinner';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 enableScreens();
 
@@ -118,6 +119,7 @@ const Route = () => {
   const [dbLoaded, setDbLoaded] = useState(false);
   const uid = storage.get('uid', StoreType.STRING);
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
   const checkNmToken = async () => {
     if (token && uid) {
@@ -144,6 +146,7 @@ const Route = () => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
   };
 
   const fetchAndSaveUserInfo = async () => {
@@ -165,6 +168,7 @@ const Route = () => {
       // await checkTokenAndUpdate();
       await openDatabases();
       setDbLoaded(true);
+      updateUnreadMessagesCount();
       await findFastestServer();
     };
 

+ 4 - 0
src/components/ErrorModal/index.tsx

@@ -13,11 +13,14 @@ import { CommonActions, useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { storage } from 'src/storage';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 export const ErrorModal = () => {
   const { error, hideError, navigateToLogin } = useError();
   const navigation = useNavigation();
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
 
   const handleClose = () => {
     if (navigateToLogin) {
@@ -27,6 +30,7 @@ export const ErrorModal = () => {
       storage.remove('visitedTilesUrl');
       storage.remove('filterSettings');
       updateNotificationStatus();
+      updateUnreadMessagesCount();
 
       navigation.dispatch(
         CommonActions.reset({

+ 3 - 0
src/components/MenuDrawer/index.tsx

@@ -19,6 +19,7 @@ import BellIcon from 'assets/icons/notifications/bell-solid.svg';
 
 import { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
@@ -31,6 +32,7 @@ export const MenuDrawer = (props: any) => {
     action: () => {}
   });
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
   const openModal = (type: string, message: string, action: any) => {
     setModalInfo({
@@ -52,6 +54,7 @@ export const MenuDrawer = (props: any) => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
     navigation.dispatch(
       CommonActions.reset({
         index: 1,

+ 45 - 0
src/components/MessagesDot/index.tsx

@@ -0,0 +1,45 @@
+import React from 'react';
+import { DimensionValue, Text, View } from 'react-native';
+import { Colors } from 'src/theme';
+
+const MessagesDot = ({
+  messagesCount,
+  right = 0,
+  top = -2
+}: {
+  messagesCount: number;
+  right?: DimensionValue;
+  top?: DimensionValue;
+}) => {
+  return (
+    <View
+      style={[
+        {
+          position: 'absolute',
+          right,
+          top
+        }
+      ]}
+    >
+      <View
+        style={{
+          paddingHorizontal: 4,
+          paddingVertical: 2,
+          backgroundColor: Colors.ORANGE,
+          borderRadius: 10,
+          minWidth: 18,
+          justifyContent: 'center',
+          alignItems: 'center',
+          borderWidth: 1,
+          borderColor: Colors.WHITE
+        }}
+      >
+        <Text style={{ fontFamily: 'montserrat-700', fontSize: 10, color: Colors.WHITE }}>
+          {messagesCount > 99 ? '99+' : messagesCount}
+        </Text>
+      </View>
+    </View>
+  );
+};
+
+export default MessagesDot;

+ 25 - 3
src/components/TabBarButton/index.tsx

@@ -13,6 +13,11 @@ import { Colors } from '../../theme';
 import { styles } from './style';
 import BlinkingDot from '../BlinkingDot';
 import { useNotification } from 'src/contexts/NotificationContext';
+import MessagesDot from '../MessagesDot';
+import { storage, StoreType } from 'src/storage';
+import { WarningModal } from '../WarningModal';
+import { useState } from 'react';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const getTabIcon = (routeName: string) => {
   switch (routeName) {
@@ -43,15 +48,24 @@ const TabBarButton = ({
   navigation: any;
 }) => {
   const IconComponent: React.FC<SvgProps> | null = getTabIcon(label);
+  const token = storage.get('token', StoreType.STRING);
   const { isNotificationActive } = useNotification();
+  const unreadMessagesCount = useMessagesStore((state) => state.unreadMessagesCount);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
 
   let currentColor = focused ? Colors.DARK_BLUE : Colors.LIGHT_GRAY;
 
   return (
     <TouchableWithoutFeedback
-      onPress={() =>
-        label === NAVIGATION_PAGES.MENU_DRAWER ? (navigation as any)?.openDrawer() : onPress()
-      }
+      onPress={() => {
+        if (label === NAVIGATION_PAGES.MENU_DRAWER) {
+          (navigation as any)?.openDrawer();
+        } else if (label === NAVIGATION_PAGES.IN_APP_MESSAGES_TAB && !token) {
+          setIsWarningModalVisible(true);
+        } else {
+          onPress();
+        }
+      }}
     >
       <View style={styles.buttonStyle}>
         <View style={{ alignItems: 'center' }}>
@@ -59,8 +73,16 @@ const TabBarButton = ({
           {label === NAVIGATION_PAGES.IN_APP_TRAVELLERS_TAB && isNotificationActive && (
             <BlinkingDot diameter={8} />
           )}
+          {label === NAVIGATION_PAGES.IN_APP_MESSAGES_TAB && unreadMessagesCount > 0 && (
+            <MessagesDot messagesCount={unreadMessagesCount} />
+          )}
           <Text style={[styles.labelStyle, { color: currentColor }]}>{label}</Text>
         </View>
+        <WarningModal
+          type={'unauthorized'}
+          isVisible={isWarningModalVisible}
+          onClose={() => setIsWarningModalVisible(false)}
+        />
       </View>
     </TouchableWithoutFeedback>
   );

+ 1 - 0
src/components/index.ts

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

+ 1 - 1
src/contexts/NotificationContext.tsx

@@ -19,7 +19,7 @@ export const NotificationProvider = ({ children }: { children: React.ReactNode }
       try {
         const data = await fetchFriendsNotification(token as string);
         const isActive = data && data.active;
-        
+
         if (typeof isActive === 'boolean') {
           setIsNotificationActive(isActive);
           storage.set('friendsNotification', isActive);

+ 3 - 5
src/contexts/PushNotificationContext.tsx

@@ -5,6 +5,7 @@ import { Linking, Platform } from 'react-native';
 import { CommonActions, useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { usePostSetSettingsMutation } from '@api/notifications';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const PushNotificationContext = createContext<{
   isSubscribed: boolean;
@@ -25,15 +26,12 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
   );
   const { mutateAsync: setNotificationsSettings } = usePostSetSettingsMutation();
   const navigation = useNavigation();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
   const lastNotificationResponse = Notifications.useLastNotificationResponse();
 
   useEffect(() => {
     if (lastNotificationResponse && Platform.OS === 'android') {
-      console.log(
-        'lastNotificationResponse',
-        lastNotificationResponse.notification.request.content.data
-      );
       const data = lastNotificationResponse.notification.request.content.data;
 
       if (data?.screen && data?.parentScreen) {
@@ -113,7 +111,7 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
       Notifications.setBadgeCountAsync(0);
 
       const notificationListener = Notifications.addNotificationReceivedListener((notification) => {
-        console.log('Notification received');
+        updateUnreadMessagesCount();
       });
 
       const responseListener = Notifications.addNotificationResponseReceivedListener((response) => {

+ 9 - 1
src/modules/api/chat/chat-api.ts

@@ -107,6 +107,10 @@ export interface PostGetBlockedReturn extends ResponseType {
   }[];
 }
 
+export interface PostGetUnreadCountReturn extends ResponseType {
+  unread_conversations: number;
+}
+
 export const chatApi = {
   searchUsers: (token: string, search: string) =>
     request.postForm<PostSearchUsersReturn>(API.SEARCH_USERS, { token, search }),
@@ -140,5 +144,9 @@ export const chatApi = {
   deleteChat: (data: PostDeleteChat) => request.postForm<ResponseType>(API.DELETE_CHAT, data),
   unreactToMessage: (data: PostUnreactToMessage) =>
     request.postForm<ResponseType>(API.UNREACT_TO_MESSAGE, data),
-  getBlocked: (token: string) => request.postForm<PostGetBlockedReturn>(API.GET_BLOCKED, { token })
+  getBlocked: (token: string) => request.postForm<PostGetBlockedReturn>(API.GET_BLOCKED, { token }),
+  getUnreadMessagesCount: (token: string) =>
+    request.postForm<PostGetUnreadCountReturn>(API.GET_UNREAD_MESSAGES_PRESENT, {
+      token
+    })
 };

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

@@ -18,5 +18,6 @@ export const chatQueryKeys = {
   setMute: () => ['setMute'] as const,
   deleteChat: () => ['deleteChat'] as const,
   unreactToMessage: () => ['unreactToMessage'] as const,
-  getBlocked: (token: string) => ['getBlocked', token] as const
+  getBlocked: (token: string) => ['getBlocked', token] as const,
+  getUnreadMessagesCount: (token: string) => ['getUnreadMessagesCount', token] as const
 };

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

@@ -13,3 +13,4 @@ export * from './use-post-set-block';
 export * from './use-post-delete-conversation';
 export * from './use-post-unreact-to-message';
 export * from './use-post-get-blocked';
+export * from './use-post-get-new-messages-present';

+ 21 - 0
src/modules/api/chat/queries/use-post-get-new-messages-present.tsx

@@ -0,0 +1,21 @@
+import { chatQueryKeys } from '../chat-query-keys';
+import { type PostGetUnreadCountReturn, chatApi } from '../chat-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchUnreadMessagesCount = async (token: string) => {
+  try {
+    const data: PostGetUnreadCountReturn = await queryClient.fetchQuery({
+      queryKey: chatQueryKeys.getUnreadMessagesCount(token),
+      queryFn: async () => {
+        const response = await chatApi.getUnreadMessagesCount(token);
+        return response.data;
+      },
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch unread messages count:', error);
+  }
+};

+ 4 - 0
src/screens/InAppScreens/MessagesScreen/index.tsx

@@ -41,6 +41,7 @@ import { useChatStore } from 'src/stores/chatStore';
 import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import SwipeableBlockedRow from './Components/SwipeableBlockedRow';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const TypingIndicator = () => {
   const [dots, setDots] = useState('');
@@ -69,6 +70,8 @@ const MessagesScreen = () => {
   const { data: chatsData, refetch } = usePostGetChatsListQuery(token, index === 2 ? 1 : 0, true);
   const { data: blockedData, refetch: refetchBlocked } = usePostGetBlockedQuery(token, true);
   const [blocked, setBlocked] = useState<Blocked[]>([]);
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
 
   const [filteredChats, setFilteredChats] = useState<{
     all: Chat[];
@@ -168,6 +171,7 @@ const MessagesScreen = () => {
     useCallback(() => {
       refetch();
       initializeSocket();
+      updateUnreadMessagesCount();
 
       return () => {
         if (socket.current) {

+ 4 - 0
src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx

@@ -44,6 +44,7 @@ import { ButtonVariants } from 'src/types/components';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useDeleteUserMutation } from '@api/app';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const ProfileSchema = yup.object({
   username: yup.string().optional(),
@@ -74,6 +75,8 @@ export const EditPersonalInfo = () => {
 
   const { data, error } = usePostGetProfileQuery(String(token), true);
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
 
   const regions = useGetRegionsWithFlagQuery(true);
   const [modalInfo, setModalInfo] = useState({
@@ -108,6 +111,7 @@ export const EditPersonalInfo = () => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
     navigation.dispatch(
       CommonActions.reset({
         index: 1,

+ 4 - 0
src/screens/LoginScreen/index.tsx

@@ -15,6 +15,7 @@ import { fetchAndSaveStatistics } from 'src/database/statisticsService';
 import { useNetInfo } from '@react-native-community/netinfo';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { usePostGetProfileInfoDataQuery } from '@api/user';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -31,6 +32,8 @@ const LoginScreen: FC<Props> = ({ navigation }) => {
 
   const { data, mutate: userLogin } = useLoginMutation();
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
   const { data: profileData } = usePostGetProfileInfoDataQuery(
     data?.token || '',
     data?.uid ? +data.uid : 0,
@@ -51,6 +54,7 @@ const LoginScreen: FC<Props> = ({ navigation }) => {
       storage.set('uid', data.uid.toString());
       storage.set('isFirstLaunch', false);
       updateNotificationStatus();
+      updateUnreadMessagesCount();
       updateLocalData(data.token);
     }
   }, [data]);

+ 14 - 14
src/screens/NotificationsScreen/index.tsx

@@ -50,6 +50,20 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
     }
   }, [notificationsSettings]);
 
+  const handleSubscribe = async () => {
+    const deviceData = await registerForPushNotificationsAsync();
+
+    if (deviceData?.notificationToken) {
+      toggleSubscription();
+      await saveNotificationToken({
+        token,
+        platform: deviceData.platform,
+        n_token: deviceData.notificationToken
+      });
+      refetchData();
+    }
+  };
+
   useEffect(() => {
     const subscription = AppState.addEventListener('change', async (nextAppState) => {
       if (nextAppState === 'active' && initialPermissionStatus !== null) {
@@ -118,20 +132,6 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
     }
   };
 
-  const handleSubscribe = async () => {
-    const deviceData = await registerForPushNotificationsAsync();
-
-    if (deviceData?.notificationToken) {
-      toggleSubscription();
-      await saveNotificationToken({
-        token,
-        platform: deviceData.platform,
-        n_token: deviceData.notificationToken
-      });
-      refetchData();
-    }
-  };
-
   async function registerForPushNotificationsAsync() {
     const existingStatus = await checkNotificationPermissions();
     let finalStatus = existingStatus;

+ 31 - 0
src/stores/unreadMessagesStore.ts

@@ -0,0 +1,31 @@
+import { fetchUnreadMessagesCount } from '@api/chat';
+import { storage, StoreType } from 'src/storage';
+import { create } from 'zustand';
+
+interface MessagesState {
+  unreadMessagesCount: number;
+  setUnreadMessagesCount: (count: number) => void;
+  updateUnreadMessagesCount: () => Promise<void>;
+}
+
+export const useMessagesStore = create<MessagesState>((set) => ({
+  unreadMessagesCount: (storage.get('unreadMessagesCount', StoreType.NUMBER) as number) ?? 0,
+  setUnreadMessagesCount: (count: number) => set({ unreadMessagesCount: count }),
+  updateUnreadMessagesCount: async () => {
+    const token = storage.get('token', StoreType.STRING);
+    if (token) {
+      try {
+        const messagesData = await fetchUnreadMessagesCount(token as string);
+        if (messagesData && typeof messagesData.unread_conversations !== 'undefined') {
+          set({ unreadMessagesCount: messagesData.unread_conversations });
+          storage.set('unreadMessagesCount', messagesData.unread_conversations);
+        }
+      } catch (error) {
+        console.error('Failed to fetch unread conversations count', error);
+      }
+    } else {
+      set({ unreadMessagesCount: 0 });
+      storage.set('unreadMessagesCount', 0);
+    }
+  }
+}));

+ 4 - 2
src/types/api.ts

@@ -139,7 +139,8 @@ export enum API_ENDPOINT {
   SET_MUTE = 'set-mute',
   DELETE_CHAT = 'delete-conversation',
   UNREACT_TO_MESSAGE = 'unreact-to-message',
-  GET_BLOCKED = 'get-blocked'
+  GET_BLOCKED = 'get-blocked',
+  GET_UNREAD_MESSAGES_PRESENT = 'new-messages-present'
 }
 
 export enum API {
@@ -255,7 +256,8 @@ export enum API {
   SET_MUTE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_MUTE}`,
   DELETE_CHAT = `${API_ROUTE.CHAT}/${API_ENDPOINT.DELETE_CHAT}`,
   UNREACT_TO_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.UNREACT_TO_MESSAGE}`,
-  GET_BLOCKED = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_BLOCKED}`
+  GET_BLOCKED = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_BLOCKED}`,
+  GET_UNREAD_MESSAGES_PRESENT = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_UNREAD_MESSAGES_PRESENT}`
 }
 
 export type BaseAxiosError = AxiosError;