[FIX] Remove some unnecessary re-renders on Messagebox (#1341)

This commit is contained in:
Diego Mello 2019-10-30 11:14:41 -03:00 committed by GitHub
parent 66222a3f9a
commit fcb420a773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 341 additions and 259 deletions

View File

@ -1,47 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity, ActivityIndicator } from 'react-native';
import FastImage from 'react-native-fast-image';
import styles from './styles';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_PRIMARY } from '../../constants/colors';
export default class CommandPreview extends React.PureComponent {
static propTypes = {
onPress: PropTypes.func,
item: PropTypes.object
};
constructor(props) {
super(props);
this.state = { loading: true };
}
render() {
const { onPress, item } = this.props;
const { loading } = this.state;
return (
<TouchableOpacity
style={styles.commandPreview}
onPress={() => onPress(item)}
testID={`command-preview-item${ item.id }`}
>
{item.type === 'image'
? (
<FastImage
style={styles.commandPreviewImage}
source={{ uri: item.value }}
resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => this.setState({ loading: true })}
onLoad={() => this.setState({ loading: false })}
>
{ loading ? <ActivityIndicator /> : null }
</FastImage>
)
: <CustomIcon name='file-generic' size={36} color={COLOR_PRIMARY} />
}
</TouchableOpacity>
);
}
}

View File

@ -0,0 +1,44 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity, ActivityIndicator } from 'react-native';
import FastImage from 'react-native-fast-image';
import styles from '../styles';
import { CustomIcon } from '../../../lib/Icons';
import { COLOR_PRIMARY } from '../../../constants/colors';
import MessageboxContext from '../Context';
const Item = ({ item }) => {
const context = useContext(MessageboxContext);
const { onPressCommandPreview } = context;
const [loading, setLoading] = useState(true);
return (
<TouchableOpacity
style={styles.commandPreview}
onPress={() => onPressCommandPreview(item)}
testID={`command-preview-item${ item.id }`}
>
{item.type === 'image'
? (
<FastImage
style={styles.commandPreviewImage}
source={{ uri: item.value }}
resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => setLoading(true)}
onLoad={() => setLoading(false)}
>
{ loading ? <ActivityIndicator /> : null }
</FastImage>
)
: <CustomIcon name='file-generic' size={36} color={COLOR_PRIMARY} />
}
</TouchableOpacity>
);
};
Item.propTypes = {
item: PropTypes.object
};
export default Item;

View File

@ -0,0 +1,40 @@
import React from 'react';
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
import Item from './Item';
import styles from '../styles';
const CommandsPreview = React.memo(({ commandPreview, showCommandPreview }) => {
if (!showCommandPreview) {
return null;
}
return (
<FlatList
testID='commandbox-container'
style={styles.mentionList}
data={commandPreview}
renderItem={({ item }) => <Item item={item} />}
keyExtractor={item => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
);
}, (prevProps, nextProps) => {
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false;
}
if (!equal(prevProps.commandPreview, nextProps.commandPreview)) {
return false;
}
return true;
});
CommandsPreview.propTypes = {
commandPreview: PropTypes.array,
showCommandPreview: PropTypes.bool
};
export default CommandsPreview;

View File

@ -0,0 +1,4 @@
import React from 'react';
const MessageboxContext = React.createContext();
export default MessageboxContext;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles';
import I18n from '../../../i18n';
const FixedMentionItem = ({ item, onPress }) => (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => onPress(item)}
>
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
<Text style={styles.mentionText}>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
</TouchableOpacity>
);
FixedMentionItem.propTypes = {
item: PropTypes.object,
onPress: PropTypes.func
};
export default FixedMentionItem;

View File

@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { shortnameToUnicode } from 'emoji-toolkit';
import styles from '../styles';
import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
const MentionEmoji = ({ item }) => {
const context = useContext(MessageboxContext);
const { baseUrl } = context;
if (item.name) {
return (
<CustomEmoji
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={baseUrl}
/>
);
}
return (
<Text style={styles.mentionItemEmoji}>
{shortnameToUnicode(`:${ item }:`)}
</Text>
);
};
MentionEmoji.propTypes = {
item: PropTypes.object
};
export default MentionEmoji;

View File

