diff --git a/app/(tabs)/messages.tsx b/app/(tabs)/messages.tsx index 7ad60a2..4dcca09 100644 --- a/app/(tabs)/messages.tsx +++ b/app/(tabs)/messages.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useReducer, useState, useRef } from 'react'; +import { io } from 'socket.io-client'; import { ThemedText, ThemedView } from '@/components/theme/Theme'; import { Alert, Linking, Platform, StyleSheet, ActivityIndicator } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -27,14 +28,21 @@ import {getMessages, sendMessage} from '@/constants/APIs'; import { getUser, getPartner } from '@/components/services/SecureStore'; import { sendPushNotification } from '@/components/services/notifications/PushNotificationManager'; +// Reducer function for managing state const reducer = (state: GCState, action: GCStateAction) => { switch (action.type) { case ActionKind.SEND_MESSAGE: { - return { ...state, messages: action.payload }; + return { + ...state, + messages: action.payload + }; } case ActionKind.LOAD_EARLIER_MESSAGES: { - return { ...state, loadEarlier: true, - isLoadingEarlier: false, messages: action.payload + return { + ...state, + loadEarlier: true, + isLoadingEarlier: false, + messages: action.payload }; } case ActionKind.LOAD_EARLIER_START: { @@ -50,14 +58,14 @@ const reducer = (state: GCState, action: GCStateAction) => { const MessagesScreen = () => { const [user, setUser] = useState(null); const [partner, setPartner] = useState(null); - const [loading, setLoading] = useState(true); const [state, dispatch] = useReducer(reducer, { messages: [], - step: 0, loadEarlier: true, isLoadingEarlier: false, isTyping: false, }); + + // Get user and partner data from SecureStore const msgUser: GCUser = { _id: user?.id ?? 0, name: user?.fullName ?? 'You', @@ -71,6 +79,7 @@ const MessagesScreen = () => { `${process.env.EXPO_PUBLIC_API_URL}/images/default-profile.png`, }; + // Initialize users & (trying to) fetch & display initial messages useEffect(() => { const initialize = async () => { try{ @@ -79,31 +88,70 @@ const MessagesScreen = () => { if (userData && partnerData) { setUser(userData); setPartner(partnerData); - await fetchInitialMessages(userData.id); } else throw new Error('User or partner not found'); } catch (error) { console.error('Error initializing users:', error); Alert.alert('Error', 'Failed to initialize users. Please try again.'); - } finally { - setLoading(false); + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: [] }); } }; initialize(); }, []); - const fetchInitialMessages = async (userId: number, limit: number = 20, offset: number = 0) => { - try { - const initialMessages = await getMessages(userId, limit, offset); - if (initialMessages) { - const formattedMessages = formatMessages(initialMessages); - dispatch({ type: ActionKind.SEND_MESSAGE, payload: formattedMessages }); - } - } catch (error) { - console.error('Error fetching initial messages:', error); - } - }; + useEffect(() => { + if (!user || !partner) return; + const socket = io(process.env.EXPO_PUBLIC_WEBSOCKET_URL as string, { + transports: ['websocket'], + }); + socket.on('connect', () => { + //console.log('Connected to WebSocket server'); + socket.emit('join', user.id); + }); + socket.on('connect_error', (error) => { + console.error('Error connecting to WebSocket server:', error); + }); + socket.on('message', async (newMessage) => { + console.log('New message received:', newMessage); + const formattedMessage = formatMessages([newMessage]); + dispatch({ + type: ActionKind.SEND_MESSAGE, + payload: GiftedChat.append(state.messages, formattedMessage), + }); + if (user && partner) { + dispatch({ type: ActionKind.LOAD_EARLIER_START }); + const initialMessages = await getMessages(user.id, 20, 0); + console.log('initial messages: ', initialMessages); + if (initialMessages) { + const formattedMessages = formatMessages(initialMessages); + console.log('formatted messages: ', formattedMessages); + dispatch({ + type: ActionKind.LOAD_EARLIER_MESSAGES, + payload: formattedMessages, + }); + } + } else console.log('user or partner not initialized'); + }); + return () => { + socket.disconnect(); + }; + }, [user, partner]); + useEffect(() => { + const fetchMessages = async () => { + if (user && partner) { + dispatch({ type: ActionKind.LOAD_EARLIER_START }); + const initialMessages = await getMessages(user.id, 20, 0); + if (initialMessages) { + const formattedMessages = formatMessages(initialMessages); + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: formattedMessages }); + } + } + }; + fetchMessages(); + }, [user, partner]); + + // Format messages for GiftedChat const formatMessages = (dbMessages: Message[]): IMessage[] => { if (!user || !partner) return []; return dbMessages.map((msg) => ({ @@ -111,88 +159,86 @@ const MessagesScreen = () => { text: msg.text, createdAt: new Date(msg.createdAt), user: msg.senderId === user.id ? msgUser : msgPartner, - })) as IMessage[]; -}; - -const onSend = useCallback(async (messages: any[]) => { - if (!user || !partner) return; - - // Prepare the message to be sent - const createdAt = new Date(); - const tempId = Math.round(Math.random() * -1000000); // Temporary ID - - const messageToSend: Message = { - id: tempId, - senderId: user.id, - receiverId: partner?.id ?? 0, - text: messages[0].text, - createdAt, - isRead: false, - hasLocation: false, - hasMedia: false, - hasQuickReply: false, + })) as IMessage[]; }; - const notificationMessage: NotificationMessage = { - sound: 'default', - title: 'New message from ' + user.fullName, - body: messageToSend.text, - data: { - message: messageToSend, - }, - }; - //sendPushNotification(partner.pushToken, notificationMessage); - sendPushNotification(user.pushToken, notificationMessage); - // Add the message with a tempId immediately to the state - const tempFormattedMessages = formatMessages([messageToSend]); - dispatch({ - type: ActionKind.SEND_MESSAGE, - payload: GiftedChat.append(state.messages, tempFormattedMessages), - }); + /* -------- Send message function -------- */ + const onSend = useCallback(async (messages: any[]) => { + if (!user || !partner) return; - try { - // Send the message to the server - const sentMessage = await sendMessage(messageToSend); + // Prepare the message to be sent + const createdAt = new Date(); + const tempId = Math.round(Math.random() * -1000000); // Temporary ID - // Fetch the latest messages from the server to ensure consistency - const updatedMessages = await getMessages(user?.id ?? 0); - if (updatedMessages) { - const formattedMessages = formatMessages(updatedMessages); - dispatch({ type: ActionKind.SEND_MESSAGE, payload: formattedMessages }); + const messageToSend: Message = { + id: tempId, + senderId: user.id, + receiverId: partner?.id ?? 0, + text: messages[0].text, + createdAt, + isRead: false, + hasLocation: false, + hasMedia: false, + hasQuickReply: false, + }; + const notificationMessage: NotificationMessage = { + sound: 'default', + title: 'New message from ' + user.fullName, + body: messageToSend.text, + data: { + message: messageToSend, + }, + }; + //sendPushNotification(partner.pushToken, notificationMessage); + sendPushNotification(user.pushToken, notificationMessage); + + // Add the message with a tempId immediately to the state + const tempFormattedMessages = formatMessages([messageToSend]); + const updatedMessages = GiftedChat.append(state.messages, tempFormattedMessages); + dispatch({ + type: ActionKind.SEND_MESSAGE, + payload: updatedMessages, + }); + + try { + // Send the message to the server + const sentMessage = await sendMessage(messageToSend); + + // Fetch the latest messages from the server to ensure consistency + const updatedMessages = await getMessages(user?.id ?? 0); + if (updatedMessages) { + const formattedMessages = formatMessages(updatedMessages); + dispatch({ type: ActionKind.SEND_MESSAGE, payload: formattedMessages }); + } + + } catch (error) { + console.error('Error sending message:', error); + // In case of an error, remove the temporary message from the state + const updatedMessages = state.messages.filter((msg) => msg._id !== tempId); + dispatch({ type: ActionKind.SEND_MESSAGE, payload: updatedMessages }); + Alert.alert('Error', 'Failed to send message. Please try again.'); } - - } catch (error) { - console.error('Error sending message:', error); - - // In case of an error, remove the temporary message from the state - const updatedMessages = state.messages.filter((msg) => msg._id !== tempId); - dispatch({ type: ActionKind.SEND_MESSAGE, payload: updatedMessages }); - Alert.alert('Error', 'Failed to send message. Please try again.'); - } -}, [user, partner, state.messages]); + }, [user, partner, state.messages]); const onLoadEarlier = useCallback(async () => { - if (!user) return; - + if (!user) { + console.log('User not found'); + return; + } // Set loading state dispatch({ type: ActionKind.LOAD_EARLIER_START }); - // Fetch the current size of messages already in chat to calculate the new offset const offset = state.messages.length; - try { const earlierMessages = await getMessages(user.id, 20, offset); if (earlierMessages) { const formattedMessages = formatMessages(earlierMessages); - - // Prepend older messages - dispatch({ - type: ActionKind.LOAD_EARLIER_MESSAGES, - payload: GiftedChat.prepend(state.messages, formattedMessages), - }); + const updatedMessages = GiftedChat.prepend(state.messages, formattedMessages); + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: updatedMessages }); } } catch (error) { console.error('Error loading earlier messages:', error); + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: [] }); } }, [user, state.messages]); @@ -307,10 +353,6 @@ const onSend = useCallback(async (messages: any[]) => { ); }, []); - if (loading) return ( - - ); - return ( diff --git a/babel.config.js b/babel.config.js index 9d89e13..d872de3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,5 +2,6 @@ module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], + plugins: ['react-native-reanimated/plugin'], }; }; diff --git a/constants/Types.ts b/constants/Types.ts index 6417f33..e33f235 100644 --- a/constants/Types.ts +++ b/constants/Types.ts @@ -125,7 +125,6 @@ export type GCMessage = { }; export type GCState = { messages: any[]; - step: number; loadEarlier?: boolean; isLoadingEarlier?: boolean; isTyping: boolean; diff --git a/package.json b/package.json index 46f43ca..e8fd9ec 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.31.1", - "react-native-web": "~0.19.13" + "react-native-web": "~0.19.13", + "socket.io-client": "^4.8.0" }, "devDependencies": { "@babel/core": "^7.25.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5c0b1e..88944dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: react-native-web: specifier: ~0.19.13 version: 0.19.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + socket.io-client: + specifier: ^4.8.0 + version: 4.8.0 devDependencies: '@babel/core': specifier: ^7.25.9 @@ -1393,6 +1396,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@teovilla/react-native-web-maps@0.9.5': resolution: {integrity: sha512-gyVqyzb7PypH3bD4s3GmLK8Xv8KUNNedyXVjzASmTO1QKbMyoadjKaSrBWJezShbs3nz6JEL6OZchx9ccwMbjA==} peerDependencies: @@ -2234,6 +2240,13 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.2: + resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -4378,6 +4391,14 @@ packages: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} + socket.io-client@4.8.0: + resolution: {integrity: sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4967,6 +4988,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -5006,6 +5039,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlhttprequest-ssl@2.1.1: + resolution: {integrity: sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7099,6 +7136,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@socket.io/component-emitter@3.1.2': {} + '@teovilla/react-native-web-maps@0.9.5(@react-google-maps/api@2.20.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(expo-location@17.0.1(expo@51.0.38(@babel/core@7.25.9)(@babel/preset-env@7.25.9(@babel/core@7.25.9))))(react-native-maps@1.14.0(react-native-web@0.19.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.25.9)(@babel/preset-env@7.25.9(@babel/core@7.25.9))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.25.9)(@babel/preset-env@7.25.9(@babel/core@7.25.9))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)': dependencies: '@react-google-maps/api': 2.20.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -7985,6 +8024,20 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.2: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + entities@4.5.0: {} env-editor@0.4.2: {} @@ -10643,6 +10696,24 @@ snapshots: slugify@1.6.6: {} + socket.io-client@4.8.0: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -11214,6 +11285,8 @@ snapshots: ws@7.5.10: {} + ws@8.17.1: {} + ws@8.18.0: {} xcode@3.0.1: @@ -11236,6 +11309,8 @@ snapshots: xmlchars@2.2.0: {} + xmlhttprequest-ssl@2.1.1: {} + xtend@4.0.2: {} y18n@4.0.3: {}