feat: improve chat UX
This commit is contained in:
parent
955fa1f49f
commit
6121e9fc65
|
@ -47,8 +47,10 @@
|
|||
},
|
||||
"chat": {
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"fieldPlaceHolder": "Type your message here",
|
||||
"messagebox": "Type a message",
|
||||
"messageTo": "Private message to {{recipient}}",
|
||||
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat"
|
||||
|
|
|
@ -15,7 +15,7 @@ export type Props = {
|
|||
*
|
||||
* @extends PureComponent
|
||||
*/
|
||||
export default class AbstractMessageContainer extends PureComponent<Props> {
|
||||
export default class AbstractMessageContainer<P: Props> extends PureComponent<P> {
|
||||
static defaultProps = {
|
||||
messages: []
|
||||
};
|
||||
|
@ -46,7 +46,7 @@ export default class AbstractMessageContainer extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
groups.push(currentGrouping);
|
||||
currentGrouping.length && groups.push(currentGrouping);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import { TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconChatSend } from '../../../base/icons';
|
||||
import { Platform } from '../../../base/react';
|
||||
|
||||
|
@ -13,7 +14,12 @@ type Props = {
|
|||
/**
|
||||
* Callback to invoke on message send.
|
||||
*/
|
||||
onSend: Function
|
||||
onSend: Function,
|
||||
|
||||
/**
|
||||
* Function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -37,7 +43,7 @@ type State = {
|
|||
/**
|
||||
* Implements the chat input bar with text field and action(s).
|
||||
*/
|
||||
export default class ChatInputBar extends Component<Props, State> {
|
||||
class ChatInputBar extends Component<Props, State> {
|
||||
/**
|
||||
* Instantiates a new instance of the component.
|
||||
*
|
||||
|
@ -53,6 +59,7 @@ export default class ChatInputBar extends Component<Props, State> {
|
|||
};
|
||||
|
||||
this._onChangeText = this._onChangeText.bind(this);
|
||||
this._onFieldReferenceAvailable = this._onFieldReferenceAvailable.bind(this);
|
||||
this._onFocused = this._onFocused.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
@ -76,6 +83,8 @@ export default class ChatInputBar extends Component<Props, State> {
|
|||
onChangeText = { this._onChangeText }
|
||||
onFocus = { this._onFocused(true) }
|
||||
onSubmitEditing = { this._onSubmit }
|
||||
placeholder = { this.props.t('chat.fieldPlaceHolder') }
|
||||
ref = { this._onFieldReferenceAvailable }
|
||||
returnKeyType = 'send'
|
||||
style = { styles.inputField }
|
||||
value = { this.state.message } />
|
||||
|
@ -105,6 +114,18 @@ export default class ChatInputBar extends Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
_onFieldReferenceAvailable: Object => void;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the field reference is available.
|
||||
*
|
||||
* @param {Object} field - The reference to the field.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFieldReferenceAvailable(field) {
|
||||
field && field.focus();
|
||||
}
|
||||
|
||||
_onFocused: boolean => Function;
|
||||
|
||||
/**
|
||||
|
@ -138,3 +159,5 @@ export default class ChatInputBar extends Component<Props, State> {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(ChatInputBar);
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
|
||||
import AbstractMessageContainer, { type Props }
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
|
||||
import AbstractMessageContainer, { type Props as AbstractProps }
|
||||
from '../AbstractMessageContainer';
|
||||
|
||||
import ChatMessageGroup from './ChatMessageGroup';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the feature.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* Function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a container to render all the chat messages in a conference.
|
||||
*/
|
||||
export default class MessageContainer extends AbstractMessageContainer {
|
||||
class MessageContainer extends AbstractMessageContainer<Props> {
|
||||
/**
|
||||
* Instantiates a new instance of the component.
|
||||
*
|
||||
|
@ -22,6 +40,7 @@ export default class MessageContainer extends AbstractMessageContainer {
|
|||
super(props);
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._renderListEmptyComponent = this._renderListEmptyComponent.bind(this);
|
||||
this._renderMessageGroup = this._renderMessageGroup.bind(this);
|
||||
}
|
||||
|
||||
|
@ -31,10 +50,16 @@ export default class MessageContainer extends AbstractMessageContainer {
|
|||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const data = this._getMessagesGroupedBySender();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data = { this._getMessagesGroupedBySender() }
|
||||
inverted = { true }
|
||||
ListEmptyComponent = { this._renderListEmptyComponent }
|
||||
data = { data }
|
||||
|
||||
// Workaround for RN bug:
|
||||
// https://github.com/facebook/react-native/issues/21196
|
||||
inverted = { Boolean(data.length) }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
keyboardShouldPersistTaps = 'always'
|
||||
renderItem = { this._renderMessageGroup }
|
||||
|
@ -58,7 +83,26 @@ export default class MessageContainer extends AbstractMessageContainer {
|
|||
return `key_${index}`;
|
||||
}
|
||||
|
||||
_renderMessageGroup: Object => React$Element<*>;
|
||||
_renderListEmptyComponent: () => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Renders a message when there are no messages in the chat yet.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { _styles, t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.emptyComponentWrapper }>
|
||||
<Text style = { _styles.emptyComponentText }>
|
||||
{ t('chat.noMessagesMessage') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderMessageGroup: Object => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
|
@ -70,3 +114,17 @@ export default class MessageContainer extends AbstractMessageContainer {
|
|||
return <ChatMessageGroup messages = { messages } />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_styles: ColorSchemeRegistry.get(state, 'Chat')
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MessageContainer));
|
||||
|
|
|
@ -42,6 +42,13 @@ export default {
|
|||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
emptyComponentWrapper: {
|
||||
alignSelf: 'center',
|
||||
flex: 1,
|
||||
padding: BoxModel.padding,
|
||||
paddingTop: '10%'
|
||||
},
|
||||
|
||||
/**
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
|
@ -143,6 +150,11 @@ ColorSchemeRegistry.register('Chat', {
|
|||
fontSize: 13
|
||||
},
|
||||
|
||||
emptyComponentText: {
|
||||
color: schemeColor('displayName'),
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
localMessageBubble: {
|
||||
backgroundColor: schemeColor('localMsgBackground'),
|
||||
borderTopRightRadius: 0
|
||||
|
|
|
@ -14,7 +14,7 @@ import ChatMessageGroup from './ChatMessageGroup';
|
|||
*
|
||||
* @extends AbstractMessageContainer
|
||||
*/
|
||||
export default class MessageContainer extends AbstractMessageContainer {
|
||||
export default class MessageContainer extends AbstractMessageContainer<Props> {
|
||||
/**
|
||||
* Whether or not chat has been scrolled to the bottom of the screen. Used
|
||||
* to determine if chat should be scrolled automatically to the bottom when
|
||||
|
|
Loading…
Reference in New Issue