rn: replace 3rd party chat library with custom implementation

This commit is contained in:
Bettenbuk Zoltan 2019-04-25 15:17:49 +02:00 committed by Saúl Ibarra Corretgé
parent 1cb9bbc7a4
commit 0b6c51f666
10 changed files with 264 additions and 177 deletions

79
package-lock.json generated
View File

@ -2423,15 +2423,6 @@
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz",
"integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==" "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw=="
}, },
"@expo/react-native-action-sheet": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@expo/react-native-action-sheet/-/react-native-action-sheet-1.1.2.tgz",
"integrity": "sha512-//2EvHVBFVGSAzuJvG0I1UoQVzGJBo2f1CkO+RMnEWdR0FeWYmV7+pCThIroL1czRm/oOtoMxiGS6FgXt6QgVA==",
"requires": {
"hoist-non-react-statics": "^2.2.2",
"prop-types": "^15.5.10"
}
},
"@jitsi/sdp-interop": { "@jitsi/sdp-interop": {
"version": "0.1.14", "version": "0.1.14",
"resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-0.1.14.tgz",
@ -2843,12 +2834,13 @@
"@segment/top-domain": "^3.0.0", "@segment/top-domain": "^3.0.0",
"blueimp-md5": "^2.10.0", "blueimp-md5": "^2.10.0",
"json3": "^3.3.2", "json3": "^3.3.2",
"lodash": "^4.17.4" "lodash": "^4.17.4",
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
}, },
"dependencies": { "dependencies": {
"ua-parser-js": { "ua-parser-js": {
"version": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d", "version": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d",
"from": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d" "from": "github:amplitude/ua-parser-js#ed538f1"
} }
} }
}, },
@ -3506,14 +3498,6 @@
"util.promisify": "^1.0.0" "util.promisify": "^1.0.0"
} }
}, },
"babel-plugin-check-es2015-constants": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
"integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
"requires": {
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-emotion": { "babel-plugin-emotion": {
"version": "9.2.11", "version": "9.2.11",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz",
@ -8868,11 +8852,6 @@
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.1.9.tgz", "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.1.9.tgz",
"integrity": "sha1-lkojxU5IiUBbSGGlyfBIDUUUHfo=" "integrity": "sha1-lkojxU5IiUBbSGGlyfBIDUUUHfo="
}, },
"keymirror": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -12181,37 +12160,11 @@
"jssha": "^2.2.0" "jssha": "^2.2.0"
} }
}, },
"react-native-communications": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-native-communications/-/react-native-communications-2.2.1.tgz",
"integrity": "sha1-eIO1ayCgAu63kMET+GFuqGksp5U="
},
"react-native-fast-image": { "react-native-fast-image": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.1.1.tgz", "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.1.1.tgz",
"integrity": "sha512-kEzgZxbbXYhy27u5GnhrKitn+XDBFAHSDUJdYC6llMi5cDPjgcqhOAQABj0K+ga5pn+/xPZLmD882rrUGiwVVA==" "integrity": "sha512-kEzgZxbbXYhy27u5GnhrKitn+XDBFAHSDUJdYC6llMi5cDPjgcqhOAQABj0K+ga5pn+/xPZLmD882rrUGiwVVA=="
}, },
"react-native-gifted-chat": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/react-native-gifted-chat/-/react-native-gifted-chat-0.6.0.tgz",
"integrity": "sha512-KYI/okKUZmjcJM3I6BP10KG1WNkCKBZhY8N47wk407dr+KqLS4+LR13UKo7j3f++5SrX2Ex+7vYvIQ2pBdzCiA==",
"requires": {
"@expo/react-native-action-sheet": "^1.0.1",
"moment": "^2.19.0",
"react-native-communications": "2.2.1",
"react-native-lightbox": "^0.7.0",
"react-native-parsed-text": "^0.0.20",
"react-native-video": "^3.2.1",
"uuid": "3.3.0"
},
"dependencies": {
"uuid": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.0.tgz",
"integrity": "sha512-ijO9N2xY/YaOqQ5yz5c4sy2ZjWmA6AR6zASb/gdpeKZ8+948CxwfMW9RrKVk5may6ev8c0/Xguu32e2Llelpqw=="
}
}
},
"react-native-google-signin": { "react-native-google-signin": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.2.tgz", "resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.2.tgz",
@ -12227,28 +12180,11 @@
"resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-4.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-4.0.0.tgz",
"integrity": "sha512-0Fotox+eLXQooeibVs3P60yASYUWjtRw9MZNmbuHt5UZQrgUrAKsE4jm7gTr4tPU1m1RkwGzcgUFpcOkh/ec7g==" "integrity": "sha512-0Fotox+eLXQooeibVs3P60yASYUWjtRw9MZNmbuHt5UZQrgUrAKsE4jm7gTr4tPU1m1RkwGzcgUFpcOkh/ec7g=="
}, },
"react-native-lightbox": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz",
"integrity": "sha512-HS3T4WlCd0Gb3us2d6Jse5m6KjNhngnKm35Wapq30WtQa9s+/VMmtuktbGPGaWtswcDyOj6qByeJBw9W80iPCA==",
"requires": {
"prop-types": "^15.5.10"
}
},
"react-native-linear-gradient": { "react-native-linear-gradient": {
"version": "2.5.3", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.3.tgz", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.3.tgz",
"integrity": "sha512-XdusrOXXlkI+yQpUW7YLeiq9cZiBwkvQX4XEkHPVrJ9H47gsKmdgBwObkZBzBQUP0dKK/Sg6aVpETEis4w43bQ==" "integrity": "sha512-XdusrOXXlkI+yQpUW7YLeiq9cZiBwkvQX4XEkHPVrJ9H47gsKmdgBwObkZBzBQUP0dKK/Sg6aVpETEis4w43bQ=="
}, },
"react-native-parsed-text": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/react-native-parsed-text/-/react-native-parsed-text-0.0.20.tgz",
"integrity": "sha512-n77hYu64Tr3oclzIXBXXaiLh1WbMKdA2Y0x6bX/yqwxAM4afcObENY5VrNB+EsTBJBEDqrypA9D1p2cLEIHkuQ==",
"requires": {
"babel-plugin-check-es2015-constants": "6.22.0",
"prop-types": "^15.5.10"
}
},
"react-native-sound": { "react-native-sound": {
"version": "0.10.12", "version": "0.10.12",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.12.tgz", "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.12.tgz",
@ -12305,15 +12241,6 @@
} }
} }
}, },
"react-native-video": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-3.2.1.tgz",
"integrity": "sha512-Xansfoo/to80FwhM1HKlf7pCxDZ5RtV+kG3piCVvsNAhPY4GGwiOGUH9y3Y+mFQIDEWcY8I9j16lsFYAbnue3g==",
"requires": {
"keymirror": "0.1.1",
"prop-types": "^15.5.10"
}
},
"react-native-webrtc": { "react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5", "version": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5",
"from": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5", "from": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5",

View File

@ -67,7 +67,6 @@
"react-native-calendar-events": "1.6.4", "react-native-calendar-events": "1.6.4",
"react-native-callstats": "3.58.2", "react-native-callstats": "3.58.2",
"react-native-fast-image": "5.1.1", "react-native-fast-image": "5.1.1",
"react-native-gifted-chat": "0.6.0",
"react-native-google-signin": "1.0.2", "react-native-google-signin": "1.0.2",
"react-native-immersive": "2.0.0", "react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0", "react-native-keep-awake": "4.0.0",

View File

@ -43,6 +43,6 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const { message } = ownProps; const { message } = ownProps;
return { return {
_avatarURL: getAvatarURLByParticipantId(state, message.user._id) _avatarURL: getAvatarURLByParticipantId(state, message.id)
}; };
} }

