From f48ec76cba5b2db6987fd69e6fa9270553546290 Mon Sep 17 00:00:00 2001 From: gibbyb Date: Mon, 21 Oct 2024 16:58:41 -0500 Subject: [PATCH] fix stuff --- app/(tabs)/_messages.tsx | 264 +++++++++++++++++++++++++++++++++++++++ app/(tabs)/messages.tsx | 176 ++++++++++++++++++-------- constants/APIs.ts | 51 +++++++- 3 files changed, 437 insertions(+), 54 deletions(-) create mode 100644 app/(tabs)/_messages.tsx diff --git a/app/(tabs)/_messages.tsx b/app/(tabs)/_messages.tsx new file mode 100644 index 0000000..0e747d9 --- /dev/null +++ b/app/(tabs)/_messages.tsx @@ -0,0 +1,264 @@ +import React, { useCallback, useReducer } from 'react'; +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 AccessoryBar from '@/components/chat/AccessoryBar'; +import CustomActions from '@/components/chat/CustomActions'; +import CustomView from '@/components/chat/CustomView'; +//import NavBar from '@/components/chat/NavBar'; +import earlierMessages from '@/components/chat/data/earlierMessages'; +import messages from '@/components/chat/data/messages'; +import * as Clipboard from 'expo-clipboard'; +import { + GCUser, + GCState, + GCStateAction, + ActionKind, +} from '@/constants/Types'; +import {getInitialMessages, sendMessage} from '@/constants/APIs'; + +const tempUser: GCUser = { + _id: 1, + name: 'Developer', +}; + +const reducer = (state: GCState, action: GCStateAction) => { + switch (action.type) { + case ActionKind.SEND_MESSAGE: { + return { + ...state, + step: state.step + 1, + 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, + } + } + } +}; + +const MessagesScreen = () => { + //const newMessages = + const [state, dispatch] = useReducer(reducer, { + messages: messages, + step: 0, + loadEarlier: true, + isLoadingEarlier: false, + isTyping: false, + }) + + const onSend = useCallback((messages: any[]) => { + const sentMessages = [{ ...messages[0], sent: true, received: true }] + const newMessages = GiftedChat.append( + state.messages, sentMessages, Platform.OS !== 'web' + ); + dispatch({ type: ActionKind.SEND_MESSAGE, payload: newMessages }); + }, [dispatch, state.messages]); + + const onLoadEarlier = useCallback(() => { + dispatch({ type: ActionKind.LOAD_EARLIER_START }); + setTimeout(() => { + const newMessages = GiftedChat.prepend( + state.messages, earlierMessages() as IMessage[], Platform.OS !== 'web' + ); + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: newMessages }) + }, 1500) // simulating network + }, [dispatch, 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (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, + tempUser, + }, + ]); + else if (replies.length > 1) + onSend([ + { + createdAt, + _id: Math.round(Math.random() * 1000000), + text: replies.map(reply => reply.title).join(', '), + tempUser, + }, + ]); + else console.warn('replies param is not set correctly'); + }, []); + + const renderQuickReplySend = useCallback(() => { + return {'custom send =>'} + }, []); + + const setIsTyping = useCallback((isTyping: boolean) => { + dispatch({ type: ActionKind.SET_IS_TYPING, payload: isTyping }); + }, [dispatch]); + + const onSendFromUser = useCallback((messages: IMessage[] = []) => { + const createdAt = new Date(); + const messagesToUpload = messages.map(message => ({ + ...message, tempUser, createdAt, id: Math.round(Math.random() * 1000000), + })); + onSend(messagesToUpload); + }, [onSend]); + + const renderAccessory = useCallback(() => { + return ( + setIsTyping(!state.isTyping)} + /> + ); + }, [onSendFromUser, setIsTyping, state.isTyping]); + + const renderCustomActions = useCallback( + props => + Platform.OS === 'web' ? null : ( + + ), + [onSendFromUser] + ); + + 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, + }, +}); diff --git a/app/(tabs)/messages.tsx b/app/(tabs)/messages.tsx index 861d40d..c533dc3 100644 --- a/app/(tabs)/messages.tsx +++ b/app/(tabs)/messages.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; import { ThemedText, ThemedView } from '@/components/theme/Theme'; -import { Alert, Linking, Platform, StyleSheet } from 'react-native'; +import { Alert, Linking, Platform, StyleSheet, ActivityIndicator } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { GiftedChat, @@ -10,31 +10,25 @@ import { SystemMessage, } from 'react-native-gifted-chat'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; -import AccessoryBar from '@/components/chat/AccessoryBar'; import CustomActions from '@/components/chat/CustomActions'; import CustomView from '@/components/chat/CustomView'; -//import NavBar from '@/components/chat/NavBar'; -import earlierMessages from '@/components/chat/data/earlierMessages'; -import messages from '@/components/chat/data/messages'; import * as Clipboard from 'expo-clipboard'; import { GCUser, GCState, GCStateAction, ActionKind, + User, + Message, } from '@/constants/Types'; - -const tempUser: GCUser = { - _id: 1, - name: 'Developer', -}; +import {getInitialMessages, sendMessage} from '@/constants/APIs'; +import { getUser, getPartner } from '@/components/services/SecureStore'; const reducer = (state: GCState, action: GCStateAction) => { switch (action.type) { case ActionKind.SEND_MESSAGE: { return { ...state, - step: state.step + 1, messages: action.payload, }; } @@ -62,30 +56,118 @@ const reducer = (state: GCState, action: GCStateAction) => { }; const MessagesScreen = () => { + const [user, setUser] = useState(null); + const [partner, setPartner] = useState(null); const [state, dispatch] = useReducer(reducer, { - messages: messages, + messages: [], step: 0, loadEarlier: true, isLoadingEarlier: false, isTyping: false, - }) + }); + const msgUser: GCUser = { + _id: user?.id || 0, + name: user?.fullName ?? 'You', + avatar: user?.pfpUrl ?? require('@/assets/images/default-profile.png'), + }; + const msgPartner: GCUser = { + _id: partner?.id || 0, + name: partner?.fullName ?? 'Your Partner', + avatar: partner?.pfpUrl ?? require('@/assets/images/default-profile.png'), + }; - const onSend = useCallback((messages: any[]) => { - const sentMessages = [{ ...messages[0], sent: true, received: true }] - const newMessages = GiftedChat.append( - state.messages, sentMessages, Platform.OS !== 'web' - ); - dispatch({ type: ActionKind.SEND_MESSAGE, payload: newMessages }); - }, [dispatch, state.messages]); + useEffect(() => { + const initializeUsers = async () => { + const userData: User = await getUser() as User; + const partnerData: User = await getPartner() as User; + if (userData && partnerData) { + setUser(userData); + setPartner(partnerData); + fetchInitialMessages(userData.id); + } + }; + initializeUsers(); + }, []); - const onLoadEarlier = useCallback(() => { + const fetchInitialMessages = async (userId: number) => { + try { + const initialMessages = await getInitialMessages(userId); + if (initialMessages) { + const formattedMessages = formatMessages(initialMessages); + dispatch({ type: ActionKind.SEND_MESSAGE, payload: formattedMessages }); + } + } catch (error) { + console.error('Error fetching initial messages:', error); + } + }; + + 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[]; +}; + + const onSend = useCallback(async (messages: any[]) => { + if (!user || !partner) return; + const tempId = Math.round(Math.random() * -1000000); + try { + const messageToSend: Message = { + id: tempId, + senderId: user.id, + receiverId: partner?.id ?? 0, + text: messages[0].text, + createdAt: new Date(), + isRead: false, + hasLocation: false, + hasMedia: false, + hasQuickReply: false, + }; + const tempFormattedMessages = formatMessages([messageToSend]); + dispatch({ + type: ActionKind.SEND_MESSAGE, + payload: GiftedChat.append(state.messages, tempFormattedMessages, Platform.OS !== 'web'), + }); + const sentMessage = await sendMessage(messageToSend); + if (!sentMessage) + throw new Error('Message failed to send'); + const updatedMessages = state.messages.map(msg => + msg._id === tempId ? {...msg, _id: sentMessage.id} : msg + ) as IMessage[]; + dispatch({ + type: ActionKind.SEND_MESSAGE, + payload: updatedMessages, + }); + } catch (error) { + console.error('Error sending message:', error); + 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, state.messages]); + + + const onLoadEarlier = useCallback(async () => { + if (!user || !partner) return; dispatch({ type: ActionKind.LOAD_EARLIER_START }); - setTimeout(() => { - const newMessages = GiftedChat.prepend( - state.messages, earlierMessages() as IMessage[], Platform.OS !== 'web' - ); - dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: newMessages }) - }, 1500) // simulating network + try { + const earlierMessages = await getInitialMessages(user.id, 20, state.messages.length); + if (earlierMessages) { + const formattedMessages = formatMessages(earlierMessages); + dispatch({ + type: ActionKind.LOAD_EARLIER_MESSAGES, + payload: GiftedChat.prepend(state.messages, formattedMessages, Platform.OS !== 'web') + }); + } + } catch (error) { + console.error('Error fetching earlier messages:', error); + } }, [dispatch, state.messages]); const parsePatterns = useCallback(() => { @@ -113,7 +195,6 @@ const MessagesScreen = () => { 'Cancel', ] const cancelButtonIndex = options.length - 1; - // eslint-disable-next-line @typescript-eslint/no-explicit-any (context as any).actionSheet().showActionSheetWithOptions( { options, cancelButtonIndex }, (buttonIndex: number) => { @@ -135,7 +216,7 @@ const MessagesScreen = () => { createdAt, _id: Math.round(Math.random() * 1000000), text: replies[0].title, - tempUser, + user: msgUser, }, ]); else if (replies.length > 1) @@ -144,7 +225,7 @@ const MessagesScreen = () => { createdAt, _id: Math.round(Math.random() * 1000000), text: replies.map(reply => reply.title).join(', '), - tempUser, + user: msgUser, }, ]); else console.warn('replies param is not set correctly'); @@ -158,29 +239,12 @@ const MessagesScreen = () => { dispatch({ type: ActionKind.SET_IS_TYPING, payload: isTyping }); }, [dispatch]); - const onSendFromUser = useCallback((messages: IMessage[] = []) => { - const createdAt = new Date(); - const messagesToUpload = messages.map(message => ({ - ...message, tempUser, createdAt, id: Math.round(Math.random() * 1000000), - })); - onSend(messagesToUpload); - }, [onSend]); - - const renderAccessory = useCallback(() => { - return ( - setIsTyping(!state.isTyping)} - /> - ); - }, [onSendFromUser, setIsTyping, state.isTyping]); - const renderCustomActions = useCallback( props => Platform.OS === 'web' ? null : ( - + ), - [onSendFromUser] + [onSend] ); const renderSystemMessage = useCallback(props => { @@ -207,6 +271,11 @@ const MessagesScreen = () => { ); }, []); + + if (!user || !partner) return ( + + ); + return ( @@ -217,7 +286,7 @@ const MessagesScreen = () => { onLoadEarlier={onLoadEarlier} isLoadingEarlier={state.isLoadingEarlier} parsePatterns={parsePatterns} - user={tempUser} + user={msgUser as GCUser} scrollToBottom onPressAvatar={onPressAvatar} onLongPressAvatar={onLongPressAvatar} @@ -228,7 +297,6 @@ const MessagesScreen = () => { fontWeight: '200', }} renderQuickReplySend={renderQuickReplySend} - //renderAccessory={renderAccessory} renderActions={renderCustomActions} renderSystemMessage={renderSystemMessage} renderCustomView={renderCustomView} @@ -249,7 +317,9 @@ const MessagesScreen = () => { const ChatWrapper = () => { return ( - + + + ); }; diff --git a/constants/APIs.ts b/constants/APIs.ts index 8343639..f1b29fd 100644 --- a/constants/APIs.ts +++ b/constants/APIs.ts @@ -1,5 +1,5 @@ import * as FileSystem from 'expo-file-system'; -import type { User, RelationshipData } from '@/constants/Types'; +import type { User, RelationshipData, Message } from '@/constants/Types'; export const getInitialDataByAppleId = async (appleId: string) => { try { @@ -194,3 +194,52 @@ export const sendRelationshipRequest = async (userId: number, targetUserId: numb throw error; } }; + +export const getInitialMessages = async (userId: number, limit: number = 20) => { + if (!userId || isNaN(userId)) return; + try { + const apiUrl = `${process.env.EXPO_PUBLIC_API_URL}/api/messages/getInitialMessages`; + const response = await fetch((apiUrl + `?userId=${userId}&limit=${limit}`), { + headers: { + 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', + }, + }); + if (!response.ok) { + throw new Error( + `Error getting initial messages: ${response.status} ${response.statusText}` + ); + } + const messages = await response.json() as Message[]; + return messages; + } catch (error: unknown) { + console.error('Error getting initial messages:', error); + throw error; + } +}; + +export const sendMessage = async (message: Message) => { + if (!message) return; + try { + const apiUrl = `${process.env.EXPO_PUBLIC_API_URL}/api/messages/send`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', + }, + body: JSON.stringify({ + message: message, + }), + }); + if (!response.ok) { + throw new Error( + `Error sending message: ${response.status} ${response.statusText}` + ); + } + const messageData = await response.json() as Message; + return messageData; + } catch (error: unknown) { + console.error('Error sending message:', error); + throw error; + } +};