Add chat tab. Add GiftedChat & customize it a bit

This commit is contained in:
2024-10-10 11:04:40 -05:00
parent c001c71df8
commit fb0d89eba8
16 changed files with 1261 additions and 9 deletions

View File

@ -30,6 +30,17 @@ const TabLayout = () => {
),
}}
/>
<Tabs.Screen
name='messages'
options={{
title: 'Messages',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? 'chatbubbles' : 'chatbubbles-outline'} color={color} />
),
}}
/>
<Tabs.Screen
name='settings'
options={{

View File

@ -1,14 +1,64 @@
import { StyleSheet } from "react-native";
import React, { useEffect, useState } from "react";
import { StyleSheet, Alert } from "react-native";
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import FontAwesome from "@expo/vector-icons/FontAwesome";
import Button from "@/components/buttons/Button";
import { getUserData } from "@/components/services/securestorage/UserData";
const Index = () => {
const scheme = useColorScheme() ?? 'light';
const [pushToken, setPushToken] = useState<string | null>(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 (
<ThemedView style={styles.container}>
<ThemedText style={styles.text}>
@ -16,9 +66,9 @@ const Index = () => {
</ThemedText>
<ThemedView style={styles.footerContainer}>
<Button width={220} height={60}
onPress={() => alert('You pressed a button.')}
onPress={sendPushNotification}
>
<FontAwesome name="picture-o" size={18}
<FontAwesome name="bell" size={18}
color={Colors[scheme].background} style={styles.buttonIcon}
/>
<ThemedText
@ -27,7 +77,7 @@ const Index = () => {
{color: Colors[scheme].background}
]}
>
Press this button
Send Push Notification
</ThemedText>
</Button>
</ThemedView>

320
app/(tabs)/messages.tsx Normal file
View File

@ -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 <ThemedText>{' custom send =>'}</ThemedText>
}, [])
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 (
<AccessoryBar
onSend={onSendFromUser}
isTyping={() => setIsTyping(!state.isTyping)}
/>
)
}, [onSendFromUser, setIsTyping, state.isTyping])
const renderCustomActions = useCallback(
props =>
Platform.OS === 'web'
? null
: (
<CustomActions {...props} onSend={onSendFromUser} />
),
[onSendFromUser]
)
const renderSystemMessage = useCallback(props => {
return (
<SystemMessage
{...props}
containerStyle={{
marginBottom: 15,
}}
textStyle={{
fontSize: 14,
}}
/>
)
}, [])
const renderCustomView = useCallback(props => {
return <CustomView {...props} />
}, [])
const renderSend = useCallback((props: SendProps<IMessage>) => {
return (
<Send {...props} containerStyle={{ justifyContent: 'center', paddingHorizontal: 10 }}>
<MaterialIcons size={30} color={'tomato'} name={'send'} />
</Send>
)
}, [])
return (
<SafeAreaView style={styles.fill}>
<NavBar />
<ThemedView style={styles.fill}>
<GiftedChat
messages={state.messages}
onSend={onSend}
loadEarlier={state.loadEarlier}
onLoadEarlier={onLoadEarlier}
isLoadingEarlier={state.isLoadingEarlier}
parsePatterns={parsePatterns}
user={user}
scrollToBottom
onPressAvatar={onPressAvatar}
onLongPressAvatar={onLongPressAvatar}
onLongPress={handleLongPress}
onQuickReply={onQuickReply}
quickReplyStyle={{ borderRadius: 2 }}
quickReplyTextStyle={{
fontWeight: '200',
}}
renderQuickReplySend={renderQuickReplySend}
renderAccessory={renderAccessory}
renderActions={renderCustomActions}
renderSystemMessage={renderSystemMessage}
renderCustomView={renderCustomView}
renderSend={renderSend}
keyboardShouldPersistTaps='never'
timeTextStyle={{
left: { color: 'red' },
right: { color: 'yellow' },
}}
isTyping={state.isTyping}
inverted={Platform.OS !== 'web'}
infiniteScroll
/>
</ThemedView>
</SafeAreaView>
)
}
const AppWrapper = () => {
return (
<SafeAreaProvider>
<App />
</SafeAreaProvider>
)
}
const styles = StyleSheet.create({
fill: {
flex: 1,
},
})
export default AppWrapper