浏览代码

websockets simple

Viktoriia 8 月之前
父节点
当前提交
6a471de368

+ 2 - 0
app.config.ts

@@ -10,6 +10,7 @@ const API_HOST = env.ENV === 'production' ? env.PRODUCTION_API_HOST : env.DEVELO
 const MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
 
 const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
+const WEBSOCKET_URL = env.ENV === 'production' ? env.PRODUCTION_WEBSOCKET_URL : env.DEVELOPMENT_WEBSOCKET_URL;
 
 dotenv.config({
   path: path.resolve(process.cwd(), '.env')
@@ -33,6 +34,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     API_HOST: API_HOST,
     MAP_HOST: MAP_HOST,
     GOOGLE_MAP_PLACES_APIKEY: GOOGLE_MAP_PLACES_APIKEY,
+    WEBSOCKET_URL: WEBSOCKET_URL,
     eas: {
       projectId: env.EAS_PROJECT_ID
     }

+ 2 - 0
src/constants/secrets.ts

@@ -17,3 +17,5 @@ export const APP_VERSION = Constants?.expoConfig?.version ?? Constants?.manifest
 
 export const GOOGLE_MAP_PLACES_APIKEY =
   extra?.GOOGLE_MAP_PLACES_APIKEY || Constants?.expoConfig?.extra?.GOOGLE_MAP_PLACES_APIKEY;
+
+export const WEBSOCKET_URL = extra?.WEBSOCKET_URL || Constants?.expoConfig?.extra?.WEBSOCKET_URL;

+ 91 - 24
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -53,7 +53,7 @@ import {
   usePostUnreactToMessageMutation
 } from '@api/chat';
 import { CustomMessage, Message, Reaction } from '../types';
-import { API_HOST } from 'src/constants';
+import { API_HOST, WEBSOCKET_URL } from 'src/constants';
 import { getFontSize } from 'src/utils';
 import ReactionBar from '../Components/ReactionBar';
 import OptionsMenu from '../Components/OptionsMenu';
@@ -106,11 +106,76 @@ const ChatScreen = ({ route }: { route: any }) => {
 
   const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
   const [isRerendering, setIsRerendering] = useState<boolean>(false);
+  const [isTyping, setIsTyping] = useState<boolean>(false);
 
   const messageRefs = useRef<{ [key: string]: any }>({});
   const flatList = useRef<FlatList | null>(null);
   const scrollY = useSharedValue(0);
 
+  const socket = useRef<WebSocket | null>(null);
+
+  useEffect(() => {
+    socket.current = new WebSocket(WEBSOCKET_URL);
+
+    socket.current.onopen = () => {
+      socket.current?.send(JSON.stringify({ token }));
+    };
+
+    socket.current.onmessage = (event) => {
+      const data = JSON.parse(event.data);
+      handleWebSocketMessage(data);
+    };
+
+    socket.current.onclose = () => {
+      console.log('WebSocket connection closed chat screen');
+    };
+
+    return () => {
+      socket.current?.close();
+    };
+  }, [token]);
+
+  const handleWebSocketMessage = (data: any) => {
+    switch (data.action) {
+      case 'new_message':
+        if (data.conversation_with === id) {
+          refetch();
+        }
+        break;
+      case 'is_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(true);
+        }
+        break;
+      case 'stopped_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(false);
+        }
+        break;
+      case 'new_reaction':
+        if (data.conversation_with === id) {
+          refetch();
+        }
+        break;
+      default:
+        break;
+    }
+  };
+
+  const sendWebSocketMessage = (action: string) => {
+    if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+      socket.current.send(JSON.stringify({ action, conversation_with: id }));
+    }
+  };
+
+  const handleTyping = (isTyping: boolean) => {
+    if (isTyping) {
+      sendWebSocketMessage('is_typing');
+    } else {
+      sendWebSocketMessage('stopped_typing');
+    }
+  };
+
   const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
     return {
       _id: message.id,
@@ -142,19 +207,24 @@ const ChatScreen = ({ route }: { route: any }) => {
       if (chatData?.messages) {
         const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
 
-        const firstUnreadIndex = mappedMessages.findLastIndex(
-          (msg) => !msg.received && !msg?.deleted && msg.user._id === id
-        );
-        if (firstUnreadIndex !== -1) {
-          setUnreadMessageIndex(firstUnreadIndex);
+        if (unreadMessageIndex === null) {
+          const firstUnreadIndex = mappedMessages.findLastIndex(
+            (msg) => !msg.received && !msg?.deleted && msg.user._id === id
+          );
+
+          if (firstUnreadIndex !== -1) {
+            setUnreadMessageIndex(firstUnreadIndex);
 
-          const unreadMarker: any = {
-            _id: 'unreadMarker',
-            text: 'Unread messages',
-            system: true
-          };
+            const unreadMarker: any = {
+              _id: 'unreadMarker',
+              text: 'Unread messages',
+              system: true
+            };
 
-          mappedMessages.splice(firstUnreadIndex, 0, unreadMarker);
+            mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker);
+          } else {
+            setUnreadMessageIndex(0);
+          }
         }
         setMessages(mappedMessages);
       }
@@ -185,6 +255,8 @@ const ChatScreen = ({ route }: { route: any }) => {
         {
           onSuccess: (res) => {
             newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
+            // sendWebSocketMessage('messages_read');
+            sendWebSocketMessage('new_message');
           }
         }
       );
@@ -276,6 +348,8 @@ const ChatScreen = ({ route }: { route: any }) => {
       {
         onSuccess: () => {
           setMessages((prevMessages) => prevMessages.filter((msg) => msg._id !== messageId));
+          // sendWebSocketMessage('message_deleted');
+          sendWebSocketMessage('new_message');
         }
       }
     );
@@ -330,6 +404,7 @@ const ChatScreen = ({ route }: { route: any }) => {
               return msg;
             })
           );
