diff --git a/package-lock.json b/package-lock.json index b85041dc7..035caa644 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2423,15 +2423,6 @@ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", "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": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/@jitsi/sdp-interop/-/sdp-interop-0.1.14.tgz", @@ -2843,12 +2834,13 @@ "@segment/top-domain": "^3.0.0", "blueimp-md5": "^2.10.0", "json3": "^3.3.2", - "lodash": "^4.17.4" + "lodash": "^4.17.4", + "ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d" }, "dependencies": { "ua-parser-js": { "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" } }, - "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": { "version": "9.2.11", "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", "integrity": "sha1-lkojxU5IiUBbSGGlyfBIDUUUHfo=" }, - "keymirror": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz", - "integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU=" - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -12181,37 +12160,11 @@ "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": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.1.1.tgz", "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": { "version": "1.0.2", "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", "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": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.3.tgz", "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": { "version": "0.10.12", "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": { "version": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5", "from": "github:jitsi/react-native-webrtc#659d2fe417b52356b1b706636de97e23bae3e9f5", diff --git a/package.json b/package.json index 87f5226cf..b6f842b8b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "react-native-calendar-events": "1.6.4", "react-native-callstats": "3.58.2", "react-native-fast-image": "5.1.1", - "react-native-gifted-chat": "0.6.0", "react-native-google-signin": "1.0.2", "react-native-immersive": "2.0.0", "react-native-keep-awake": "4.0.0", diff --git a/react/features/chat/components/AbstractChatMessage.js b/react/features/chat/components/AbstractChatMessage.js index 33fbc70c1..00be6fa97 100644 --- a/react/features/chat/components/AbstractChatMessage.js +++ b/react/features/chat/components/AbstractChatMessage.js @@ -43,6 +43,6 @@ export function _mapStateToProps(state: Object, ownProps: Props) { const { message } = ownProps; return { - _avatarURL: getAvatarURLByParticipantId(state, message.user._id) + _avatarURL: getAvatarURLByParticipantId(state, message.id) }; } diff --git a/react/features/chat/components/native/Chat.js b/react/features/chat/components/native/Chat.js index b3d91c56e..1272adbad 100644 --- a/react/features/chat/components/native/Chat.js +++ b/react/features/chat/components/native/Chat.js @@ -1,8 +1,7 @@ // @flow import React from 'react'; -import { SafeAreaView, View } from 'react-native'; -import { GiftedChat } from 'react-native-gifted-chat'; +import { KeyboardAvoidingView, SafeAreaView } from 'react-native'; import { translate } from '../../../base/i18n'; @@ -20,7 +19,8 @@ import AbstractChat, { type Props } from '../AbstractChat'; -import ChatMessage from './ChatMessage'; +import ChatInputBar from './ChatInputBar'; +import MessageContainer from './MessageContainer'; import styles from './styles'; /** @@ -28,111 +28,31 @@ import styles from './styles'; * the mobile client. */ class Chat extends AbstractChat { - - /** - * 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()}. * * @inheritdoc */ render() { - // Gifted chat requires a special object format and a reversed list - // of messages. - const messages - = this.props._messages.map(this._transformMessage).reverse(); - return ( - +
- + + -
+
); } - - _onSend: (Array) => 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 ( - - ); - } - - _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)); diff --git a/react/features/chat/components/native/ChatInputBar.js b/react/features/chat/components/native/ChatInputBar.js new file mode 100644 index 000000000..1f9430879 --- /dev/null +++ b/react/features/chat/components/native/ChatInputBar.js @@ -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 { + /** + * 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 ( + + + + ); + } + + _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: '' }); + } +} diff --git a/react/features/chat/components/native/ChatMessage.js b/react/features/chat/components/native/ChatMessage.js index f4ddd5b8c..f46146a8b 100644 --- a/react/features/chat/components/native/ChatMessage.js +++ b/react/features/chat/components/native/ChatMessage.js @@ -35,7 +35,7 @@ class ChatMessage extends AbstractChatMessage { render() { const { message } = this.props; const timeStamp = getLocalizedDateFormatter( - message.createdAt).format(TIMESTAMP_FORMAT); + new Date(message.timestamp)).format(TIMESTAMP_FORMAT); const localMessage = message.messageType === 'local'; // Style arrays that need to be updated in various scenarios, such as @@ -53,7 +53,7 @@ class ChatMessage extends AbstractChatMessage { // The bubble needs to be differently styled. textWrapperStyle.push(styles.ownTextWrapper); - } else if (message.system) { + } else if (message.messageType === 'error') { // The bubble needs to be differently styled. textWrapperStyle.push(styles.systemTextWrapper); } @@ -74,7 +74,12 @@ class ChatMessage extends AbstractChatMessage { !localMessage && this._renderDisplayName() } - { message.text } + { message.messageType === 'error' + ? this.props.t('chat.error', { + error: message.error, + originalText: message.message + }) + : message.message } @@ -112,7 +117,7 @@ class ChatMessage extends AbstractChatMessage { return ( - { message.user.name } + { message.displayName } ); } diff --git a/react/features/chat/components/native/MessageContainer.js b/react/features/chat/components/native/MessageContainer.js new file mode 100644 index 000000000..c9569ee66 --- /dev/null +++ b/react/features/chat/components/native/MessageContainer.js @@ -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 +} + +/** + * Implements a container to render all the chat messages in a conference. + */ +export default class MessageContainer extends Component { + /** + * 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 ( + + ); + } + + _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 ( + + ); + } +} diff --git a/react/features/chat/components/native/styles.js b/react/features/chat/components/native/styles.js index 4154d70e6..56a70cc4c 100644 --- a/react/features/chat/components/native/styles.js +++ b/react/features/chat/components/native/styles.js @@ -1,6 +1,6 @@ // @flow -import { ColorPalette } from '../../../base/styles'; +import { BoxModel, ColorPalette } from '../../../base/styles'; /** * The styles of the feature chat. @@ -28,6 +28,7 @@ export default { }, chatContainer: { + alignItems: 'stretch', flex: 1, flexDirection: 'column' }, @@ -49,6 +50,29 @@ export default { 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. */ diff --git a/react/features/chat/functions.js b/react/features/chat/functions.js index bec532819..47a1332fc 100644 --- a/react/features/chat/functions.js +++ b/react/features/chat/functions.js @@ -14,6 +14,11 @@ export function getUnreadCount(state: Object) { return 0; } + if (navigator.product === 'ReactNative') { + // React native stores the messages in a reversed order. + return messages.indexOf(lastReadMessage); + } + const lastReadIndex = messages.lastIndexOf(lastReadMessage); return messagesCount - (lastReadIndex + 1); diff --git a/react/features/chat/reducer.js b/react/features/chat/reducer.js index d3ee55e79..0e9709791 100644 --- a/react/features/chat/reducer.js +++ b/react/features/chat/reducer.js @@ -22,14 +22,22 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => { 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 { ...state, lastReadMessage: action.hasRead ? newMessage : state.lastReadMessage, - messages: [ - ...state.messages, - newMessage - ] + messages }; } @@ -44,7 +52,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => { return { ...state, isOpen: !state.isOpen, - lastReadMessage: state.messages[state.messages.length - 1] + lastReadMessage: state.messages[ + navigator.product === 'ReactNative' ? 0 : state.messages.length - 1] }; }