Merge branch 'master' into example-pusher-chatkit

This commit is contained in:
Farid Safi
2018-06-05 11:55:40 +02:00
committed by GitHub
37 changed files with 2895 additions and 1048 deletions

View File

@@ -4,3 +4,4 @@ ios
example
example-slack-message
example-pusher-chatkit
example-expo

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ TODO.md
.idea
.vscode
*.log
Exponent-*.app
*.log

View File

@@ -6,4 +6,7 @@ TODO.md
screenshots/
.babelrc
tests/
README.md
ISSUE_TEMPLATE.md
circle.yml
codecov.yml

View File

@@ -3,8 +3,8 @@ node_js:
- "node"
cache: false
before_install:
- yarn global add exp
- yarn global add exp@50.0.0
script:
- 'export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo "$TRAVIS_REPO_SLUG"; else echo "$TRAVIS_PULL_REQUEST_SLUG#$TRAVIS_PULL_REQUEST_BRANCH"; fi)'
- 'echo "BRANCH=$BRANCH"'
- 'export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then yarn deploy; else yarn appr; fi)'
- 'export BRANCH="$TRAVIS_PULL_REQUEST_SLUG#$TRAVIS_PULL_REQUEST_BRANCH"'
- 'echo $BRANCH'
- 'if [ "$TRAVIS_PULL_REQUEST_SLUG" != "FaridSafi/react-native-gifted-chat" ]; then echo "No appr because external PR"; else yarn appr; fi'

View File

@@ -1,3 +1,3 @@
{
"ignore_dirs": [".git", "node_modules", "example"]
"ignore_dirs": [".git", "node_modules"]
}

View File