View File

@ -1,8 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { SafeAreaView, View } from 'react-native'; import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
import { GiftedChat } from 'react-native-gifted-chat';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
@ -20,7 +19,8 @@ import AbstractChat, {
type Props type Props
} from '../AbstractChat'; } from '../AbstractChat';
import ChatMessage from './ChatMessage'; import ChatInputBar from './ChatInputBar';
import MessageContainer from './MessageContainer';
import styles from './styles'; import styles from './styles';
/** /**
@ -28,111 +28,31 @@ import styles from './styles';
* the mobile client. * the mobile client.
*/ */
class Chat extends AbstractChat<Props> { class Chat extends AbstractChat<Props> {
/**
* Initializes a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSend = this._onSend.bind(this);
this._renderMessage = this._renderMessage.bind(this);
this._transformMessage = this._transformMessage.bind(this);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
* @inheritdoc * @inheritdoc
*/ */
render() { render() {
// Gifted chat requires a special object format and a reversed list
// of messages.
const messages
= this.props._messages.map(this._transformMessage).reverse();
return ( return (
<SlidingView <SlidingView
position = 'bottom' position = 'bottom'
show = { this.props._isOpen } > show = { this.props._isOpen } >
<View style = { styles.chatContainer }> <KeyboardAvoidingView
behavior = 'padding'
style = { styles.chatContainer }>
<Header> <Header>
<BackButton onPress = { this.props._onToggleChat } /> <BackButton onPress = { this.props._onToggleChat } />
<HeaderLabel labelKey = 'chat.title' /> <HeaderLabel labelKey = 'chat.title' />
</Header> </Header>
<SafeAreaView style = { styles.backdrop }> <SafeAreaView style = { styles.backdrop }>
<GiftedChat <MessageContainer messages = { this.props._messages } />
messages = { messages } <ChatInputBar onSend = { this.props._onSendMessage } />
onSend = { this._onSend }
renderMessage = { this._renderMessage } />
</SafeAreaView> </SafeAreaView>
</View> </KeyboardAvoidingView>
</SlidingView> </SlidingView>
); );
} }
_onSend: (Array<Object>) => void;
/**
* Callback to trigger a message send action.
*
* @param {string} message - The chat message to display.
* @returns {void}
*/
_onSend([ message ]) {
this.props._onSendMessage(message.text);
}
_renderMessage: Object => React$Element<*>
/**
* Renders a single message.
*
* @param {Object} messageProps - The message props object to be rendered.
* @returns {React$Element<*>}
*/
_renderMessage(messageProps) {
const { currentMessage } = messageProps;
return (
<ChatMessage message = { currentMessage } />
);
}
_transformMessage: (Object, number) => Object;
/**
* Transforms a Jitsi message object to a format that gifted-chat can
* handle.
*
* @param {Object} message - The chat message in our internal format.
* @param {number} index - The index of the message in the array.
* @returns {Object}
*/
_transformMessage(message, index) {
const system = message.messageType === 'error';
return (
{
_id: index,
createdAt: new Date(message.timestamp),
messageType: message.messageType,
system,
text: system
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message,
user: {
_id: message.id,
name: message.displayName
}
}
);
}
} }
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat)); export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@ -0,0 +1,122 @@
// @flow
import React, { Component } from 'react';
import { TextInput, View } from 'react-native';
import { Platform } from '../../../base/react';
import styles from './styles';
type Props = {
/**
* Callback to invoke on message send.
*/
onSend: Function
};
type State = {
/**
* Boolean to show if an extra padding needs to be added to the bar.
*/
addPadding: boolean,
/**
* The value of the input field.
*/
message: string
};
/**
* Implements the chat input bar with text field and action(s).
*/
export default class ChatInputBar extends Component<Props, State> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
addPadding: false,
message: ''
};
this._onChangeText = this._onChangeText.bind(this);
this._onFocused = this._onFocused.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<View
style = { [
styles.inputBar,
this.state.addPadding ? styles.extraBarPadding : null
] }>
<TextInput
blurOnSubmit = { false }
multiline = { false }
onBlur = { this._onFocused(false) }
onChangeText = { this._onChangeText }
onFocus = { this._onFocused(true) }
onSubmitEditing = { this._onSubmit }
returnKeyType = 'send'
style = { styles.inputField }
value = { this.state.message } />
</View>
);
}
_onChangeText: string => void;
/**
* Callback to handle the change of the value of the text field.
*
* @param {string} text - The current value of the field.
* @returns {void}
*/
_onChangeText(text) {
this.setState({
message: text
});
}
_onFocused: boolean => Function;
/**
* Constructs a callback to be used to update the padding of the field if necessary.
*
* @param {boolean} focused - True of the field is focused.
* @returns {Function}
*/
_onFocused(focused) {
return () => {
Platform.OS === 'android' && this.setState({
addPadding: focused
});
};
}
_onSubmit: () => void;
/**
* Callback to handle the submit event of the text field.
*
* @returns {void}
*/
_onSubmit() {
const message = this.state.message.trim();
message && this.props.onSend(message);
this.setState({ message: '' });
}
}