+          sendWebSocketMessage('new_reaction');
         }
       }
     );
@@ -484,14 +559,6 @@ const ChatScreen = ({ route }: { route: any }) => {
     }, [navigation])
   );
 
-  useEffect(() => {
-    const intervalId = setInterval(() => {
-      refetch();
-    }, 5000);
-
-    return () => clearInterval(intervalId);
-  }, [refetch]);
-
   const onSend = useCallback(
     (newMessages: CustomMessage[] = []) => {
       if (replyMessage) {
@@ -511,7 +578,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           reply_to_id: replyMessage ? (replyMessage._id as number) : -1
         },
         {
-          onSuccess: (res) => console.log('res', res),
+          onSuccess: () => sendWebSocketMessage('new_message'),
           onError: (err) => console.log('err', err)
         }
       );
@@ -644,7 +711,7 @@ const ChatScreen = ({ route }: { route: any }) => {
     reactToMessage(
       { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
       {
-        onSuccess: (res) => console.log('res', res),
+        onSuccess: () => sendWebSocketMessage('new_reaction'),
         onError: (err) => console.log('err', err)
       }
     );
@@ -978,6 +1045,8 @@ const ChatScreen = ({ route }: { route: any }) => {
           isCustomViewBottom={false}
           messageContainerRef={messageContainerRef}
           minComposerHeight={34}
+          onInputTextChanged={(text) => handleTyping(text.length > 0)}
+          isTyping={isTyping}
           renderSend={(props) => (
             <View
               style={{
@@ -1040,8 +1109,6 @@ const ChatScreen = ({ route }: { route: any }) => {
               }
             }
           ]}
-          // inverted={true}
-          // isTyping={true}
         />
 
         <Modal visible={!!selectedMedia} transparent={true}>

+ 98 - 14
src/screens/InAppScreens/MessagesScreen/index.tsx

@@ -1,5 +1,12 @@
 import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { View, Text, TouchableOpacity, Image, Platform, TouchableHighlight } from 'react-native';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Image,
+  Platform,
+  TouchableHighlight,
+} from 'react-native';
 import {
   AvatarWithInitials,
   HorizontalTabView,
@@ -11,7 +18,7 @@ import { NAVIGATION_PAGES } from 'src/types';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
 
 import AddChatIcon from 'assets/icons/messages/chat-plus.svg';
-import { API_HOST } from 'src/constants';
+import { API_HOST, WEBSOCKET_URL } from 'src/constants';
 import { Colors } from 'src/theme';
 import SwipeableRow from './Components/SwipeableRow';
 import { FlashList } from '@shopify/flash-list';
@@ -35,6 +42,25 @@ import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import SwipeableBlockedRow from './Components/SwipeableBlockedRow';
 
+const TypingIndicator = () => {
+  const [dots, setDots] = useState('');
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      setDots((prevDots) => {
+        if (prevDots.length >= 3) {
+          return '';
+        }
+        return prevDots + '.';
+      });
+    }, 500);
+
+    return () => clearInterval(interval);
+  }, []);
+
+  return <Text style={styles.typingText}>Typing{dots}</Text>;
+};
+
 const MessagesScreen = () => {
   const navigation = useNavigation();
   const token = storage.get('token', StoreType.STRING) as string;
@@ -53,6 +79,58 @@ const MessagesScreen = () => {
   const [search, setSearch] = useState('');
   const openRowRef = useRef<any>(null);
   const { isWarningModalVisible, setIsWarningModalVisible } = useChatStore();
+  const [typingUsers, setTypingUsers] = useState<{ [key: string]: boolean }>({});
+
+  const socket = useRef<WebSocket | null>(null);
+
+  const initializeSocket = () => {
+    if (socket.current) {
+      socket.current.close();
+    }
+
+    setTimeout(() => {
+      socket.current = new WebSocket(WEBSOCKET_URL);
+
+      socket.current.onopen = () => {
+        socket.current?.send(JSON.stringify({ token }));
+      };
+
+      socket.current.onmessage = (event) => {
+        const data = JSON.parse(event.data);
+        handleWebSocketMessage(data);
+      };
+
+      socket.current.onclose = () => {
+        console.log('WebSocket connection closed');
+      };
+    }, 500);
+  };
+
+  const handleWebSocketMessage = (data: any) => {
+    switch (data.action) {
+      case 'new_message':
+        refetch();
+        break;
+      case 'is_typing':
+        if (data.conversation_with) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.conversation_with]: true
+          }));
+        }
+        break;
+      case 'stopped_typing':
+        if (data.conversation_with) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.conversation_with]: false
+          }));
+        }
+        break;
+      default:
+        break;
+    }
+  };
 
   const handleRowOpen = (ref: any) => {
     if (openRowRef.current && openRowRef.current !== ref) {
@@ -89,16 +167,16 @@ const MessagesScreen = () => {
   useFocusEffect(
     useCallback(() => {
       refetch();
-    }, [])
-  );
-
-  useEffect(() => {
-    const intervalId = setInterval(() => {
-      refetch();
-    }, 5000);
+      initializeSocket();
 
-    return () => clearInterval(intervalId);
-  }, [refetch]);
+      return () => {
+        if (socket.current) {
+          socket.current.close();
+          socket.current = null;
+        }
+      };
+    }, [token])
+  );
 
   const filterChatsByTab = () => {
     let filteredList = chats;
@@ -162,6 +240,7 @@ const MessagesScreen = () => {
         refetchBlocked={refetchBlocked}
       >
         <TouchableHighlight
+          key={`${item.uid}-${typingUsers[item.uid]}`}
           activeOpacity={0.8}
           onPress={() =>
             navigation.navigate(
@@ -211,9 +290,13 @@ const MessagesScreen = () => {
               </View>
 
               <View style={[styles.rowContainer, { flex: 1, gap: 6 }]}>
-                <Text numberOfLines={2} style={styles.chatMessage}>
-                  {item.short}
-                </Text>
+                {typingUsers[item.uid] ? (
+                  <TypingIndicator />
+                ) : (
+                  <Text numberOfLines={2} style={styles.chatMessage}>
+                    {item.short}
+                  </Text>
+                )}
 
                 {item.unread_count > 0 ? (
                   <View style={styles.unreadBadge}>
@@ -335,6 +418,7 @@ const MessagesScreen = () => {
               renderItem={renderChatItem}
               keyExtractor={(item, index) => `${item.uid}-${index}`}
               estimatedItemSize={78}
+              extraData={typingUsers}
             />
           )
         }

+ 8 - 1
src/screens/InAppScreens/MessagesScreen/styles.tsx

@@ -75,5 +75,12 @@ export const styles = StyleSheet.create({
   },
   swipeButton: {
     paddingHorizontal: 20
-  }
+  },
+  typingText: {
+    flex: 1,
+    fontSize: getFontSize(12),
+    color: Colors.LIGHT_GRAY,
+    fontStyle: 'italic',
+    height: '100%'
+  },
 });