From 8c095b72cd4a71bcdf9a00ed33caee643a358313 Mon Sep 17 00:00:00 2001 From: Johan-dutoit Date: Wed, 7 Jun 2023 17:57:31 +0200 Subject: [PATCH] refactor(example): change to functional component --- README.md | 19 +- example/App.tsx | 422 ++++++++++++------------- example/example-expo/CustomActions.tsx | 63 ++-- src/GiftedChat.tsx | 7 +- 4 files changed, 265 insertions(+), 246 deletions(-) diff --git a/README.md b/README.md index 9a5fbe5dd..f13cefe27 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,14 @@

-## The future of GiftedChat 🎉 +## The future of GiftedChat 🎉 + Please give us your advice: [Related PR](https://github.com/FaridSafi/react-native-gifted-chat/pull/1775) ## Please vote **GiftedChat** depends on other packages and some needs a boost, please vote for PRs will improve it, thanks: + - https://github.com/watadarkstar/react-native-typing-animation/issues/18 ## Features @@ -100,23 +102,24 @@ Please give us your advice: [Related PR](https://github.com/FaridSafi/react-nati - Use version `0.0.10` for RN `< 0.40.0` ## Testing + `Test_ID` is exported as constants that can be used in your testing library of choice -Gifted Chat uses `onLayout` to determine the height of the chat container. To trigger `onLayout` during your tests, you can run the following bits of code. +Gifted Chat uses `onLayout` to determine the height of the chat container. To trigger `onLayout` during your tests, you can run the following bits of code. ```typescript const WIDTH = 200 // or any number const HEIGHT = 2000 // or any number -const loadingWrapper = getByTestId(Test_ID.LOADING.WRAPPER) -fireEvent(loadingWrapper, "layout", { +const loadingWrapper = getByTestId(TEST_ID.LOADING_WRAPPER) +fireEvent(loadingWrapper, 'layout', { nativeEvent: { layout: { width: WIDTH, height: HEIGHT, }, }, -}); +}) ``` ## Installation @@ -143,7 +146,7 @@ import React, { useState, useCallback, useEffect } from 'react' import { GiftedChat } from 'react-native-gifted-chat' export function Example() { - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]) useEffect(() => { setMessages([ @@ -161,7 +164,9 @@ export function Example() { }, []) const onSend = useCallback((messages = []) => { - setMessages(previousMessages => GiftedChat.append(previousMessages, messages)) + setMessages(previousMessages => + GiftedChat.append(previousMessages, messages), + ) }, []) return ( diff --git a/example/App.tsx b/example/App.tsx index ef57ad4dc..56b5df8df 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,33 +1,20 @@ -import MaterialIcons from '@expo/vector-icons/MaterialIcons' -import * as Linking from 'expo-linking' -import { SafeAreaView } from 'react-native-safe-area-context' -import AppLoading from 'expo-app-loading' -import React, { Component } from 'react' -import { StyleSheet, View, Text, Platform, Alert } from 'react-native' +import { MaterialIcons } from '@expo/vector-icons' +import React, { useCallback, useReducer } from 'react' +import { Alert, Linking, Platform, StyleSheet, Text, View } from 'react-native' import { - Bubble, GiftedChat, - SystemMessage, IMessage, Send, SendProps, + SystemMessage, } from 'react-native-gifted-chat' - +import { SafeAreaView } from 'react-native-safe-area-context' +import { NavBar } from './components/navbar' import AccessoryBar from './example-expo/AccessoryBar' import CustomActions from './example-expo/CustomActions' import CustomView from './example-expo/CustomView' -import messagesData from './example-expo/data/messages' import earlierMessages from './example-expo/data/earlierMessages' -import { NavBar } from './components/navbar' - -const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#f5f5f5', }, - content: { backgroundColor: "#ffffff", flex: 1, } -}) - -const filterBotMessages = message => - !message.system && message.user && message.user._id && message.user._id === 2 -const findStep = step => message => message._id === step +import messagesData from './example-expo/data/messages' const user = { _id: 1, @@ -40,92 +27,98 @@ const otherUser = { avatar: 'https://facebook.github.io/react/img/logo_og.png', } -export default class App extends Component { - state = { - inverted: false, - step: 0, - messages: [], - loadEarlier: true, - typingText: null, - isLoadingEarlier: false, - appIsReady: false, - isTyping: false, - } - - _isMounted = false +interface IState { + messages: any[] + step: number + loadEarlier?: boolean + isLoadingEarlier?: boolean + isTyping: boolean +} - componentDidMount() { - this._isMounted = true - // init with only system messages - this.setState({ - messages: messagesData, // messagesData.filter(message => message.system), - appIsReady: true, - isTyping: false, - }) - } +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', +} - componentWillUnmount() { - this._isMounted = false - } +// An interface for our actions +interface StateAction { + type: ActionKind + payload?: any +} - onLoadEarlier = () => { - this.setState(() => { +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, } - }) - - setTimeout(() => { - if (this._isMounted === true) { - this.setState((previousState: any) => { - return { - messages: GiftedChat.prepend( - previousState.messages, - earlierMessages() as IMessage[], - Platform.OS !== 'web', - ), - loadEarlier: true, - isLoadingEarlier: false, - } - }) + } + case ActionKind.SET_IS_TYPING: { + return { + ...state, + isTyping: action.payload, } - }, 1500) // simulating network + } } +} - onSend = (messages = []) => { - const step = this.state.step + 1 - this.setState((previousState: any) => { +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 }] - return { - messages: GiftedChat.append( - previousState.messages, - sentMessages, - Platform.OS !== 'web', - ), - step, - } - }) - // for demo purpose - // setTimeout(() => this.botSend(step), Math.round(Math.random() * 1000)) - } + const newMessages = GiftedChat.append( + state.messages, + sentMessages, + Platform.OS !== 'web', + ) - botSend = (step = 0) => { - const newMessage = (messagesData as IMessage[]) - .reverse() - // .filter(filterBotMessages) - .find(findStep(step)) - if (newMessage) { - this.setState((previousState: any) => ({ - messages: GiftedChat.append( - previousState.messages, - [newMessage], - Platform.OS !== 'web', - ), - })) - } - } + dispatch({ type: ActionKind.SEND_MESSAGE, payload: newMessages }) + }, + [dispatch, state.messages], + ) + + const onLoadEarlier = useCallback(() => { + console.log('loading') + dispatch({ type: ActionKind.LOAD_EARLIER_START }) + setTimeout(() => { + const newMessages = GiftedChat.prepend( + state.messages, + earlierMessages() as IMessage[], + Platform.OS !== 'web', + ) - parsePatterns = (_linkStyle: any) => { + dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: newMessages }) + }, 1500) // simulating network + }, [dispatch, state.messages]) + + const parsePatterns = useCallback((_linkStyle: any) => { return [ { pattern: /#(\w+)/, @@ -133,79 +126,20 @@ export default class App extends Component { onPress: () => Linking.openURL('http://gifted.chat'), }, ] - } - - renderCustomView(props) { - return - } - - onReceive = (text: string) => { - this.setState((previousState: any) => { - return { - messages: GiftedChat.append( - previousState.messages as any, - [ - { - _id: Math.round(Math.random() * 1000000), - text, - createdAt: new Date(), - user: otherUser, - }, - ], - Platform.OS !== 'web', - ), - } - }) - } - - onSendFromUser = (messages: IMessage[] = []) => { - const createdAt = new Date() - const messagesToUpload = messages.map(message => ({ - ...message, - user, - createdAt, - _id: Math.round(Math.random() * 1000000), - })) - this.onSend(messagesToUpload) - } + }, []) - setIsTyping = () => { - this.setState({ - isTyping: !this.state.isTyping, - }) - } + const onLongPressAvatar = useCallback((pressedUser: any) => { + Alert.alert(JSON.stringify(pressedUser)) + }, []) - renderAccessory = () => ( - - ) + const onPressAvatar = useCallback(() => { + Alert.alert('On avatar press') + }, []) - renderCustomActions = props => - Platform.OS === 'web' ? null : ( - - ) - - renderBubble = (props: any) => { - return - } - - renderSystemMessage = props => { - return ( - - ) - } - - onQuickReply = replies => { + const onQuickReply = useCallback((replies: any[]) => { const createdAt = new Date() if (replies.length === 1) { - this.onSend([ + onSend([ { createdAt, _id: Math.round(Math.random() * 1000000), @@ -214,7 +148,7 @@ export default class App extends Component { }, ]) } else if (replies.length > 1) { - this.onSend([ + onSend([ { createdAt, _id: Math.round(Math.random() * 1000000), @@ -225,62 +159,120 @@ export default class App extends Component { } else { console.warn('replies param is not set correctly') } - } + }, []) - renderQuickReplySend = () => {' custom send =>'} + const renderQuickReplySend = useCallback(() => { + return {' custom send =>'} + }, []) - renderSend = (props: SendProps) => ( - - - + const setIsTyping = useCallback( + (isTyping: boolean) => { + dispatch({ type: ActionKind.SET_IS_TYPING, payload: isTyping }) + }, + [dispatch], ) - render() { - if (!this.state.appIsReady) { - return - } + 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 ( - - - - alert(JSON.stringify(user))} - onPressAvatar={() => alert('short press')} - onPress={() => { - Alert.alert('Bubble pressed') - }} - onQuickReply={this.onQuickReply} - keyboardShouldPersistTaps='never' - renderAccessory={Platform.OS === 'web' ? null : this.renderAccessory} - renderActions={this.renderCustomActions} - renderBubble={this.renderBubble} - renderSystemMessage={this.renderSystemMessage} - renderCustomView={this.renderCustomView} - renderSend={this.renderSend} - quickReplyStyle={{ borderRadius: 2 }} - quickReplyTextStyle={{ - fontWeight: '200', - }} - renderQuickReplySend={this.renderQuickReplySend} - inverted={Platform.OS !== 'web'} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} - isTyping={this.state.isTyping} - infiniteScroll - /> - - + setIsTyping(true)} + /> ) - } + }, [onSendFromUser, setIsTyping]) + + 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 styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#f5f5f5' }, + content: { backgroundColor: '#ffffff', flex: 1 }, +}) + +export default App diff --git a/example/example-expo/CustomActions.tsx b/example/example-expo/CustomActions.tsx index b2829ad7f..baa33abc9 100644 --- a/example/example-expo/CustomActions.tsx +++ b/example/example-expo/CustomActions.tsx @@ -1,21 +1,41 @@ import PropTypes from 'prop-types' -import React from 'react' +import React, { useCallback } from 'react' import { + StyleProp, StyleSheet, Text, + TextStyle, TouchableOpacity, View, ViewPropTypes, + ViewStyle, } from 'react-native' +import { useActionSheet } from '@expo/react-native-action-sheet' import { getLocationAsync, pickImageAsync, takePictureAsync, } from './mediaUtils' -export default class CustomActions extends React.Component { - onActionsPress = () => { +interface Props { + renderIcon?: () => React.ReactNode + wrapperStyle?: StyleProp + containerStyle?: StyleProp + iconTextStyle?: StyleProp + onSend: (messages: any) => void +} + +const CustomActions = ({ + renderIcon, + iconTextStyle, + containerStyle, + wrapperStyle, + onSend, +}: Props) => { + const { showActionSheetWithOptions } = useActionSheet() + + const onActionsPress = useCallback(() => { const options = [ 'Choose From Library', 'Take Picture', @@ -23,13 +43,12 @@ export default class CustomActions extends React.Component { 'Cancel', ] const cancelButtonIndex = options.length - 1 - this.context.actionSheet().showActionSheetWithOptions( + showActionSheetWithOptions( { options, cancelButtonIndex, }, async buttonIndex => { - const { onSend } = this.props switch (buttonIndex) { case 0: pickImageAsync(onSend) @@ -43,31 +62,31 @@ export default class CustomActions extends React.Component { } }, ) - } + }, [showActionSheetWithOptions]) - renderIcon = () => { - if (this.props.renderIcon) { - return this.props.renderIcon() + const renderIconComponent = useCallback(() => { + if (renderIcon) { + return renderIcon() } return ( - - + + + + ) - } + }, []) - render() { - return ( - - {this.renderIcon()} - - ) - } + return ( + + <>{renderIconComponent()} + + ) } +export default CustomActions + const styles = StyleSheet.create({ container: { width: 26, diff --git a/src/GiftedChat.tsx b/src/GiftedChat.tsx index 769739b0a..9b1623662 100644 --- a/src/GiftedChat.tsx +++ b/src/GiftedChat.tsx @@ -6,7 +6,7 @@ import { import dayjs from 'dayjs' import localizedFormat from 'dayjs/plugin/localizedFormat' import PropTypes from 'prop-types' -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { createRef, useEffect, useMemo, useRef, useState } from 'react' import { Animated, FlatList, @@ -53,6 +53,8 @@ import * as utils from './utils' dayjs.extend(localizedFormat) export interface GiftedChatProps { + /* Message container ref */ + messageContainerRef?: React.RefObject> /* Messages to display */ messages?: TMessage[] /* Typing Indicator state */ @@ -256,6 +258,7 @@ function GiftedChat( inverted = true, minComposerHeight = MIN_COMPOSER_HEIGHT, maxComposerHeight = MAX_COMPOSER_HEIGHT, + messageContainerRef = createRef>(), } = props const isMountedRef = useRef(false) @@ -264,7 +267,7 @@ function GiftedChat( const maxHeightRef = useRef(undefined) const isFirstLayoutRef = useRef(true) const actionSheetRef = useRef(null) - const messageContainerRef = useRef>(null) + let _isTextInputWasFocused: boolean = false let textInputRef = useRef()