View File

@ -35,7 +35,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
render() { render() {
const { message } = this.props; const { message } = this.props;
const timeStamp = getLocalizedDateFormatter( const timeStamp = getLocalizedDateFormatter(
message.createdAt).format(TIMESTAMP_FORMAT); new Date(message.timestamp)).format(TIMESTAMP_FORMAT);
const localMessage = message.messageType === 'local'; const localMessage = message.messageType === 'local';
// Style arrays that need to be updated in various scenarios, such as // Style arrays that need to be updated in various scenarios, such as
@ -53,7 +53,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
// The bubble needs to be differently styled. // The bubble needs to be differently styled.
textWrapperStyle.push(styles.ownTextWrapper); textWrapperStyle.push(styles.ownTextWrapper);
} else if (message.system) { } else if (message.messageType === 'error') {
// The bubble needs to be differently styled. // The bubble needs to be differently styled.
textWrapperStyle.push(styles.systemTextWrapper); textWrapperStyle.push(styles.systemTextWrapper);
} }
@ -74,7 +74,12 @@ class ChatMessage extends AbstractChatMessage<Props> {
!localMessage && this._renderDisplayName() !localMessage && this._renderDisplayName()
} }
<Text style = { styles.messageText }> <Text style = { styles.messageText }>
{ message.text } { message.messageType === 'error'
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message }
</Text> </Text>
</View> </View>
<Text style = { styles.timeText }> <Text style = { styles.timeText }>
@ -112,7 +117,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
return ( return (
<Text style = { styles.displayName }> <Text style = { styles.displayName }>
{ message.user.name } { message.displayName }
</Text> </Text>
); );
} }

