Skip to content

Commit

Permalink
Message render performance (#880)
Browse files Browse the repository at this point in the history
- Refactored Message component to use React.memo and re-render only what's necessary
- Added a test mode to toggle markdown parse by long press drawer (it'll be removed in the next release)
  • Loading branch information
diegolmello authored May 20, 2019
1 parent 31cf0e5 commit 60418b7
Show file tree
Hide file tree
Showing 57 changed files with 15,504 additions and 15,374 deletions.
27,723 changes: 13,767 additions & 13,956 deletions __tests__/__snapshots__/Storyshots.test.js.snap

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions app/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ export function setAllSettings(settings) {
};
}

export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
payload: emojis
};
}

export function login() {
return {
type: 'LOGIN'
Expand Down
121 changes: 121 additions & 0 deletions app/containers/FileModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import {
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
} from 'react-native';
import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import ImageViewer from 'react-native-image-zoom-viewer';
import VideoPlayer from 'react-native-video-controls';

import sharedStyles from '../views/Styles';
import { COLOR_WHITE } from '../constants/colors';
import { formatAttachmentUrl } from '../lib/utils';

const styles = StyleSheet.create({
safeArea: {
flex: 1
},
modal: {
margin: 0
},
titleContainer: {
width: '100%',
alignItems: 'center',
marginVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
description: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 14,
...sharedStyles.textMedium
},
indicator: {
flex: 1
}
});

const Indicator = React.memo(() => (
<ActivityIndicator style={styles.indicator} />
));

const ModalContent = React.memo(({
attachment, onClose, user, baseUrl
}) => {
if (attachment && attachment.image_url) {
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
return (
<SafeAreaView style={styles.safeArea}>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{attachment.title}</Text>
{attachment.description ? <Text style={styles.description}>{attachment.description}</Text> : null}
</View>
</TouchableWithoutFeedback>
<ImageViewer
imageUrls={[{ url }]}
onClick={onClose}
backgroundColor='transparent'
enableSwipeDown
onSwipeDown={onClose}
renderIndicator={() => null}
renderImage={props => <FastImage {...props} />}
loadingRender={() => <Indicator />}
/>
</SafeAreaView>
);
}
if (attachment && attachment.video_url) {
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
return (
<SafeAreaView style={styles.safeArea}>
<VideoPlayer
source={{ uri }}
onBack={onClose}
disableVolume
/>
</SafeAreaView>
);
}
return null;
});

const FileModal = React.memo(({
isVisible, onClose, attachment, user, baseUrl
}) => (
<Modal
style={styles.modal}
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
onSwipeComplete={onClose}
swipeDirection={['up', 'left', 'right', 'down']}
>
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
</Modal>
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);

FileModal.propTypes = {
isVisible: PropTypes.bool,
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onClose: PropTypes.func
};
FileModal.displayName = 'FileModal';

ModalContent.propTypes = {
attachment: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onClose: PropTypes.func
};
ModalContent.displayName = 'FileModalContent';

export default FileModal;
4 changes: 2 additions & 2 deletions app/containers/HeaderButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export const CustomHeaderButtons = React.memo(props => (
/>
));

export const DrawerButton = React.memo(({ navigation, testID }) => (
export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
<CustomHeaderButtons left>
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} />
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
</CustomHeaderButtons>
));

Expand Down
7 changes: 3 additions & 4 deletions app/containers/MessageBox/ReplyPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import moment from 'moment';
import { connect } from 'react-redux';

import Markdown from '../message/Markdown';
import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import {
Expand Down Expand Up @@ -49,15 +50,13 @@ const styles = StyleSheet.create({

@connect(state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
customEmojis: state.customEmojis,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class ReplyPreview extends Component {
static propTypes = {
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
customEmojis: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired
}
Expand All @@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {

render() {
const {
message, Message_TimeFormat, customEmojis, baseUrl, username
message, Message_TimeFormat, baseUrl, username
} = this.props;
const time = moment(message.ts).format(Message_TimeFormat);
return (
Expand All @@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} />
</View>
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
</View>
Expand Down
1 change: 1 addition & 0 deletions app/containers/MessageBox/UploadModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export default class UploadModal extends Component {
animationOut='fadeOut'
useNativeDriver
hideModalContentWhileAnimating
avoidKeyboard
>
<View style={[styles.container, { width: width - 32 }]}>
<View style={styles.titleContainer}>
Expand Down
151 changes: 151 additions & 0 deletions app/containers/ReactionsModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from 'react';
import {
View, Text, FlatList, StyleSheet, SafeAreaView
} from 'react-native';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import Touchable from 'react-native-platform-touchable';

import Emoji from './message/Emoji';
import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
import { COLOR_WHITE } from '../constants/colors';

const styles = StyleSheet.create({
titleContainer: {
alignItems: 'center',
paddingVertical: 10
},
title: {
color: COLOR_WHITE,
textAlign: 'center',
fontSize: 16,
...sharedStyles.textSemibold
},
reactCount: {
color: COLOR_WHITE,
fontSize: 13,
...sharedStyles.textRegular
},
peopleReacted: {
color: COLOR_WHITE,
fontSize: 14,
...sharedStyles.textMedium
},
peopleItemContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
emojiContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center'
},
itemContainer: {
height: 50,
flexDirection: 'row'
},
listContainer: {
flex: 1
},
closeButton: {
position: 'absolute',
left: 0,
top: 10,
color: COLOR_WHITE
}
});
const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 };

const Item = React.memo(({ item, user, baseUrl }) => {
const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3)
.map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
if (count > 3) {
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
} else {
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
}
return (
<View style={styles.itemContainer}>
<View style={styles.emojiContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
baseUrl={baseUrl}
/>
</View>
<View style={styles.peopleItemContainer}>
<Text style={styles.reactCount}>
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
</Text>
<Text style={styles.peopleReacted}>{ usernames }</Text>
</View>
</View>
);
});

const ModalContent = React.memo(({ message, onClose, ...props }) => {
if (message && message.reactions) {
return (
<SafeAreaView style={{ flex: 1 }}>
<Touchable onPress={onClose}>
<View style={styles.titleContainer}>
<CustomIcon
style={styles.closeButton}
name='cross'
size={20}
/>
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
</View>
</Touchable>
<FlatList
style={styles.listContainer}
data={message.reactions}
renderItem={({ item }) => <Item item={item} {...props} />}
keyExtractor={item => item.emoji}
/>
</SafeAreaView>
);
}
return null;
});

const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
backdropOpacity={0.8}
onSwipeComplete={onClose}
swipeDirection={['up', 'left', 'right', 'down']}
>
<ModalContent onClose={onClose} {...props} />
</Modal>
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);

ReactionsModal.propTypes = {
isVisible: PropTypes.bool,
onClose: PropTypes.func
};
ReactionsModal.displayName = 'ReactionsModal';

ModalContent.propTypes = {
message: PropTypes.object,
onClose: PropTypes.func
};
ModalContent.displayName = 'ReactionsModalContent';

Item.propTypes = {
item: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string
};
Item.displayName = 'ReactionsModalItem';

export default ReactionsModal;
44 changes: 44 additions & 0 deletions app/containers/message/Attachments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';

import Image from './Image';
import Audio from './Audio';
import Video from './Video';
import Reply from './Reply';

const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
}) => {
if (!attachments || attachments.length === 0) {
return null;
}

return attachments.map((file, index) => {
if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}
if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}
if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
}

// eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
});
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));

Attachments.propTypes = {
attachments: PropTypes.array,
timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
useMarkdown: PropTypes.bool,
onOpenFileModal: PropTypes.func,
getCustomEmoji: PropTypes.func
};
Attachments.displayName = 'MessageAttachments';

export default Attachments;
Loading

0 comments on commit 60418b7

Please sign in to comment.