@ -0,0 +1,87 @@
import React, { useContext } from 'react';
import { TouchableOpacity, Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles';
import Avatar from '../../Avatar';
import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji';
import {
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS
} from '../constants';
const MentionItem = ({
item, trackingType
}) => {
const context = useContext(MessageboxContext);
const { baseUrl, user, onPressMention } = context;
const defineTestID = (type) => {
switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`;
case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${ item.command || item }`;
default:
return `mention-item-${ item.username || item.name || item }`;
}
};
const testID = defineTestID(trackingType);
if (item.username === 'all' || item.username === 'here') {
return <FixedMentionItem item={item} onPress={onPressMention} />;
}
let content = (
<>
<Avatar
style={styles.avatar}
text={item.username || item.name}
size={30}
type={item.t}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<Text style={styles.mentionText}>{ item.username || item.name || item }</Text>
</>
);
if (trackingType === MENTIONS_TRACKING_TYPE_EMOJIS) {
content = (
<>
<MentionEmoji item={item} />
<Text style={styles.mentionText}>:{ item.name || item }:</Text>
</>
);
}
if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) {
content = (
<>
<Text style={styles.slash}>/</Text>
<Text>{ item.command}</Text>
</>
);
}
return (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => onPressMention(item)}
testID={testID}
>
{content}
</TouchableOpacity>
);
};
MentionItem.propTypes = {
item: PropTypes.object,
trackingType: PropTypes.string
};
export default MentionItem;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
import styles from '../styles';
import MentionItem from './MentionItem';
const Mentions = React.memo(({ mentions, trackingType }) => {
if (!trackingType) {
return null;
}
return (
<FlatList
testID='messagebox-container'
style={styles.mentionList}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} />}
keyExtractor={item => item.id || item.username || item.command || item}
keyboardShouldPersistTaps='always'
/>
);
}, (prevProps, nextProps) => {
if (prevProps.trackingType !== nextProps.trackingType) {
return false;
}
if (!equal(prevProps.mentions, nextProps.mentions)) {
return false;
}
return true;
});
Mentions.propTypes = {
mentions: PropTypes.array,
trackingType: PropTypes.string
};
export default Mentions;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
@ -47,45 +47,38 @@ const styles = StyleSheet.create({
}
});
class ReplyPreview extends Component {
static propTypes = {
useMarkdown: PropTypes.bool,
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
getCustomEmoji: PropTypes.func
const ReplyPreview = React.memo(({
message, Message_TimeFormat, baseUrl, username, useMarkdown, replying, getCustomEmoji, close
}) => {
if (!replying) {
return null;
}
shouldComponentUpdate() {
return false;
}
close = () => {
const { close } = this.props;
close();
}
render() {
const {
message, Message_TimeFormat, baseUrl, username, useMarkdown, getCustomEmoji
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View style={styles.container}>
<View style={styles.messageContainer}>
<View style={styles.header}>
<Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} preview />
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View style={styles.container}>
<View style={styles.messageContainer}>
<View style={styles.header}>
<Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} preview />
</View>
);
}
}
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={close} />
</View>
);
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying);
ReplyPreview.propTypes = {
replying: PropTypes.bool,
useMarkdown: PropTypes.bool,
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
getCustomEmoji: PropTypes.func
};
const mapStateToProps = state => ({
useMarkdown: state.markdown.useMarkdown,

View File

@ -0,0 +1,4 @@
export const MENTIONS_TRACKING_TYPE_USERS = '@';
export const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_COUNT_TO_DISPLAY = 4;

View File

@ -1,10 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert, ScrollView
View, TextInput, Alert
} from 'react-native';
import { connect } from 'react-redux';
import { shortnameToUnicode } from 'emoji-toolkit';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker';
import equal from 'deep-equal';
@ -16,8 +15,6 @@ import { userTyping as userTypingAction } from '../../actions/room';
import RocketChat from '../../lib/rocketchat';
import styles from './styles';
import database from '../../lib/database';
import Avatar from '../Avatar';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { emojis } from '../../emojis';
import Recording from './Recording';
import UploadModal from './UploadModal';
@ -29,13 +26,16 @@ import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons';
import { isAndroid } from '../../utils/deviceInfo';
import CommandPreview from './CommandPreview';
import { canUploadFile } from '../../utils/media';
const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
const MENTIONS_COUNT_TO_DISPLAY = 4;
import Mentions from './Mentions';
import MessageboxContext from './Context';
import {
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS,
MENTIONS_COUNT_TO_DISPLAY,
MENTIONS_TRACKING_TYPE_USERS
} from './constants';
import CommandsPreview from './CommandsPreview';
const imagePickerConfig = {
cropping: true,
@ -545,7 +545,6 @@ class MessageBox extends Component {
}
}
showUploadModal = (file) => {
this.setState({ file: { ...file, isVisible: true } });
}
@ -726,175 +725,29 @@ class MessageBox extends Component {
});
}
renderFixedMentionItem = item => (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this.onPressMention(item)}
>
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
<Text style={styles.mentionText}>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
</TouchableOpacity>
)
renderMentionEmoji = (item) => {
const { baseUrl } = this.props;
if (item.name) {
return (
<CustomEmoji
key='mention-item-avatar'
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={baseUrl}
/>
);
}
return (
<Text
key='mention-item-avatar'
style={styles.mentionItemEmoji}
>
{shortnameToUnicode(`:${ item }:`)}
</Text>
);
}
renderMentionItem = ({ item }) => {
const { trackingType } = this.state;
const { baseUrl, user } = this.props;
if (item.username === 'all' || item.username === 'here') {
return this.renderFixedMentionItem(item);
}
const defineTestID = (type) => {
switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`;
case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${ item.command || item }`;
default:
return `mention-item-${ item.username || item.name || item }`;
}
};
const testID = defineTestID(trackingType);
return (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this.onPressMention(item)}
testID={testID}
>
{(() => {
switch (trackingType) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return (
<>
{this.renderMentionEmoji(item)}
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
</>
);
case MENTIONS_TRACKING_TYPE_COMMANDS:
return (
<>
<Text key='mention-item-command' style={styles.slash}>/</Text>
<Text key='mention-item-param'>{ item.command}</Text>
</>
);
default:
return (
<>
<Avatar
key='mention-item-avatar'
style={styles.avatar}
text={item.username || item.name}
size={30}
type={item.t}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name || item }</Text>
</>
);
}
})()
}
</TouchableOpacity>
);
}
renderMentions = () => {
const { mentions, trackingType } = this.state;
if (!trackingType) {
return null;
}
return (
<ScrollView
testID='messagebox-container'
style={styles.scrollViewMention}
keyboardShouldPersistTaps='always'
>
<FlatList
style={styles.mentionList}
data={mentions}
extraData={mentions}
renderItem={this.renderMentionItem}
keyExtractor={item => item.id || item.username || item.command || item}
keyboardShouldPersistTaps='always'
/>
</ScrollView>
);
};
renderCommandPreviewItem = ({ item }) => (
<CommandPreview item={item} onPress={this.onPressCommandPreview} />
);
renderCommandPreview = () => {
const { commandPreview, showCommandPreview } = this.state;
if (!showCommandPreview) {
return null;
}
return (
<View key='commandbox-container' testID='commandbox-container'>
<FlatList
style={styles.mentionList}
data={commandPreview}
renderItem={this.renderCommandPreviewItem}
keyExtractor={item => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
</View>
);
}
renderReplyPreview = () => {
const {
message, replying, replyCancel, user, getCustomEmoji
} = this.props;
if (!replying) {
return null;
}
return <ReplyPreview key='reply-preview' message={message} close={replyCancel} username={user.username} getCustomEmoji={getCustomEmoji} />;
};
renderContent = () => {
const { recording, showEmojiKeyboard, showSend } = this.state;
const { editing } = this.props;
const {
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
} = this.state;
const {
editing, message, replying, replyCancel, user, getCustomEmoji
} = this.props;
if (recording) {
return (<Recording onFinish={this.finishAudioMessage} />);
return <Recording onFinish={this.finishAudioMessage} />;
}
return (
<>
{this.renderCommandPreview()}
{this.renderMentions()}
<View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()}
<CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} />
<Mentions mentions={mentions} trackingType={trackingType} />
<View style={styles.composer}>
<ReplyPreview
message={message}
close={replyCancel}
username={user.username}
replying={replying}
getCustomEmoji={getCustomEmoji}
/>
<View
style={[styles.textArea, editing && styles.editing]}
testID='messagebox'
@ -936,8 +789,16 @@ class MessageBox extends Component {
render() {
console.count(`${ this.constructor.name }.render calls`);
const { showEmojiKeyboard, file } = this.state;
const { user, baseUrl } = this.props;
return (
<>
<MessageboxContext.Provider
value={{
user,
baseUrl,
onPressMention: this.onPressMention,
onPressCommandPreview: this.onPressCommandPreview
}}
>
<KeyboardAccessoryView
renderContent={this.renderContent}
kbInputRef={this.component}
@ -955,7 +816,7 @@ class MessageBox extends Component {
close={() => this.setState({ file: {} })}
submit={this.sendMediaMessage}
/>
</>
</MessageboxContext.Provider>
);
}
}