View File

@ -0,0 +1,76 @@
// @flow
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import ChatMessage from './ChatMessage';
import styles from './styles';
type Props = {
/**
* The messages array to render.
*/
messages: Array<Object>
}
/**
* Implements a container to render all the chat messages in a conference.
*/
export default class MessageContainer extends Component<Props> {
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._renderMessage = this._renderMessage.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<FlatList
data = { this.props.messages }
inverted = { true }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderMessage }
style = { styles.messageContainer } />
);
}
_keyExtractor: Object => string
/**
* Key extractor for the flatlist.
*
* @param {Object} item - The flatlist item that we need the key to be
* generated for.
* @param {number} index - The index of the element.
* @returns {string}
*/
_keyExtractor(item, index) {
return `key_${index}`;
}
_renderMessage: Object => React$Element<*>;
/**
* Renders a single chat message.
*
* @param {Object} message - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessage({ item: message }) {
return (
<ChatMessage message = { message } />
);
}
}

View File

@ -1,6 +1,6 @@
// @flow // @flow
import { ColorPalette } from '../../../base/styles'; import { BoxModel, ColorPalette } from '../../../base/styles';
/** /**
* The styles of the feature chat. * The styles of the feature chat.
@ -28,6 +28,7 @@ export default {
}, },
chatContainer: { chatContainer: {
alignItems: 'stretch',
flex: 1, flex: 1,
flexDirection: 'column' flexDirection: 'column'
}, },
@ -49,6 +50,29 @@ export default {
fontSize: 13 fontSize: 13
}, },
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
extraBarPadding: {
paddingBottom: 30
},
inputBar: {
borderTopColor: 'rgb(209, 219, 231)',
borderTopWidth: 1,
flexDirection: 'row',
paddingHorizontal: BoxModel.padding
},
inputField: {
flex: 1,
height: 48
},
messageContainer: {
flex: 1
},
/** /**
* The message text itself. * The message text itself.
*/ */

View File

@ -14,6 +14,11 @@ export function getUnreadCount(state: Object) {
return 0; return 0;
} }
if (navigator.product === 'ReactNative') {
// React native stores the messages in a reversed order.
return messages.indexOf(lastReadMessage);
}
const lastReadIndex = messages.lastIndexOf(lastReadMessage); const lastReadIndex = messages.lastIndexOf(lastReadMessage);
return messagesCount - (lastReadIndex + 1); return messagesCount - (lastReadIndex + 1);

View File

@ -22,14 +22,22 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
timestamp: action.timestamp timestamp: action.timestamp
}; };
// React native, unlike web, needs a reverse sorted message list.
const messages = navigator.product === 'ReactNative'
? [
newMessage,
...state.messages
]
: [
...state.messages,
newMessage
];
return { return {
...state, ...state,
lastReadMessage: lastReadMessage:
action.hasRead ? newMessage : state.lastReadMessage, action.hasRead ? newMessage : state.lastReadMessage,
messages: [ messages
...state.messages,
newMessage
]
}; };
} }
@ -44,7 +52,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
return { return {
...state, ...state,
isOpen: !state.isOpen, isOpen: !state.isOpen,
lastReadMessage: state.messages[state.messages.length - 1] lastReadMessage: state.messages[
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1]
}; };
} }