From fb0d89eba8a0ed608e0ffd5d83e81707872c8b1b Mon Sep 17 00:00:00 2001 From: gibbyb Date: Thu, 10 Oct 2024 11:04:40 -0500 Subject: [PATCH] Add chat tab. Add GiftedChat & customize it a bit --- app.json | 10 + app/(tabs)/_layout.tsx | 11 + app/(tabs)/index.tsx | 60 +++- app/(tabs)/messages.tsx | 320 +++++++++++++++++++++ assets/fonts/SpaceMono-Regular.ttf | Bin components/chat/AccessoryBar.tsx | 53 ++++ components/chat/CustomActions.tsx | 111 +++++++ components/chat/CustomView.tsx | 92 ++++++ components/chat/NavBar.tsx | 21 ++ components/chat/data/earlierMessages.js | 129 +++++++++ components/chat/data/messages.js | 168 +++++++++++ components/chat/mediaUtils.ts | 82 ++++++ example/assets/fonts/SpaceMono-Regular.ttf | Bin example/scripts/reset-project.js | 0 package-lock.json | 205 ++++++++++++- package.json | 8 +- 16 files changed, 1261 insertions(+), 9 deletions(-) create mode 100644 app/(tabs)/messages.tsx mode change 100644 => 100755 assets/fonts/SpaceMono-Regular.ttf create mode 100644 components/chat/AccessoryBar.tsx create mode 100644 components/chat/CustomActions.tsx create mode 100644 components/chat/CustomView.tsx create mode 100644 components/chat/NavBar.tsx create mode 100644 components/chat/data/earlierMessages.js create mode 100644 components/chat/data/messages.js create mode 100644 components/chat/mediaUtils.ts mode change 100644 => 100755 example/assets/fonts/SpaceMono-Regular.ttf mode change 100644 => 100755 example/scripts/reset-project.js diff --git a/app.json b/app.json index aa0dac5..7fbec2e 100644 --- a/app.json +++ b/app.json @@ -17,6 +17,10 @@ "usesAppleSignIn": true, "config": { "usesNonExemptEncryption": false + }, + "infoPList": { + "NSLocationWhenInUseUsageDescription": "This app uses your location in order to allow you to share your location in chat.", + "NSCameraUsageDescription": "This app uses your camera to take photos & send them in the chat." } }, "android": { @@ -38,6 +42,12 @@ { "faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data." } + ], + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location." + } ] ], "experiments": { diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 0edcba2..3c4359c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -30,6 +30,17 @@ const TabLayout = () => { ), }} /> + + ( + + ), + }} + /> + { const scheme = useColorScheme() ?? 'light'; + const [pushToken, setPushToken] = useState(null); + + useEffect(() => { + const fetchUserData = async () => { + const userData = await getUserData(); + if (userData) { + setPushToken(userData.pushToken); + } + }; + fetchUserData(); + }, []); + + const sendPushNotification = async () => { + if (!pushToken) { + Alert.alert('Error', 'Push token not available'); + return; + } + + const message = { + to: pushToken, + sound: 'default', + title: 'Hey Baby!', + body: 'Are you ready for push notifications?!?', + data: { + someData: 'goes here' + }, + }; + + try { + const response = await fetch(`https://exp.host/--/api/v2/push/send`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Accept-encoding': 'gzip, deflate', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }); + + const result = await response.json(); + console.log('Result:', result); + Alert.alert('Success', 'Push notification sent successfully'); + + } catch (error) { + console.error('Error sending push notification:', error); + Alert.alert('Error', 'Failed to send push notification'); + } + }; + return ( @@ -16,9 +66,9 @@ const Index = () => { diff --git a/app/(tabs)/messages.tsx b/app/(tabs)/messages.tsx new file mode 100644 index 0000000..cc95d10 --- /dev/null +++ b/app/(tabs)/messages.tsx @@ -0,0 +1,320 @@ +import React, { useCallback, useReducer } from 'react' +import { ThemedView } from '@/components/ThemedView' +import { ThemedText } from '@/components/ThemedText' +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 NavBar from '@/components/chat/NavBar' +import AccessoryBar from '@/components/chat/AccessoryBar' +import CustomActions from '@/components/chat/CustomActions' +import CustomView from '@/components/chat/CustomView' +import earlierMessages from '@/components/chat/data/earlierMessages' +import messagesData from '@/components/chat/data/messages' +import * as Clipboard from 'expo-clipboard' + +const user = { + _id: 1, + name: 'Developer', +} + +// const otherUser = { +// _id: 2, +// name: 'React Native', +// avatar: 'https://facebook.github.io/react/img/logo_og.png', +// } + +interface IState { + messages: any[] + step: number + loadEarlier?: boolean + isLoadingEarlier?: boolean + isTyping: boolean +} + +enum ActionKind { + SEND_MESSAGE = 'SEND_MESSAGE', + LOAD_EARLIER_MESSAGES = 'LOAD_EARLIER_MESSAGES', + LOAD_EARLIER_START = 'LOAD_EARLIER_START', + SET_IS_TYPING = 'SET_IS_TYPING', + // LOAD_EARLIER_END = 'LOAD_EARLIER_END', +} + +// An interface for our actions +interface StateAction { + type: ActionKind + payload?: any +} + +function reducer (state: IState, action: StateAction) { + 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 App = () => { + const [state, dispatch] = useReducer(reducer, { + messages: messagesData, + 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+)/, + style: { textDecorationLine: 'underline', color: 'darkorange' }, + onPress: () => Linking.openURL('http://gifted.chat'), + }, + ] + }, []) + + const onLongPressAvatar = useCallback((pressedUser: any) => { + Alert.alert(JSON.stringify(pressedUser)) + }, []) + + const onPressAvatar = useCallback(() => { + Alert.alert('On avatar press') + }, []) + + 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, + user, + }, + ]) + else if (replies.length > 1) + onSend([ + { + createdAt, + _id: Math.round(Math.random() * 1000000), + text: replies.map(reply => reply.title).join(', '), + user, + }, + ]) + 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, + user, + 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 AppWrapper = () => { + return ( + + + + ) +} + +const styles = StyleSheet.create({ + fill: { + flex: 1, + }, +}) + +export default AppWrapper diff --git a/assets/fonts/SpaceMono-Regular.ttf b/assets/fonts/SpaceMono-Regular.ttf old mode 100644 new mode 100755 diff --git a/components/chat/AccessoryBar.tsx b/components/chat/AccessoryBar.tsx new file mode 100644 index 0000000..831b6ad --- /dev/null +++ b/components/chat/AccessoryBar.tsx @@ -0,0 +1,53 @@ +import { MaterialIcons } from '@expo/vector-icons' +import React from 'react' +import { StyleSheet, TouchableOpacity } from 'react-native' +import { ThemedView } from '@/components/ThemedView' + +import { + getLocationAsync, + pickImageAsync, + takePictureAsync, +} from '@/components/chat/mediaUtils' + +export default class AccessoryBar extends React.Component { + render () { + const { onSend, isTyping } = this.props + + return ( + +