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

@ -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<any> {
render () {
const { onSend, isTyping } = this.props
return (
<ThemedView style={styles.container}>
<Button onPress={() => pickImageAsync(onSend)} name='photo' />
<Button onPress={() => takePictureAsync(onSend)} name='camera' />
<Button onPress={() => getLocationAsync(onSend)} name='my-location' />
<Button
onPress={() => {
isTyping()
}}
name='chat'
/>
</ThemedView>
)
}
}
const Button = ({
onPress,
size = 30,
color = 'rgba(255,255,255, 0.8)',
...props
}) => (
<TouchableOpacity onPress={onPress}>
<MaterialIcons size={size} color={color} {...props} />
</TouchableOpacity>
)
const styles = StyleSheet.create({
container: {
height: 44,
width: '100%',
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0,0,0,0.3)',
},
})

View File

@ -0,0 +1,111 @@
import React, { useCallback } from 'react'
import {
StyleProp,
ViewStyle,
TextStyle,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { useActionSheet } from '@expo/react-native-action-sheet'
import {
getLocationAsync,
pickImageAsync,
takePictureAsync,
} from '@/components/chat/mediaUtils'
interface Props {
renderIcon?: () => React.ReactNode
wrapperStyle?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle>
iconTextStyle?: StyleProp<TextStyle>
onSend: (messages: unknown) => void
}
const CustomActions = ({
renderIcon,
iconTextStyle,
containerStyle,
wrapperStyle,
onSend,
}: Props) => {
const { showActionSheetWithOptions } = useActionSheet()
const onActionsPress = useCallback(() => {
const options = [
'Choose From Library',
'Take Picture',
'Send Location',
'Cancel',
]
const cancelButtonIndex = options.length - 1
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
pickImageAsync(onSend)
return
case 1:
takePictureAsync(onSend)
return
case 2:
getLocationAsync(onSend)
}
}
)
}, [showActionSheetWithOptions, onSend])
const renderIconComponent = useCallback(() => {
if (renderIcon)
return renderIcon()
return (
<View style={[styles.wrapper, wrapperStyle]}>
<Text style={[styles.iconText, iconTextStyle]}>+</Text>
</View>
)
}, [renderIcon, wrapperStyle, iconTextStyle])
return (
<TouchableOpacity
style={[styles.container, containerStyle]}
onPress={onActionsPress}
>
{renderIconComponent()}
</TouchableOpacity>
)
}
export default CustomActions
const styles = StyleSheet.create({
container: {
width: 26,
height: 26,
marginLeft: 10,
marginBottom: 10,
},
wrapper: {
borderRadius: 13,
borderColor: '#b2b2b2',
borderWidth: 2,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
iconText: {
color: '#b2b2b2',
fontWeight: 'bold',
fontSize: 16,
lineHeight: 16,
backgroundColor: 'transparent',
textAlign: 'center',
},
})

View File

@ -0,0 +1,92 @@
import React, { useCallback } from 'react'
import * as Linking from 'expo-linking'
import {
Platform,
StyleSheet,
TouchableOpacity,
View,
Text,
StyleProp,
ViewStyle,
} from 'react-native'
import MapView from 'react-native-maps'
interface Props {
currentMessage: any
containerStyle?: StyleProp<ViewStyle>
mapViewStyle?: StyleProp<ViewStyle>
}
const CustomView = ({
currentMessage,
containerStyle,
mapViewStyle,
}: Props) => {
const openMapAsync = useCallback(async () => {
if (Platform.OS === 'web') {
alert('Opening the map is not supported.')
return
}
const { location = {} } = currentMessage
const url = Platform.select({
ios: `http://maps.apple.com/?ll=${location.latitude},${location.longitude}`,
default: `http://maps.google.com/?q=${location.latitude},${location.longitude}`,
})
try {
const supported = await Linking.canOpenURL(url)
if (supported)
return Linking.openURL(url)
alert('Opening the map is not supported.')
} catch (e) {
alert(e.message)
}
}, [currentMessage])
if (currentMessage.location)
return (
<TouchableOpacity
style={[styles.container, containerStyle]}
onPress={openMapAsync}
>
{Platform.OS !== 'web'
? (
<MapView
style={[styles.mapView, mapViewStyle]}
region={{
latitude: currentMessage.location.latitude,
longitude: currentMessage.location.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
scrollEnabled={false}
zoomEnabled={false}
/>
)
: (
<View style={{ padding: 15 }}>
<Text style={{ color: 'tomato', fontWeight: 'bold' }}>
Map not supported in web yet, sorry!
</Text>
</View>
)}
</TouchableOpacity>
)
return null
}
export default CustomView
const styles = StyleSheet.create({
container: {},
mapView: {
width: 150,
height: 100,
borderRadius: 13,
margin: 3,
},
})

View File

@ -0,0 +1,21 @@
import React from 'react'
import { ThemedView } from '@/components/ThemedView'
import { ThemedText } from '@/components/ThemedText'
import { Platform } from 'react-native'
const NavBar = () => {
if (Platform.OS === 'web')
return null
return (
<ThemedView
style={{
alignItems: 'center',
paddingTop: 10,
}}
>
<ThemedText>💬 Chat{'\n'}</ThemedText>
</ThemedView>
)
}
export default NavBar

View File

@ -0,0 +1,129 @@
export default () => [
{
_id: Math.round(Math.random() * 1000000),
text:
'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text:
'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text:
'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text:
'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'React Native lets you build mobile apps using only JavaScript',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: Math.round(Math.random() * 1000000),
text: 'This is a system message.',
createdAt: new Date(Date.UTC(2016, 7, 30, 17, 20, 0)),
system: true,
},
]

View File

@ -0,0 +1,168 @@
export default [
{
_id: 9,
text: '#awesome 3',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 8,
text: '#awesome 2',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 7,
text: '#awesome',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 6,
text: 'Paris',
createdAt: new Date(),
user: {
_id: 2,
name: 'React Native',
},
image:
'https://www.xtrafondos.com/wallpapers/torre-eiffel-en-paris-415.jpg',
sent: true,
received: true,
},
{
_id: 5,
text: 'Send me a picture!',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 4,
text: '',
createdAt: new Date(),
user: {
_id: 2,
name: 'React Native',
},
sent: true,
received: true,
location: {
latitude: 48.864601,
longitude: 2.398704,
},
},
{
_id: 3,
text: 'Where are you?',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 2,
text: 'Yes, and I use #GiftedChat!',
createdAt: new Date(),
user: {
_id: 2,
name: 'React Native',
},
sent: true,
received: true,
},
{
_id: 1,
text: 'Are you building a chat app?',
createdAt: new Date(),
user: {
_id: 1,
name: 'Developer',
},
},
{
_id: 10,
text: 'This is a quick reply. Do you love Gifted Chat? (radio) KEEP IT',
createdAt: new Date(),
quickReplies: {
type: 'radio', // or 'checkbox',
keepIt: true,
values: [
{
title: '😋 Yes',
value: 'yes',
},
{
title:
'📷 Yes, let me show you with a picture! Again let me show you with a picture!',
value: 'yes_picture',
},
{
title: '😞 Nope. What?',
value: 'no',
},
],
},
user: {
_id: 2,
name: 'React Native',
},
},
{
_id: 20,
text: 'This is a quick reply. Do you love Gifted Chat? (checkbox)',
createdAt: new Date(),
quickReplies: {
type: 'checkbox', // or 'checkbox',
values: [
{
title: 'Yes',
value: 'yes',
},
{
title: 'Yes, let me show you with a picture!',
value: 'yes_picture',
},
{
title: 'Nope. What?',
value: 'no',
},
],
},
user: {
_id: 2,
name: 'React Native',
},
},
{
_id: 30,
createdAt: new Date(),
video: 'https://media.giphy.com/media/3o6ZthZjk09Xx4ktZ6/giphy.mp4',
user: {
_id: 2,
name: 'React Native',
},
},
{
_id: 31,
createdAt: new Date(),
audio:
'https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_700KB.mp3',
user: {
_id: 2,
name: 'React Native',
},
},
]

View File

@ -0,0 +1,82 @@
import * as Linking from 'expo-linking'
import * as Location from 'expo-location'
import * as Permissions from 'expo-permissions'
import * as ImagePicker from 'expo-image-picker'
import { Alert } from 'react-native'
export default async function getPermissionAsync (
permission: Permissions.PermissionType
) {
const { status } = await Permissions.askAsync(permission)
if (status !== 'granted') {
const permissionName = permission.toLowerCase().replace('_', ' ')
Alert.alert(
'Cannot be done 😞',
`If you would like to use this feature, you'll need to enable the ${permissionName} permission in your phone settings.`,
[
{
text: 'Let\'s go!',
onPress: () => Linking.openURL('app-settings:'),
},
{ text: 'Nevermind', onPress: () => {}, style: 'cancel' },
],
{ cancelable: true }
)
return false
}
return true
}
export async function getLocationAsync (
onSend: (locations: { location: Location.LocationObjectCoords }[]) => void
) {
const response = await Location.requestForegroundPermissionsAsync()
if (!response.granted)
return
const location = await Location.getCurrentPositionAsync()
if (!location)
return
onSend([{ location: location.coords }])
}
export async function pickImageAsync (
onSend: (images: { image: string }[]) => void
) {
const response = await ImagePicker.requestMediaLibraryPermissionsAsync()
if (!response.granted)
return
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
})
if (result.canceled)
return
const images = result.assets.map(({ uri: image }) => ({ image }))
onSend(images)
}
export async function takePictureAsync (
onSend: (images: { image: string }[]) => void
) {
const response = await ImagePicker.requestCameraPermissionsAsync()
if (!response.granted)
return
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
})
if (result.canceled)
return
const images = result.assets.map(({ uri: image }) => ({ image }))
onSend(images)
}