mirror of
https://github.com/zhigang1992/react-native-gifted-chat.git
synced 2026-01-12 17:42:27 +08:00
Merge branch 'master' into example-pusher-chatkit
This commit is contained in:
@@ -4,3 +4,4 @@ ios
|
||||
example
|
||||
example-slack-message
|
||||
example-pusher-chatkit
|
||||
example-expo
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ TODO.md
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
*.log
|
||||
Exponent-*.app
|
||||
*.log
|
||||
|
||||
@@ -6,4 +6,7 @@ TODO.md
|
||||
screenshots/
|
||||
.babelrc
|
||||
tests/
|
||||
README.md
|
||||
ISSUE_TEMPLATE.md
|
||||
circle.yml
|
||||
codecov.yml
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": [".git", "node_modules", "example"]
|
||||
"ignore_dirs": [".git", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
example-expo/assets/avatar.png
Normal file
BIN
example-expo/assets/avatar.png
Normal file
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 |
@@ -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,
|
||||
|
||||
3
example-expo/e2e/config.json
Normal file
3
example-expo/e2e/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"setupTestFrameworkScriptFile": "./init.js"
|
||||
}
|
||||
52
example-expo/e2e/e2e.spec.js
Normal file
52
example-expo/e2e/e2e.spec.js
Normal 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
12
example-expo/e2e/init.js
Normal 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
22
example-expo/e2e/init.sh
Executable 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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
10
index.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
/>
|
||||
`;
|
||||
|
||||
11
src/__tests__/utils.test.js
Normal file
11
src/__tests__/utils.test.js
Normal 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);
|
||||
});
|
||||
17
src/utils.js
17
src/utils.js
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user