From ebe366d90f03450ce03b7ecb6119e8c9fffded97 Mon Sep 17 00:00:00 2001 From: Xavier Carpentier Date: Fri, 8 May 2020 15:25:14 +0200 Subject: [PATCH 01/21] feat init rewrite --- package.json | 4 +- src/Bubble.tsx | 4 +- src/Day.tsx | 101 +++++++++++++--------------- src/GiftedChat.tsx | 68 ++++++++++--------- src/GiftedChatContext.ts | 12 ++++ src/InputToolbar.tsx | 16 ++--- src/Message.tsx | 4 +- src/Send.tsx | 124 +++++++++++++++++----------------- src/Time.tsx | 139 +++++++++++++++++++-------------------- tests/setup.js | 1 - yarn.lock | 5 ++ 11 files changed, 242 insertions(+), 236 deletions(-) create mode 100644 src/GiftedChatContext.ts diff --git a/package.json b/package.json index bccef8995..c9f091711 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "lint": "tslint --project .", "lint:fix": "./node_modules/.bin/tslint ./src/**/*.{ts,tsx} --fix", "tsc": "node_modules/.bin/tsc --noEmit", + "tsc:watch": "node_modules/.bin/tsc --watch --noEmit", "start": "yarn config:dev && expo start", "start:web": "yarn config:dev && expo start -w --dev", "build": "rm -rf lib/ && node_modules/.bin/tsc && cp flow-typedefs/*.js.flow lib/", @@ -93,9 +94,9 @@ "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz", "react-native-maps": "0.26.1", "react-native-nav": "2.0.2", + "react-native-web": "^0.11.7", "react-native-web-maps": "0.2.0", "react-test-renderer": "16.9.0", - "react-native-web": "^0.11.7", "tslint": "6.1.2", "tslint-config-prettier": "1.18.0", "typescript": "^3.8.3" @@ -109,6 +110,7 @@ "react-native-lightbox": "^0.8.1", "react-native-parsed-text": "0.0.22", "react-native-typing-animation": "^0.1.7", + "use-memo-one": "1.1.1", "uuid": "3.4.0" }, "peerDependencies": { diff --git a/src/Bubble.tsx b/src/Bubble.tsx index 61b741762..0041479df 100644 --- a/src/Bubble.tsx +++ b/src/Bubble.tsx @@ -18,7 +18,7 @@ import MessageImage from './MessageImage' import MessageVideo from './MessageVideo' import MessageAudio from './MessageAudio' -import Time from './Time' +import { Time, TimeProps } from './Time' import Color from './Color' import { StylePropType, isSameUser, isSameDay } from './utils' @@ -157,7 +157,7 @@ export interface BubbleProps { renderMessageAudio?(props: RenderMessageAudioProps): React.ReactNode renderMessageText?(props: RenderMessageTextProps): React.ReactNode renderCustomView?(bubbleProps: BubbleProps): React.ReactNode - renderTime?(timeProps: Time['props']): React.ReactNode + renderTime?(timeProps: TimeProps): React.ReactNode renderTicks?(currentMessage: TMessage): React.ReactNode renderUsername?(): React.ReactNode renderQuickReplySend?(): React.ReactNode diff --git a/src/Day.tsx b/src/Day.tsx index 7cb9f1a2a..6228579de 100644 --- a/src/Day.tsx +++ b/src/Day.tsx @@ -1,5 +1,5 @@ +import * as React from 'react' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' import { StyleSheet, Text, @@ -12,7 +12,6 @@ import { import dayjs from 'dayjs' import Color from './Color' - import { StylePropType, isSameDay } from './utils' import { DATE_FORMAT } from './Constant' import { IMessage } from './Models' @@ -44,62 +43,50 @@ export interface DayProps { inverted?: boolean } -export default class Day< - TMessage extends IMessage = IMessage -> extends PureComponent> { - static contextTypes = { - getLocale: PropTypes.func, - } - - static defaultProps = { - currentMessage: { - createdAt: null, - }, - previousMessage: {}, - nextMessage: {}, - containerStyle: {}, - wrapperStyle: {}, - textStyle: {}, - textProps: {}, - dateFormat: DATE_FORMAT, - } - - static propTypes = { - currentMessage: PropTypes.object, - previousMessage: PropTypes.object, - nextMessage: PropTypes.object, - inverted: PropTypes.bool, - containerStyle: StylePropType, - wrapperStyle: StylePropType, - textStyle: StylePropType, - textProps: PropTypes.object, - dateFormat: PropTypes.string, +export const Day = ({ + dateFormat, + currentMessage, + previousMessage, + containerStyle, + wrapperStyle, + textStyle, +}: DayProps) => { + const { getLocale } = useChatContext() + if (currentMessage && !isSameDay(currentMessage, previousMessage!)) { + return ( + + + + {dayjs(currentMessage.createdAt) + .locale(getLocale()) + .format(dateFormat)} + + + + ) } + return null +} - render() { - const { - dateFormat, - currentMessage, - previousMessage, - containerStyle, - wrapperStyle, - textStyle, - textProps, - } = this.props +Day.defaultProps = { + currentMessage: { + createdAt: null, + }, + previousMessage: {}, + nextMessage: {}, + containerStyle: {}, + wrapperStyle: {}, + textStyle: {}, + dateFormat: DATE_FORMAT, +} - if (currentMessage && !isSameDay(currentMessage, previousMessage!)) { - return ( - - - - {dayjs(currentMessage.createdAt) - .locale(this.context.getLocale()) - .format(dateFormat)} - - - - ) - } - return null - } +Day.propTypes = { + currentMessage: PropTypes.object, + previousMessage: PropTypes.object, + nextMessage: PropTypes.object, + inverted: PropTypes.bool, + containerStyle: StylePropType, + wrapperStyle: StylePropType, + textStyle: StylePropType, + dateFormat: PropTypes.string, } diff --git a/src/GiftedChat.tsx b/src/GiftedChat.tsx index 12017f787..fa630c1b2 100644 --- a/src/GiftedChat.tsx +++ b/src/GiftedChat.tsx @@ -11,6 +11,7 @@ import { FlatList, TextStyle, KeyboardAvoidingView, + LayoutChangeEvent, } from 'react-native' import { ActionSheetProvider, @@ -29,13 +30,14 @@ import SystemMessage from './SystemMessage' import MessageImage from './MessageImage' import MessageText from './MessageText' import Composer from './Composer' -import Day from './Day' +import { Day, DayProps } from './Day' import InputToolbar from './InputToolbar' import LoadEarlier from './LoadEarlier' import Message from './Message' import MessageContainer from './MessageContainer' -import Send from './Send' -import Time from './Time' +import { Send, SendProps } from './Send' +import { GiftedChatContext } from './GiftedChatContext' +import { Time, TimeProps } from './Time' import GiftedAvatar from './GiftedAvatar' import { @@ -181,9 +183,9 @@ export interface GiftedChatProps { /* Custom view inside the bubble */ renderCustomView?(props: Bubble['props']): React.ReactNode /*Custom day above a message*/ - renderDay?(props: Day['props']): React.ReactNode + renderDay?(props: DayProps): React.ReactNode /* Custom time inside a message */ - renderTime?(props: Time['props']): React.ReactNode + renderTime?(props: TimeProps): React.ReactNode /* Custom footer component on the ListView, e.g. 'User is typing...' */ renderFooter?(): React.ReactNode /* Custom component to render in the ListView when messages are empty */ @@ -197,7 +199,7 @@ export interface GiftedChatProps { /* Custom action button on the left of the message composer */ renderActions?(props: Actions['props']): React.ReactNode /* Custom send button; you can pass children to the original Send component quite easily, for example to use a custom icon (example) */ - renderSend?(props: Send['props']): React.ReactNode + renderSend?(props: SendProps): React.ReactNode /*Custom second line of actions below the message composer */ renderAccessory?(props: InputToolbar['props']): React.ReactNode /*Callback when the Action button is pressed (if set, the default actionSheet will not be used) */ @@ -230,11 +232,6 @@ class GiftedChat extends React.Component< GiftedChatProps, GiftedChatState > { - static childContextTypes = { - actionSheet: PropTypes.func, - getLocale: PropTypes.func, - } - static defaultProps = { messages: [], messagesContainerStyle: undefined, @@ -429,13 +426,13 @@ class GiftedChat extends React.Component< } } - getChildContext() { - return { - actionSheet: - this.props.actionSheet || (() => this._actionSheetRef.getContext()), - getLocale: this.getLocale, - } - } + // getChildContext() { + // return { + // actionSheet: + // this.props.actionSheet || (() => this._actionSheetRef.getContext()), + // getLocale: this.getLocale, + // } + // } componentDidMount() { const { messages, text } = this.props @@ -822,8 +819,8 @@ class GiftedChat extends React.Component< }) } - onMainViewLayout = (e: any) => { - // fix an issue when keyboard is dismissing during the initialization + onMainViewLayout = (e: LayoutChangeEvent) => { + // TODO: fix an issue when keyboard is dismissing during the initialization const { layout } = e.nativeEvent if ( this.getMaxHeight() !== layout.height || @@ -883,18 +880,27 @@ class GiftedChat extends React.Component< if (this.state.isInitialized === true) { const { wrapInSafeArea } = this.props const Wrapper = wrapInSafeArea ? SafeAreaView : View - + const actionSheet = + this.props.actionSheet || (() => this._actionSheetRef.getContext()) + const { getLocale } = this return ( - - (this._actionSheetRef = component)} - > - - {this.renderMessages()} - {this.renderInputToolbar()} - - - + + + (this._actionSheetRef = component)} + > + + {this.renderMessages()} + {this.renderInputToolbar()} + + + + ) } return ( diff --git a/src/GiftedChatContext.ts b/src/GiftedChatContext.ts new file mode 100644 index 000000000..6b5a2c4a4 --- /dev/null +++ b/src/GiftedChatContext.ts @@ -0,0 +1,12 @@ +import * as React from 'react' + +export interface IGiftedChatContext { + actionSheet?(): void + getLocale(): string +} + +export const GiftedChatContext = React.createContext({ + getLocale: () => 'en', +}) + +export const useChatContext = () => React.useContext(GiftedChatContext) diff --git a/src/InputToolbar.tsx b/src/InputToolbar.tsx index edf743e1c..c8e16a4d6 100644 --- a/src/InputToolbar.tsx +++ b/src/InputToolbar.tsx @@ -10,10 +10,11 @@ import { } from 'react-native' import Composer from './Composer' -import Send from './Send' +import { Send, SendProps } from './Send' import Actions from './Actions' import Color from './Color' import { StylePropType } from './utils' +import { IMessage } from './types' const styles = StyleSheet.create({ container: { @@ -33,23 +34,22 @@ const styles = StyleSheet.create({ }, }) -export interface InputToolbarProps { +export interface InputToolbarProps { options?: { [key: string]: any } optionTintColor?: string containerStyle?: StyleProp primaryStyle?: StyleProp accessoryStyle?: StyleProp - renderAccessory?(props: InputToolbarProps): React.ReactNode + renderAccessory?(props: InputToolbarProps): React.ReactNode renderActions?(props: Actions['props']): React.ReactNode - renderSend?(props: Send['props']): React.ReactNode + renderSend?(props: SendProps): React.ReactNode renderComposer?(props: Composer['props']): React.ReactNode onPressActionButton?(): void } -export default class InputToolbar extends React.Component< - InputToolbarProps, - { position: string } -> { +export default class InputToolbar< + TMessage extends IMessage = IMessage +> extends React.Component, { position: string }> { static defaultProps = { renderAccessory: null, renderActions: null, diff --git a/src/Message.tsx b/src/Message.tsx index 02516ca4c..566602808 100644 --- a/src/Message.tsx +++ b/src/Message.tsx @@ -5,7 +5,7 @@ import { View, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native' import Avatar from './Avatar' import Bubble from './Bubble' import SystemMessage from './SystemMessage' -import Day from './Day' +import { Day, DayProps } from './Day' import { StylePropType, isSameUser } from './utils' import { IMessage, User, LeftRightStyle } from './Models' @@ -42,7 +42,7 @@ export interface MessageProps { inverted?: boolean containerStyle?: LeftRightStyle renderBubble?(props: Bubble['props']): React.ReactNode - renderDay?(props: Day['props']): React.ReactNode + renderDay?(props: DayProps): React.ReactNode renderSystemMessage?(props: SystemMessage['props']): React.ReactNode renderAvatar?(props: Avatar['props']): React.ReactNode shouldUpdateMessage?( diff --git a/src/Send.tsx b/src/Send.tsx index 5985148b9..84b8af96d 100644 --- a/src/Send.tsx +++ b/src/Send.tsx @@ -1,5 +1,5 @@ +import * as React from 'react' import PropTypes from 'prop-types' -import React, { Component } from 'react' import { StyleSheet, Text, @@ -10,6 +10,8 @@ import { TextStyle, TouchableOpacityProps, } from 'react-native' +import { useCallbackOne, useMemoOne } from 'use-memo-one' + import Color from './Color' import { IMessage } from './Models' import { StylePropType } from './utils' @@ -45,69 +47,69 @@ export interface SendProps { ): void } -export default class Send< - TMessage extends IMessage = IMessage -> extends Component> { - static defaultProps = { - text: '', - onSend: () => {}, - label: 'Send', - containerStyle: {}, - textStyle: {}, - children: null, - alwaysShowSend: false, - disabled: false, - sendButtonProps: null, - } - - static propTypes = { - text: PropTypes.string, - onSend: PropTypes.func, - label: PropTypes.string, - containerStyle: StylePropType, - textStyle: StylePropType, - children: PropTypes.element, - alwaysShowSend: PropTypes.bool, - disabled: PropTypes.bool, - sendButtonProps: PropTypes.object, - } - - handleOnPress = () => { - const { text, onSend } = this.props +export const Send = ({ + text, + containerStyle, + children, + textStyle, + label, + alwaysShowSend, + disabled, + sendButtonProps, + onSend, +}: SendProps) => { + const handleOnPress = useCallbackOne(() => { if (text && onSend) { onSend({ text: text.trim() } as Partial, true) } - } + }, [text, onSend]) - render() { - const { - text, - containerStyle, - children, - textStyle, - label, - alwaysShowSend, - disabled, - sendButtonProps, - } = this.props - if (alwaysShowSend || (text && text.trim().length > 0)) { - return ( - - - {children || {label}} - - - ) - } - return + const showSend = useMemoOne( + () => alwaysShowSend || (text && text.trim().length > 0), + [alwaysShowSend, test], + ) + + if (showSend) { + return ( + + + {children || {label}} + + + ) } + return +} + +Send.defaultProps = { + text: '', + onSend: () => {}, + label: 'Send', + containerStyle: {}, + textStyle: {}, + children: null, + alwaysShowSend: false, + disabled: false, + sendButtonProps: null, +} + +Send.propTypes = { + text: PropTypes.string, + onSend: PropTypes.func, + label: PropTypes.string, + containerStyle: StylePropType, + textStyle: StylePropType, + children: PropTypes.element, + alwaysShowSend: PropTypes.bool, + disabled: PropTypes.bool, + sendButtonProps: PropTypes.object, } diff --git a/src/Time.tsx b/src/Time.tsx index c6d00f74a..fd9afdc54 100644 --- a/src/Time.tsx +++ b/src/Time.tsx @@ -1,5 +1,5 @@ +import * as React from 'react' import PropTypes from 'prop-types' -import React, { Component } from 'react' import { StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native' import dayjs from 'dayjs' @@ -7,18 +7,22 @@ import Color from './Color' import { TIME_FORMAT } from './Constant' import { LeftRightStyle, IMessage } from './Models' import { StylePropType } from './utils' +import { useChatContext } from './GiftedChatContext' -const containerStyle = { - marginLeft: 10, - marginRight: 10, - marginBottom: 5, -} - -const textStyle = { - fontSize: 10, - backgroundColor: 'transparent', - textAlign: 'right', -} +const { containerStyle } = StyleSheet.create({ + containerStyle: { + marginLeft: 10, + marginRight: 10, + marginBottom: 5, + }, +}) +const { textStyle } = StyleSheet.create({ + textStyle: { + fontSize: 10, + backgroundColor: 'transparent', + textAlign: 'right', + }, +}) const styles = { left: StyleSheet.create({ @@ -49,69 +53,58 @@ export interface TimeProps { timeFormat?: string } -export default class Time< - TMessage extends IMessage = IMessage -> extends Component> { - static contextTypes = { - getLocale: PropTypes.func, - } - - static defaultProps = { - position: 'left', - currentMessage: { - createdAt: null, - }, - containerStyle: {}, - timeFormat: TIME_FORMAT, - timeTextStyle: {}, - } - - static propTypes = { - position: PropTypes.oneOf(['left', 'right']), - currentMessage: PropTypes.object, - containerStyle: PropTypes.shape({ - left: StylePropType, - right: StylePropType, - }), - timeFormat: PropTypes.string, - timeTextStyle: PropTypes.shape({ - left: StylePropType, - right: StylePropType, - }), - } - - render() { - const { - position, - containerStyle, - currentMessage, - timeFormat, - timeTextStyle, - } = this.props - - if (!!currentMessage) { - return ( - ({ + position, + containerStyle, + currentMessage, + timeFormat, + timeTextStyle, +}: TimeProps) => { + const { getLocale } = useChatContext() + if (!!currentMessage) { + return ( + + - - {dayjs(currentMessage.createdAt) - .locale(this.context.getLocale()) - .format(timeFormat)} - - - ) - } - return null + {dayjs(currentMessage.createdAt) + .locale(getLocale()) + .format(timeFormat)} + + + ) } + return null +} + +Time.defaultProps = { + position: 'left', + currentMessage: { + createdAt: null, + }, + containerStyle: {}, + timeFormat: TIME_FORMAT, + timeTextStyle: {}, +} + +Time.propTypes = { + position: PropTypes.oneOf(['left', 'right']), + currentMessage: PropTypes.object, + containerStyle: PropTypes.shape({ + left: StylePropType, + right: StylePropType, + }), + timeFormat: PropTypes.string, + timeTextStyle: PropTypes.shape({ + left: StylePropType, + right: StylePropType, + }), } diff --git a/tests/setup.js b/tests/setup.js index fef7e4d50..0398aca0a 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,3 +1,2 @@ // mocks - jest.mock('@expo/react-native-action-sheet', () => 'ActionSheet') diff --git a/yarn.lock b/yarn.lock index 1e32d5f02..87cfe6c9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7325,6 +7325,11 @@ url-parse@^1.4.4: querystringify "^2.1.1" requires-port "^1.0.0" +use-memo-one@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 6c7493099c9b7928e23cbc55dd8c53d31f0c52eb Mon Sep 17 00:00:00 2001 From: Xavier Carpentier Date: Fri, 8 May 2020 15:46:39 +0200 Subject: [PATCH 02/21] fix remove old fashion context from test too --- src/GiftedChat.tsx | 8 --- src/__tests__/Bubble.test.tsx | 26 +++++----- src/__tests__/Day.test.tsx | 4 +- src/__tests__/Message.test.tsx | 94 ++++++++++++++++++---------------- src/__tests__/Time.test.tsx | 4 +- src/__tests__/context.tsx | 29 ----------- 6 files changed, 68 insertions(+), 97 deletions(-) delete mode 100644 src/__tests__/context.tsx diff --git a/src/GiftedChat.tsx b/src/GiftedChat.tsx index fa630c1b2..b5d0c956e 100644 --- a/src/GiftedChat.tsx +++ b/src/GiftedChat.tsx @@ -426,14 +426,6 @@ class GiftedChat extends React.Component< } } - // getChildContext() { - // return { - // actionSheet: - // this.props.actionSheet || (() => this._actionSheetRef.getContext()), - // getLocale: this.getLocale, - // } - // } - componentDidMount() { const { messages, text } = this.props this.setIsMounted(true) diff --git a/src/__tests__/Bubble.test.tsx b/src/__tests__/Bubble.test.tsx index e175b43fa..9e2e37a89 100644 --- a/src/__tests__/Bubble.test.tsx +++ b/src/__tests__/Bubble.test.tsx @@ -1,21 +1,23 @@ import 'react-native' import React from 'react' -import createComponentWithContext from './context' +import renderer from 'react-test-renderer' import { Bubble } from '../GiftedChat' it('should render and compare with snapshot', () => { - const tree = createComponentWithContext( - , - ).toJSON() + const tree = renderer + .create( + + user={{ _id: 1 }} + currentMessage={{ + _id: 1, + text: 'test', + createdAt: 1554744013721, + user: { _id: 1 }, + }} + />, + ) + .toJSON() expect(tree).toMatchSnapshot() }) diff --git a/src/__tests__/Day.test.tsx b/src/__tests__/Day.test.tsx index 02106f166..0006cb884 100644 --- a/src/__tests__/Day.test.tsx +++ b/src/__tests__/Day.test.tsx @@ -1,11 +1,11 @@ import 'react-native' import React from 'react' -import createComponentWithContext from './context' +import renderer from 'react-test-renderer' import { Day } from '../GiftedChat' it('should render and compare with snapshot', () => { - const component = createComponentWithContext() + const component = renderer.create() const tree = component.toJSON() expect(tree).toMatchSnapshot() diff --git a/src/__tests__/Message.test.tsx b/src/__tests__/Message.test.tsx index f1a9403c7..cc1429d02 100644 --- a/src/__tests__/Message.test.tsx +++ b/src/__tests__/Message.test.tsx @@ -1,70 +1,76 @@ import 'react-native' import React from 'react' -import createComponentWithContext from './context' +import renderer from 'react-test-renderer' import { Message } from '../GiftedChat' describe('Message component', () => { it('should render and compare with snapshot', () => { - const tree = createComponentWithContext( - , - ).toJSON() + const tree = renderer + .create( + , + ) + .toJSON() expect(tree).toMatchSnapshot() }) it('should NOT render ', () => { - const tree = createComponentWithContext( - , - ).toJSON() + const tree = renderer + .create() + .toJSON() expect(tree).toMatchSnapshot() }) it('should render with Avatar', () => { - const tree = createComponentWithContext( - , - ).toJSON() + const tree = renderer + .create( + , + ) + .toJSON() expect(tree).toMatchSnapshot() }) it('should render null if user has no Avatar', () => { - const tree = createComponentWithContext( - , - ).toJSON() + text: 'test', + createdAt: 1554744013721, + user: { + _id: 1, + avatar: null, + }, + }} + showUserAvatar + />, + ) + .toJSON() expect(tree).toMatchSnapshot() }) diff --git a/src/__tests__/Time.test.tsx b/src/__tests__/Time.test.tsx index e1c1ae13f..aaaef170e 100644 --- a/src/__tests__/Time.test.tsx +++ b/src/__tests__/Time.test.tsx @@ -1,11 +1,11 @@ import 'react-native' import React from 'react' -import createComponentWithContext from './context' +import renderer from 'react-test-renderer' import { Time } from '../GiftedChat' it('should render