Add chat tab. Add GiftedChat & customize it a bit
This commit is contained in:
53
components/chat/AccessoryBar.tsx
Normal file
53
components/chat/AccessoryBar.tsx
Normal 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)',
|
||||
},
|
||||
})
|
111
components/chat/CustomActions.tsx
Normal file
111
components/chat/CustomActions.tsx
Normal 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',
|
||||
},
|
||||
})
|
92
components/chat/CustomView.tsx
Normal file
92
components/chat/CustomView.tsx
Normal 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,
|
||||
},
|
||||
})
|
21
components/chat/NavBar.tsx
Normal file
21
components/chat/NavBar.tsx
Normal 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
|
129
components/chat/data/earlierMessages.js
Normal file
129
components/chat/data/earlierMessages.js
Normal 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,
|
||||
},
|
||||
]
|
168
components/chat/data/messages.js
Normal file
168
components/chat/data/messages.js
Normal 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',
|
||||
},
|
||||
},
|
||||
]
|
82
components/chat/mediaUtils.ts
Normal file
82
components/chat/mediaUtils.ts
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user