import React, { useCallback, useEffect, useReducer, useState } from 'react'; import { io } from 'socket.io-client'; import { ThemedText, ThemedView } from '@/components/theme/Theme'; import { Alert, Linking, Platform, StyleSheet } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { GiftedChat, IMessage, Send, SendProps, SystemMessage, } from 'react-native-gifted-chat'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import CustomActions from '@/components/chat/CustomActions'; import CustomView from '@/components/chat/CustomView'; import * as Clipboard from 'expo-clipboard'; import { GCUser, GCState, GCStateAction, ActionKind, User, Message, NotificationMessage, } from '@/constants/Types'; 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 }; } case ActionKind.LOAD_EARLIER_MESSAGES: { return { ...state, loadEarlier: true, isLoadingEarlier: false, messages: action.payload }; } case ActionKind.LOAD_EARLIER_START: { return { ...state, isLoadingEarlier: true } } case ActionKind.SET_IS_TYPING: { return { ...state, isTyping: action.payload } } default: return state; } }; const MessagesScreen = () => { const [user, setUser] = useState(null); const [partner, setPartner] = useState(null); const [state, dispatch] = useReducer(reducer, { messages: [], loadEarlier: true, isLoadingEarlier: false, isTyping: false, }); // Get user and partner data from SecureStore const msgUser: GCUser = { _id: user?.id ?? 0, name: user?.fullName ?? 'You', avatar: `${process.env.EXPO_PUBLIC_API_URL}${user?.pfpUrl}` ?? `${process.env.EXPO_PUBLIC_API_URL}/images/default-profile.png`, }; const msgPartner: GCUser = { _id: partner?.id ?? 0, name: partner?.fullName ?? 'Your Partner', avatar: `${process.env.EXPO_PUBLIC_API_URL}${partner?.pfpUrl}` ?? `${process.env.EXPO_PUBLIC_API_URL}/images/default-profile.png`, }; // Initialize users & (trying to) fetch & display initial messages useEffect(() => { const initialize = async () => { try{ const userData: User = await getUser() as User; const partnerData: User = await getPartner() as User; if (userData && partnerData) { setUser(userData); setPartner(partnerData); } 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.'); } }; initialize(); }, []); useEffect(() => { if (!user || !partner) { console.log('User or partner not found'); 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) => { const initialMessages = await getMessages(user.id, 20, 0); if (!initialMessages) return; const formattedMessages = formatMessages(initialMessages); dispatch({ type: ActionKind.SEND_MESSAGE, payload: formattedMessages, }); }); return () => { socket.disconnect(); }; }, [user, partner]); useEffect(() => { const fetchMessages = async () => { if (user && partner) { dispatch({ type: ActionKind.LOAD_EARLIER_START }); const initialMessages = await getMessages(user.id); 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) => ({ _id: msg.id, text: msg.text, createdAt: new Date(msg.createdAt), user: msg.senderId === user.id ? msgUser : msgPartner, })) as IMessage[]; }; /* -------- Send message function -------- */ 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, }; const notificationMessage: NotificationMessage = { sound: 'default', title: 'New message from ' + user.fullName, body: messageToSend.text, data: { message: messageToSend, }, }; sendPushNotification(partner.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); if (!user || isNaN(user.id)) { console.error('User not found'); throw new Error('User not found'); } // Fetch the latest messages from the server to ensure consistency const updatedMessages = await getMessages(user.id); 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.'); } }, [user, partner, state.messages]); const onLoadEarlier = useCallback(async () => { 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); 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]); const parsePatterns = useCallback(() => { return [ { pattern: /#(\w+)/g, style: { textDecorationLine: 'underline', color: 'darkorange' }, onPress: () => Linking.openURL('https://www.gbrown.org'), }, ] }, []); const onLongPressAvatar = useCallback((pressedUser: any) => { Alert.alert(JSON.stringify(pressedUser)) }, []); const onPressAvatar = useCallback(() => { Alert.alert('Pressed avatar!') }, []); const handleLongPress = useCallback((context: unknown, currentMessage: object) => { if (!currentMessage.text) return; const options = [ 'Copy text', 'Cancel', ] const cancelButtonIndex = options.length - 1; (context as any).actionSheet().showActionSheetWithOptions( { options, cancelButtonIndex }, (buttonIndex: number) => { switch (buttonIndex) { case 0: Clipboard.setStringAsync(currentMessage.text); break; default: break; } } ) }, []); const onQuickReply = useCallback((replies: any[]) => { const createdAt = new Date(); if (replies.length === 1) onSend([ { createdAt, _id: Math.round(Math.random() * 1000000), text: replies[0].title, user: msgUser, }, ]); else if (replies.length > 1) onSend([ { createdAt, _id: Math.round(Math.random() * 1000000), text: replies.map(reply => reply.title).join(', '), user: msgUser, }, ]); else console.warn('replies param is not set correctly'); }, [msgUser, onSend]); const renderQuickReplySend = useCallback(() => { return {'custom send =>'} }, []); const setIsTyping = useCallback((isTyping: boolean) => { dispatch({ type: ActionKind.SET_IS_TYPING, payload: isTyping }); }, [dispatch]); const renderCustomActions = useCallback( props => Platform.OS === 'web' ? null : ( ), [onSend] ); const renderSystemMessage = useCallback(props => { return ( ); }, []); const renderCustomView = useCallback(props => { return }, []); const renderSend = useCallback((props: SendProps) => { return ( ); }, []); return ( ) }; const ChatWrapper = () => { return ( ); }; export default ChatWrapper; const styles = StyleSheet.create({ fill: { flex: 1, }, });