feat: improve chat UX

This commit is contained in:
Bettenbuk Zoltan 2019-11-21 15:26:57 +01:00 committed by Zoltan Bettenbuk
parent 955fa1f49f
commit 6121e9fc65
6 changed files with 106 additions and 11 deletions

View File

@ -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"

View File

@ -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;
}

View File

@ -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);

View File

@ -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));

View File

@ -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

View File

@ -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