@@ -1,14 +1,16 @@
/* eslint no-alert: 0, jsx-a11y/accessible-emoji: 0 */
import React, { Component } from 'react';
import { Asset, AppLoading } from 'expo';
import { View, StyleSheet, Linking } from 'react-native';
import { GiftedChat } from 'react-native-gifted-chat';
import Sentry from 'sentry-expo';
import messagesData from './data';
import NavBar from './NavBar';
import CustomView from './CustomView';
Sentry.config('https://2a164b1e89424a5aafc186da811308cb@sentry.io/276804').install();
const styles = StyleSheet.create({
container: { flex: 1 },
});
@@ -20,18 +22,21 @@ export default class App extends Component {
constructor(props) {
super(props);
this.state = {
messages: [],
step: 0,
appIsReady: false,
};
this.onSend = this.onSend.bind(this);
this.parsePatterns = this.parsePatterns.bind(this);
}
componentWillMount() {
async componentWillMount() {
// init with only system messages
this.setState({ messages: messagesData.filter((message) => message.system) });
await Asset.fromModule(require('./assets/avatar.png')).downloadAsync();
this.setState({ messages: messagesData.filter((message) => message.system), appIsReady: true });
}
onSend(messages = []) {
@@ -40,7 +45,7 @@ export default class App extends Component {
messages: GiftedChat.append(previousState.messages, [{ ...messages[0], sent: true, received: true }]),
step,
}));
setTimeout(() => this.botSend(step), 1500 + Math.round(Math.random() * 1000));
setTimeout(() => this.botSend(step), 1200 + Math.round(Math.random() * 1000));
}
botSend(step = 0) {
@@ -59,19 +64,23 @@ export default class App extends Component {
return [
{
pattern: /#(\w+)/,
style: { ...linkStyle, color: 'orange' },
style: { ...linkStyle, color: 'darkorange' },
onPress: () => Linking.openURL('http://gifted.chat'),
},
];
}
render() {
if (!this.state.appIsReady) {
return <AppLoading />;
}
return (
<View style={styles.container}>
<View style={styles.container} accessible accessibilityLabel="main" testID="main">
<NavBar />
<GiftedChat
messages={this.state.messages}
onSend={this.onSend}
renderCustomView={CustomView}
keyboardShouldPersistTaps="never"
user={{
_id: 1,
}}

View File

@@ -29,7 +29,6 @@ export default function CustomView(props) {
zoomEnabled={false}
>
<MapView.Marker
pinColor={'#fff'}
coordinate={{
latitude: props.currentMessage.location.latitude,
longitude: props.currentMessage.location.longitude,

View File

@@ -1,12 +1,17 @@
/* eslint jsx-a11y/accessible-emoji: 0 */
import React from 'react';
import { Text } from 'react-native';
import NavBar, { NavTitle, NavButton } from 'react-native-nav';
import app from './app.json';
export default function NavBarCustom() {
return (
<NavBar>
<NavButton />
<NavTitle>💬 Gifted Chat</NavTitle>
<NavTitle>
💬 Gifted Chat{'\n'}
<Text style={{ fontSize: 10, color: '#aaa' }}>({app.expo.version})</Text>
</NavTitle>
<NavButton />
</NavBar>
);

View File

@@ -1,12 +1,12 @@
{
"expo": {
"name": "example-expo",
"description": "This project is really great.",
"name": "gifted-chat-example",
"description": "Gifted Chat Expo Example",
"slug": "example-expo",
"privacy": "public",
"sdkVersion": "24.0.0",
"sdkVersion": "25.0.0",
"platforms": ["ios", "android"],
"version": "1.0.0",
"version": "0.4.1",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
@@ -19,6 +19,18 @@
},
"androidStatusBar": {
"backgroundColor": "#000000"
},
"hooks": {
"postPublish": [
{
"file": "sentry-expo/upload-sourcemaps",
"config": {
"organization": "xavier-carpentier-sas",
"project": "giftedchat",
"authToken": "d32ac87517964ac2b5b778bf5c7b544e59dcab60d1df4fafb6fab65a9d8019ff"
}
}
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -15,6 +15,7 @@ export default [
user: {
_id: 2,
name: 'React Native',
avatar: require('./assets/avatar.png'),
},
image: 'https://lh3.googleusercontent.com/-uXipYA5hSKc/VVWKiFIvo-I/AAAAAAAAAhQ/vkjLyZNEzUA/w800-h800/1.jpg',
sent: true,
@@ -36,6 +37,7 @@ export default [
user: {
_id: 2,
name: 'React Native',
avatar: require('./assets/avatar.png'),
},
sent: true,
received: true,
@@ -60,6 +62,7 @@ export default [
user: {
_id: 2,
name: 'React Native',
avatar: require('./assets/avatar.png'),
},
sent: true,
received: true,

View File

@@ -0,0 +1,3 @@
{
"setupTestFrameworkScriptFile": "./init.js"
}

View File

@@ -0,0 +1,52 @@
const { reloadApp } = require('detox-expo-helpers');
const composerId = 'Type a message...';
const sendId = 'send';
const timeout = 3000;
async function expectTypeText(text) {
await waitFor(element(by.id(composerId)))
.toBeVisible()
.withTimeout(timeout);
await element(by.id(composerId)).tap();
await element(by.id(composerId)).typeText(text);
await waitFor(element(by.id(sendId)))
.toBeVisible()
.withTimeout(timeout);
await element(by.id(sendId)).tap();
await waitFor(element(by.text(text)))
.toBeVisible()
.withTimeout(timeout);
}
describe('GiftedChat', () => {
beforeAll(async () => {
await reloadApp();
});
it('should have main screen', async () => {
await waitFor(element(by.id('main')))
.toBeVisible()
.withTimeout(timeout);
});
it('should type text 1', async () => {
await expectTypeText('Are you building a chat app?');
});
it('should type text 2', async () => {
await expectTypeText('Where are you?');
});
it('should type text 3', async () => {
await expectTypeText('Send me a picture!');
});
it('should type text 4', async () => {
await expectTypeText('#awesome !!!');
});
it('should type text 5', async () => {
await expectTypeText("Will *Star GiftedChat's repo!");
});
});

12
example-expo/e2e/init.js Normal file
View File

@@ -0,0 +1,12 @@
const detox = require('detox');
const config = require('../package.json').detox;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500000;
beforeAll(async () => {
await detox.init(config);
});
afterAll(async () => {
await detox.cleanup();
});

22
example-expo/e2e/init.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
if [[ -z "$1" ]]
then
echo "No arguments supplied!"
echo "Please specified expo versions..."
versions=`ls -l ~/.expo/ios-simulator-app-cache | rev | cut -d' ' -f 1 | rev | grep Exponent | cut -d'-' -f 2 | tr ap " "`
echo "${versions}"
exit 1
fi
rm -rf e2e/Exponent-*.app
DEST="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../e2e/"
EXPO_APP_PATH="$HOME/.expo/ios-simulator-app-cache/Exponent-$1.app"
echo "Copy file from $EXPO_APP_PATH to $DEST"
cp -r $EXPO_APP_PATH $DEST
exit 0

View File

@@ -1,18 +1,37 @@
{
"name": "react-native-gifted-chat-expo",
"main": "node_modules/expo/AppEntry.js",
"version": "0.4.1",
"private": true,
"scripts": {
"cleaning": "watchman watch-del-all && rm -f yarn.lock && rm -rf node_modules && rm -rf $TMPDIR/react-* && yarn cache clean"
"e2e:init": "./e2e/init.sh 2.3.0",
"test:e2e": "exp r -c & detox test -c ios.sim; pkill -f exp",
"cleaning":
"watchman watch-del-all && rm -f yarn.lock && rm -rf node_modules && rm -rf $TMPDIR/react-* && yarn cache clean"
},
"dependencies": {
"expo": "^24.0.0",
"react": "16.0.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-24.0.0.tar.gz",
"react-native-gifted-chat": "https://github.com/FaridSafi/react-native-gifted-chat.git",
"react-native-nav": "2.0.2"
"expo": "^25.0.0",
"react": "16.2.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-25.0.0.tar.gz",
"react-native-gifted-chat": "FaridSafi/react-native-gifted-chat",
"react-native-nav": "2.0.2",
"sentry-expo": "1.7.0"
},
"devDependencies": {
"appr": "xcarpentier/appr"
"appr": "xcarpentier/appr",
"detox": "7.0.0-alpha.1",
"detox-expo-helpers": "0.2.0",
"jest": "22.1.4"
},
"detox": {
"test-runner": "jest",
"runner-config": "e2e/config.json",
"configurations": {
"ios.sim": {
"binaryPath": "./e2e/Exponent-2.3.0.app",
"type": "ios.simulator",
"name": "iPhone 7"
}
}
}
}

View File

@@ -2,11 +2,15 @@
/* eslint-disable */
const fs = require('fs')
const pkg = require('./package.json')
const fs = require('fs');
const pkg = require('./package.json');
const app = require('./app.json');
const { TRAVIS_BUILD_NUMBER, BRANCH } = process.env;
pkg.dependencies['react-native-gifted-chat'] = process.env.BRANCH
pkg.dependencies['react-native-gifted-chat'] = BRANCH;
app.expo['version'] = `${pkg.version}.${TRAVIS_BUILD_NUMBER || 'dev'}`;
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2), 'utf8')
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 1), 'utf8');
fs.writeFileSync('./app.json', JSON.stringify(app, null, 1), 'utf8');
process.exit()
process.exit();

File diff suppressed because it is too large Load Diff

10
index.d.ts vendored
View File

@@ -86,9 +86,9 @@ interface BubbleProps {
bottomContainerStyle: LeftRightStyle<ViewStyle>;
tickStyle: TextStyle;
containerToNextStyle: LeftRightStyle<ViewStyle>;
containertoPreviousStyle: LeftRightStyle<ViewStyle>;
containerToPreviousStyle: LeftRightStyle<ViewStyle>;
// TODO: remove in next major release
isSameDay?(currentMessage: IMessage, inextMessage: IMessage): boolean;
isSameDay?(currentMessage: IMessage, nextMessage: IMessage): boolean;
isSameUser?(currentMessage: IMessage, nextMessage: IMessage): boolean;
}
@@ -98,7 +98,7 @@ interface ComposerProps {
composerHeight?: number;
text?: string;
placeholder?: string;
placeholderTextCoolor?: string;
placeholderTextColor?: string;
textInputProps?: Partial<TextInputProperties>;
onTextChanged?(text: string): void;
onInputSizeChanged?(contentSize: number): void;
@@ -117,7 +117,7 @@ interface DayProps {
wrapperStyle?: ViewStyle;
textStyle?: TextStyle;
// TODO: remove in next major release
isSameDay?(currentMessage: IMessage, inextMessage: IMessage): boolean;
isSameDay?(currentMessage: IMessage, nextMessage: IMessage): boolean;
isSameUser?(currentMessage: IMessage, nextMessage: IMessage): boolean;
dateFormat?: string;
}
@@ -359,7 +359,7 @@ interface TimeProps {
export class Time extends React.Component<TimeProps> { }
export type utils = {
isSameUser(currrentMessage?: IMessage, message?: IMessage): boolean;
isSameUser(currentMessage?: IMessage, message?: IMessage): boolean;
isSameDay(currentMessage?: IMessage, message?: IMessage): boolean;
isSameTime(currentMessage?: IMessage, message?: IMessage): boolean;
};

View File

@@ -66,13 +66,10 @@
},
"dependencies": {
"@expo/react-native-action-sheet": "^1.0.1",
"md5": "2.2.1",
"moment": "^2.19.0",
"react-native-communications": "2.2.1",
"react-native-invertible-scroll-view": "^1.1.0",
"react-native-lightbox": "^0.7.0",
"react-native-parsed-text": "^0.0.20",
"shallowequal": "1.0.2",
"uuid": "3.2.1"
},
"peerDependencies": {

View File

@@ -1,12 +1,41 @@
/* eslint no-use-before-define: ["error", { "variables": false }] */
import PropTypes from 'prop-types';
import React from 'react';
import { StyleSheet, View, ViewPropTypes } from 'react-native';
import GiftedAvatar from './GiftedAvatar';
import { isSameUser, isSameDay, warnDeprecated } from './utils';
import { isSameUser, isSameDay } from './utils';
export default class Avatar extends React.Component {
const styles = {
left: StyleSheet.create({
container: {
marginRight: 8,
},
onTop: {
alignSelf: 'flex-start',
},
onBottom: {},
image: {
height: 36,
width: 36,
borderRadius: 18,
},
}),
right: StyleSheet.create({
container: {
marginLeft: 8,
},
onTop: {
alignSelf: 'flex-start',
},
onBottom: {},
image: {
height: 36,
width: 36,
borderRadius: 18,
},
}),
};
export default class Avatar extends React.PureComponent {
renderAvatar() {
if (this.props.renderAvatar) {
@@ -66,37 +95,6 @@ export default class Avatar extends React.Component {
}
const styles = {
left: StyleSheet.create({
container: {
marginRight: 8,
},
onTop: {
alignSelf: 'flex-start',
},
onBottom: {},
image: {
height: 36,
width: 36,
borderRadius: 18,
},
}),
right: StyleSheet.create({
container: {
marginLeft: 8,
},
onTop: {
alignSelf: 'flex-start',
},
onBottom: {},
image: {
height: 36,
width: 36,
borderRadius: 18,
},
}),
};
Avatar.defaultProps = {
renderAvatarOnTop: false,
showAvatarForEveryMessage: false,
@@ -109,9 +107,6 @@ Avatar.defaultProps = {
containerStyle: {},
imageStyle: {},
onPressAvatar: () => {},
// TODO: remove in next major release
isSameDay: warnDeprecated(isSameDay),
isSameUser: warnDeprecated(isSameUser),
};
Avatar.propTypes = {
@@ -131,7 +126,4 @@ Avatar.propTypes = {
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
// TODO: remove in next major release
isSameDay: PropTypes.func,
isSameUser: PropTypes.func,
};

View File

@@ -2,23 +2,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Text,
Clipboard,
StyleSheet,
TouchableWithoutFeedback,
View,
ViewPropTypes,
} from 'react-native';
import { Text, Clipboard, StyleSheet, TouchableWithoutFeedback, View, ViewPropTypes } from 'react-native';
import MessageText from './MessageText';
import MessageImage from './MessageImage';
import Time from './Time';
import Color from './Color';
import { isSameUser, isSameDay, warnDeprecated } from './utils';
import { isSameUser, isSameDay } from './utils';
export default class Bubble extends React.Component {
export default class Bubble extends React.PureComponent {
constructor(props) {
super(props);
@@ -136,12 +129,7 @@ export default class Bubble extends React.Component {
render() {
return (
<View
style={[
styles[this.props.position].container,
this.props.containerStyle[this.props.position],
]}
>
<View style={[styles[this.props.position].container, this.props.containerStyle[this.props.position]]}>
<View
style={[
styles[this.props.position].wrapper,
@@ -252,9 +240,6 @@ Bubble.defaultProps = {
tickStyle: {},
containerToNextStyle: {},
containerToPreviousStyle: {},
// TODO: remove in next major release
isSameDay: warnDeprecated(isSameDay),
isSameUser: warnDeprecated(isSameUser),
};
Bubble.propTypes = {
@@ -291,7 +276,4 @@ Bubble.propTypes = {
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
// TODO: remove in next major release
isSameDay: PropTypes.func,
isSameUser: PropTypes.func,
};

View File

@@ -32,6 +32,9 @@ export default class Composer extends React.Component {
render() {
return (
<TextInput
testID={this.props.placeholder}
accessible
accessibilityLabel={this.props.placeholder}
placeholder={this.props.placeholder}
placeholderTextColor={this.props.placeholderTextColor}
multiline={this.props.multiline}
@@ -41,7 +44,6 @@ export default class Composer extends React.Component {
style={[styles.textInput, this.props.textInputStyle, { height: this.props.composerHeight }]}
autoFocus={this.props.textInputAutoFocus}
value={this.props.text}
accessibilityLabel={this.props.text || this.props.placeholder}
enablesReturnKeyAutomatically
underlineColorAndroid="transparent"
keyboardAppearance={this.props.keyboardAppearance}

View File

@@ -7,7 +7,7 @@ import moment from 'moment';
import Color from './Color';
import { isSameDay, isSameUser, warnDeprecated } from './utils';
import { isSameDay } from './utils';
import { DATE_FORMAT } from './Constant';
export default function Day(
@@ -60,9 +60,6 @@ Day.defaultProps = {
containerStyle: {},
wrapperStyle: {},
textStyle: {},
// TODO: remove in next major release
isSameDay: warnDeprecated(isSameDay),
isSameUser: warnDeprecated(isSameUser),
dateFormat: DATE_FORMAT,
};
@@ -74,8 +71,5 @@ Day.propTypes = {
containerStyle: ViewPropTypes.style,
wrapperStyle: ViewPropTypes.style,
textStyle: Text.propTypes.style,
// TODO: remove in next major release
isSameDay: PropTypes.func,
isSameUser: PropTypes.func,
dateFormat: PropTypes.string,
};

View File

@@ -33,15 +33,19 @@ export default class InputToolbar extends React.Component {
}
keyboardWillShow() {
this.setState({
position: 'relative',
});
if (this.state !== 'relative') {
this.setState({
position: 'relative',
});
}
}
keyboardWillHide() {
this.setState({
position: 'absolute',
});
if (this.state !== 'absolute') {
this.setState({
position: 'absolute',
});
}
}
renderActions() {
@@ -71,9 +75,7 @@ export default class InputToolbar extends React.Component {
renderAccessory() {
if (this.props.renderAccessory) {
return (
<View style={[styles.accessory, this.props.accessoryStyle]}>
{this.props.renderAccessory(this.props)}
</View>
<View style={[styles.accessory, this.props.accessoryStyle]}>{this.props.renderAccessory(this.props)}</View>
);
}
return null;
@@ -81,9 +83,7 @@ export default class InputToolbar extends React.Component {
render() {
return (
<View
style={[styles.container, this.props.containerStyle, { position: this.state.position }]}
>
<View style={[styles.container, this.props.containerStyle, { position: this.state.position }]}>
<View style={[styles.primary, this.props.primaryStyle]}>
{this.renderActions()}
{this.renderComposer()}

View File

@@ -1,4 +1,4 @@
/* eslint no-use-before-define: ["error", { "variables": false }], react-native/no-inline-styles: 0 */
/* eslint react-native/no-inline-styles: 0 */
import PropTypes from 'prop-types';
import React from 'react';
@@ -11,7 +11,28 @@ import Day from './Day';
import { isSameUser, isSameDay } from './utils';
export default class Message extends React.Component {
const styles = {
left: StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'flex-start',
marginLeft: 8,
marginRight: 0,
},
}),
right: StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'flex-end',
marginLeft: 0,
marginRight: 8,
},
}),
};
export default class Message extends React.PureComponent {
getInnerComponentProps() {
const { containerStyle, ...props } = this.props;
@@ -88,27 +109,6 @@ export default class Message extends React.Component {
}
const styles = {
left: StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'flex-start',
marginLeft: 8,
marginRight: 0,
},
}),
right: StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'flex-end',
marginLeft: 0,
marginRight: 8,
},
}),
};
Message.defaultProps = {
renderAvatar: undefined,
renderBubble: null,

View File

@@ -3,21 +3,19 @@
no-param-reassign: 0,
no-use-before-define: ["error", { "variables": false }],
no-return-assign: 0,
react/no-string-refs: 0
react/no-string-refs: 0,
react/sort-comp: 0
*/
import PropTypes from 'prop-types';
import React from 'react';
import { ListView, View, StyleSheet } from 'react-native';
import { FlatList, View, StyleSheet, Platform } from 'react-native';
import shallowequal from 'shallowequal';
import InvertibleScrollView from 'react-native-invertible-scroll-view';
import md5 from 'md5';
import LoadEarlier from './LoadEarlier';
import Message from './Message';
export default class MessageContainer extends React.Component {
export default class MessageContainer extends React.PureComponent {
constructor(props) {
super(props);
@@ -25,61 +23,17 @@ export default class MessageContainer extends React.Component {
this.renderRow = this.renderRow.bind(this);
this.renderFooter = this.renderFooter.bind(this);
this.renderLoadEarlier = this.renderLoadEarlier.bind(this);
this.renderScrollComponent = this.renderScrollComponent.bind(this);
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => {
return r1.hash !== r2.hash;
},
});
const messagesData = this.prepareMessages(props.messages);
this.state = {
dataSource: dataSource.cloneWithRows(messagesData.blob, messagesData.keys),
};
this.renderHeaderWrapper = this.renderHeaderWrapper.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.messages === nextProps.messages) {
return;
renderFooter() {
if (this.props.renderFooter) {
const footerProps = {
...this.props,
};
return this.props.renderFooter(footerProps);
}
const messagesData = this.prepareMessages(nextProps.messages);
this.setState({
dataSource: this.state.dataSource.cloneWithRows(messagesData.blob, messagesData.keys),
});
}
shouldComponentUpdate(nextProps, nextState) {
if (!shallowequal(this.props, nextProps)) {
return true;
}
if (!shallowequal(this.state, nextState)) {
return true;
}
return false;
}
prepareMessages(messages) {
return {
keys: messages.map((m) => m._id),
blob: messages.reduce((o, m, i) => {
const previousMessage = messages[i + 1] || {};
const nextMessage = messages[i - 1] || {};
// add next and previous messages to hash to ensure updates
const toHash = JSON.stringify(m) + previousMessage._id + nextMessage._id;
o[m._id] = {
...m,
previousMessage,
nextMessage,
hash: md5(toHash),
};
return o;
}, {}),
};
}
scrollTo(options) {
this._invertibleScrollViewRef.scrollTo(options);
return null;
}
renderLoadEarlier() {
@@ -95,34 +49,33 @@ export default class MessageContainer extends React.Component {
return null;
}
renderFooter() {
if (this.props.renderFooter) {
const footerProps = {
...this.props,
};
return this.props.renderFooter(footerProps);
scrollTo(options) {
if (this.flatListRef) {
this.flatListRef.scrollToOffset(options);
}
return null;
}
renderRow(message) {
if (!message._id && message._id !== 0) {
console.warn('GiftedChat: `_id` is missing for message', JSON.stringify(message));
renderRow({ item, index }) {
if (!item._id && item._id !== 0) {
console.warn('GiftedChat: `_id` is missing for message', JSON.stringify(item));
}
if (!message.user) {
if (!message.system) {
console.warn('GiftedChat: `user` is missing for message', JSON.stringify(message));
if (!item.user) {
if (!item.system) {
console.warn('GiftedChat: `user` is missing for message', JSON.stringify(item));
}
message.user = {};
item.user = {};
}
const { messages, ...restProps } = this.props;
const previousMessage = messages[index + 1] || {};
const nextMessage = messages[index - 1] || {};
const messageProps = {
...this.props,
key: message._id,
currentMessage: message,
previousMessage: message.previousMessage,
nextMessage: message.nextMessage,
position: message.user._id === this.props.user._id ? 'right' : 'left',
...restProps,
key: item._id,
currentMessage: item,
previousMessage,
nextMessage,
position: item.user._id === this.props.user._id ? 'right' : 'left',
};
if (this.props.renderMessage) {
@@ -131,36 +84,32 @@ export default class MessageContainer extends React.Component {
return <Message {...messageProps} />;
}
renderScrollComponent(props) {
const { invertibleScrollViewProps } = this.props;
return (
<InvertibleScrollView
{...props}
{...invertibleScrollViewProps}
ref={(component) => (this._invertibleScrollViewRef = component)}
/>
);
renderHeaderWrapper() {
return <View style={styles.headerWrapper}>{this.renderLoadEarlier()}</View>;
}
render() {
const contentContainerStyle = this.props.inverted
? {}
: styles.notInvertedContentContainerStyle;
if (this.props.messages.length === 0) {
return <View style={styles.container} />;
}
return (
<View style={styles.container}>
<ListView
<FlatList
ref={(ref) => (this.flatListRef = ref)}
keyExtractor={(item) => item._id}
enableEmptySections
automaticallyAdjustContentInsets={false}
initialListSize={20}
pageSize={20}
removeClippedSubviews={Platform.OS === 'android'}
inverted={this.props.inverted}
{...this.props.listViewProps}
dataSource={this.state.dataSource}
contentContainerStyle={contentContainerStyle}
renderRow={this.renderRow}
renderHeader={this.props.inverted ? this.renderFooter : this.renderLoadEarlier}
renderFooter={this.props.inverted ? this.renderLoadEarlier : this.renderFooter}
renderScrollComponent={this.renderScrollComponent}
data={this.props.messages}
style={styles.listStyle}
contentContainerStyle={styles.contentContainerStyle}
renderItem={this.renderRow}
renderHeader={this.renderFooter}
renderFooter={this.renderLoadEarlier}
{...this.props.invertibleScrollViewProps}
ListFooterComponent={this.renderHeaderWrapper}
/>
</View>
);
@@ -172,9 +121,15 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
notInvertedContentContainerStyle: {
contentContainerStyle: {
justifyContent: 'flex-end',
},
headerWrapper: {
flex: 1,
},
listStyle: {
flex: 1,
},
});
MessageContainer.defaultProps = {
@@ -182,11 +137,11 @@ MessageContainer.defaultProps = {
user: {},
renderFooter: null,
renderMessage: null,
onLoadEarlier: () => { },
onLoadEarlier: () => {},
inverted: true,
loadEarlier: false,
listViewProps: {},
invertibleScrollViewProps: {},
invertibleScrollViewProps: {}, // TODO: support or not?
};
MessageContainer.propTypes = {
@@ -199,5 +154,5 @@ MessageContainer.propTypes = {
listViewProps: PropTypes.object,
inverted: PropTypes.bool,
loadEarlier: PropTypes.bool,
invertibleScrollViewProps: PropTypes.object,
invertibleScrollViewProps: PropTypes.object, // TODO: support or not?
};

View File

@@ -17,6 +17,10 @@ export default class MessageText extends React.Component {
this.onEmailPress = this.onEmailPress.bind(this);
}
shouldComponentUpdate(nextProps) {
return this.props.currentMessage.text !== nextProps.currentMessage.text;
}
onUrlPress(url) {
// When someone sends a message that includes a website address beginning with "www." (omitting the scheme),
// react-native-parsed-text recognizes it as a valid url, but Linking fails to open due to the missing scheme.
@@ -62,17 +66,9 @@ export default class MessageText extends React.Component {
}
render() {
const linkStyle = StyleSheet.flatten([
styles[this.props.position].link,
this.props.linkStyle[this.props.position],
]);
const linkStyle = StyleSheet.flatten([styles[this.props.position].link, this.props.linkStyle[this.props.position]]);
return (
<View
style={[
styles[this.props.position].container,
this.props.containerStyle[this.props.position],
]}
>
<View style={[styles[this.props.position].container, this.props.containerStyle[this.props.position]]}>
<ParsedText
style={[
styles[this.props.position].text,

View File

@@ -9,6 +9,9 @@ export default function Send({ text, containerStyle, onSend, children, textStyle
if (alwaysShowSend || text.trim().length > 0) {
return (
<TouchableOpacity
testID="send"
accessible
accessibilityLabel="send"
style={[styles.container, containerStyle]}
onPress={() => {
onSend({ text: text.trim() }, true);

View File

@@ -3,6 +3,7 @@
exports[`should render <Composer /> and compare with snapshot 1`] = `
<TextInput
accessibilityLabel="Type a message..."
accessible={true}
allowFontScaling={true}
autoFocus={false}
enablesReturnKeyAutomatically={true}
@@ -29,6 +30,7 @@ exports[`should render <Composer /> and compare with snapshot 1`] = `
},
]
}
testID="Type a message..."
underlineColorAndroid="transparent"
value=""
/>

View File

@@ -95,6 +95,7 @@ exports[`should render <InputToolbar /> and compare with snapshot 1`] = `
</View>
<TextInput
accessibilityLabel="Type a message..."
accessible={true}
allowFontScaling={true}
autoFocus={false}
enablesReturnKeyAutomatically={true}
@@ -121,6 +122,7 @@ exports[`should render <InputToolbar /> and compare with snapshot 1`] = `
},
]
}
testID="Type a message..."
underlineColorAndroid="transparent"
value=""
/>

View File

@@ -7,23 +7,5 @@ exports[`should render <MessageContainer /> and compare with snapshot 1`] = `
"flex": 1,
}
}
>
<RCTScrollView
automaticallyAdjustContentInsets={false}
contentContainerStyle={Object {}}
dataSource={
ListViewDataSource {
"items": 0,
}
}
enableEmptySections={true}
initialListSize={20}
pageSize={20}
renderFooter={[Function]}
renderHeader={[Function]}
renderRow={[Function]}
>
<View />
</RCTScrollView>
</View>
/>
`;

View File

@@ -0,0 +1,11 @@
import { isSameDay, isSameUser } from '../utils';
it('should test if same day', () => {
const now = new Date();
expect(isSameDay({ createdAt: now }, { createdAt: now })).toBe(true);
});
it('should test if same user', () => {
const message = { user: { _id: 1 } };
expect(isSameUser(message, message)).toBe(true);
});

View File

@@ -1,8 +1,5 @@
import moment from 'moment';
const DEPRECATION_MESSAGE =
'isSameUser and isSameDay should be imported from the utils module instead of using the props functions';
export function isSameDay(currentMessage = {}, diffMessage = {}) {
if (!diffMessage.createdAt) {
return false;
@@ -19,17 +16,5 @@ export function isSameDay(currentMessage = {}, diffMessage = {}) {
}
export function isSameUser(currentMessage = {}, diffMessage = {}) {
return !!(
diffMessage.user &&
currentMessage.user &&
diffMessage.user._id === currentMessage.user._id
);
}
export function warnDeprecated(fn) {
return (...args) => {
// eslint-disable-next-line
console.warn(DEPRECATION_MESSAGE);
return fn(...args);
};
return !!(diffMessage.user && currentMessage.user && diffMessage.user._id === currentMessage.user._id);
}

1602
yarn.lock

File diff suppressed because it is too large Load Diff