Unverified Commit d23c0555 authored by Diego Mello's avatar Diego Mello Committed by GitHub

Unnecessary re-renders removed (#570)

* shouldComponentUpdate

* Rooms list shouldcomponentupdate

* RoomView shouldComponentUpdate

* Messagebox and Message shouldComponentUpdate

* EmojiPicker shouldComponentUpdate

* RoomActions shouldComponentUpdate

* Room info shouldComponentUpdate

* Update RNN

* Use only one Flatlist if none group filter is selected

* Update fix

* shouldComponentUpdate

* Bug fixes

* ListView changes

* Bug fix

* render list bug fix

* Changes on public channels

* - RoomView saga leak removed
- Join room e2e tests added

* Rest versions

* Method call versions

* Min RocketChat version alert
parent 8384d4ee
......@@ -8,6 +8,8 @@
[![CodeFactor](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative/badge)](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative)
[![Known Vulnerabilities](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative/badge.svg)](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative)
**Supported Server Versions:** 0.66.0+
## Download
<a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
......
import React from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image';
export default class Avatar extends React.PureComponent {
export default class Avatar extends PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style,
......
......@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles';
export default class TabBar extends React.PureComponent {
export default class TabBar extends React.Component {
static propTypes = {
goToPage: PropTypes.func,
activeTab: PropTypes.number,
......@@ -11,6 +11,14 @@ export default class TabBar extends React.PureComponent {
tabEmojiStyle: PropTypes.object
}
shouldComponentUpdate(nextProps) {
const { activeTab } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
return false;
}
render() {
const {
tabs, goToPage, tabEmojiStyle, activeTab
......
......@@ -4,6 +4,8 @@ import { ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import map from 'lodash/map';
import { emojify } from 'react-emojione';
import equal from 'deep-equal';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import styles from './styles';
......@@ -28,26 +30,41 @@ export default class EmojiPicker extends Component {
constructor(props) {
super(props);
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
this.customEmojis = database.objects('customEmojis');
this.state = {
frequentlyUsed: [],
customEmojis: []
customEmojis: [],
show: false
};
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
this.customEmojis = database.objects('customEmojis');
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
}
//
// shouldComponentUpdate(nextProps) {
// return false;
// }
componentDidMount() {
this.updateFrequentlyUsed();
this.updateCustomEmojis();
requestAnimationFrame(() => this.setState({ show: true }));
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
this.customEmojis.addListener(this.updateCustomEmojis);
this.updateFrequentlyUsed();
this.updateCustomEmojis();
}
shouldComponentUpdate(nextProps, nextState) {
const { frequentlyUsed, customEmojis, show } = this.state;
const { width } = this.props;
if (nextState.show !== show) {
return true;
}
if (nextProps.width !== width) {
return true;
}
if (!equal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
if (!equal(nextState.customEmojis, customEmojis)) {
return true;
}
return false;
}
componentWillUnmount() {
......
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import ActionSheet from 'react-native-actionsheet';
import I18n from '../../i18n';
export default class FilesActions extends Component {
export default class FilesActions extends PureComponent {
static propTypes = {
hideActions: PropTypes.func.isRequired,
takePhoto: PropTypes.func.isRequired,
......
......@@ -56,6 +56,10 @@ export default class ReplyPreview extends Component {
username: PropTypes.string.isRequired
}
shouldComponentUpdate() {
return false;
}
close = () => {
const { close } = this.props;
close();
......
......@@ -90,6 +90,28 @@ export default class UploadModal extends Component {
return null;
}
shouldComponentUpdate(nextProps, nextState) {
const { name, description, file } = this.state;
const { window, isVisible } = this.props;
if (nextState.name !== name) {
return true;
}
if (nextState.description !== description) {
return true;
}
if (nextProps.isVisible !== isVisible) {
return true;
}
if (nextProps.window.width !== window.width) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
return false;
}
submit = () => {
const { file, submit } = this.props;
const { name, description } = this.state;
......@@ -162,12 +184,12 @@ export default class UploadModal extends Component {
<ScrollView style={styles.scrollView}>
<Image source={{ isStatic: true, uri: file.path }} style={styles.image} />
<TextInput
placeholder='File name'
placeholder={I18n.t('File_name')}
value={name}
onChangeText={value => this.setState({ name: value })}
/>
<TextInput
placeholder='File description'
placeholder={I18n.t('File_description')}
value={description}
onChangeText={value => this.setState({ description: value })}
/>
......
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert, Image
......@@ -9,6 +9,7 @@ import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import { userTyping as userTypingAction } from '../../actions/room';
import {
......@@ -59,7 +60,7 @@ const imagePickerConfig = {
typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction())
}))
export default class MessageBox extends React.PureComponent {
export default class MessageBox extends Component {
static propTypes = {
rid: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
......@@ -108,6 +109,43 @@ export default class MessageBox extends React.PureComponent {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
} = this.state;
const {
roomType, replying, editing
} = this.props;
if (nextProps.roomType !== roomType) {
return true;
}
if (nextProps.replying !== replying) {
return true;
}
if (nextProps.editing !== editing) {
return true;
}
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true;
}
if (nextState.showFilesAction !== showFilesAction) {
return true;
}
if (nextState.showSend !== showSend) {
return true;
}
if (nextState.recording !== recording) {
return true;
}
if (!equal(nextState.mentions, mentions)) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
return false;
}
onChangeText(text) {
const { typing } = this.props;
......
......@@ -6,6 +6,7 @@ import {
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Navigation } from 'react-native-navigation';
import equal from 'deep-equal';
import { setStackRoot as setStackRootAction } from '../actions';
import { logout as logoutAction } from '../actions/login';
......@@ -111,7 +112,8 @@ export default class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
showStatus: false
showStatus: false,
status: []
};
Navigation.events().bindComponent(this);
}
......@@ -127,6 +129,43 @@ export default class Sidebar extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const { status, showStatus } = this.state;
const {
Site_Name, stackRoot, user, baseUrl
} = this.props;
if (nextState.showStatus !== showStatus) {
return true;
}
if (nextProps.Site_Name !== Site_Name) {
return true;
}
if (nextProps.stackRoot !== stackRoot) {
return true;
}
if (nextProps.Site_Name !== Site_Name) {
return true;
}
if (nextProps.baseUrl !== baseUrl) {
return true;
}
if (nextProps.user && user) {
if (nextProps.user.language !== user.language) {
return true;
}
if (nextProps.user.status !== user.status) {
return true;
}
if (nextProps.user.username !== user.username) {
return true;
}
}
if (!equal(nextState.status, status)) {
return true;
}
return false;
}
handleChangeStack = (event) => {
const { stack } = event;
this.setStack(stack);
......@@ -140,22 +179,20 @@ export default class Sidebar extends Component {
}
setStatus = () => {
setTimeout(() => {
this.setState({
status: [{
id: 'online',
name: I18n.t('Online')
}, {
id: 'busy',
name: I18n.t('Busy')
}, {
id: 'away',
name: I18n.t('Away')
}, {
id: 'offline',
name: I18n.t('Invisible')
}]
});
this.setState({
status: [{
id: 'online',
name: I18n.t('Online')
}, {
id: 'busy',
name: I18n.t('Busy')
}, {
id: 'away',
name: I18n.t('Away')
}, {
id: 'offline',
name: I18n.t('Invisible')
}]
});
}
......
......@@ -7,6 +7,7 @@ import Video from 'react-native-video';
import Slider from 'react-native-slider';
import moment from 'moment';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Markdown from './Markdown';
......@@ -47,7 +48,7 @@ const styles = StyleSheet.create({
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
export default class Audio extends React.PureComponent {
export default class Audio extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
......@@ -69,6 +70,29 @@ export default class Audio extends React.PureComponent {
};
}
shouldComponentUpdate(nextProps, nextState) {
const {
currentTime, duration, paused, uri
} = this.state;
const { file } = this.props;
if (nextState.currentTime !== currentTime) {
return true;
}
if (nextState.duration !== duration) {
return true;
}
if (nextState.paused !== paused) {
return true;
}
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
return false;
}
onLoad(data) {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
}
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
export default class extends React.PureComponent {
export default class extends Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
......@@ -18,7 +19,22 @@ export default class extends React.PureComponent {
])
}
state = { modalVisible: false };
state = { modalVisible: false, isPressed: false };
shouldComponentUpdate(nextProps, nextState) {
const { modalVisible, isPressed } = this.state;
const { file } = this.props;
if (nextState.modalVisible !== modalVisible) {
return true;
}
if (nextState.isPressed !== isPressed) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
return false;
}
onPressButton() {
this.setState({
......
......@@ -155,6 +155,8 @@ export default {
Error_uploading: 'Error uploading',
Favorites: 'Favorites',
Files: 'Files',
File_description: 'File description',
File_name: 'File name',
Finish_recording: 'Finish recording',
For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
Forgot_my_password: 'Forgot my password',
......@@ -170,6 +172,7 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing',
Invalid_server_version: 'The server you\'re trying to connect is using a version that\'s not supported by the app anymore: {{currentVersion}}.\n\nWe require version {{minVersion}}',
Join_the_community: 'Join the community',
Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
......
......@@ -162,6 +162,8 @@ export default {
Error_uploading: 'Erro subindo',
Favorites: 'Favoritos',
Files: 'Arquivos',
File_description: 'Descrição do arquivo',
File_name: 'Nome do arquivo',
Finish_recording: 'Encerrar gravação',
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você precisa digitar sua senha',
Forgot_my_password: 'Esqueci minha senha',
......@@ -175,6 +177,7 @@ export default {
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
Invalid_server_version: 'O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.',
Join_the_community: 'Junte-se à comunidade',
Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',
......
......@@ -8,6 +8,7 @@ const restTypes = {
async function open({ type, rid }) {
try {
// RC 0.61.0
await SDK.api.post(`${ restTypes[type] }.open`, { roomId: rid });
return true;
} catch (e) {
......
......@@ -16,6 +16,7 @@ const getLastMessage = () => {
export default async function() {
try {
const lastMessage = getLastMessage();
// RC 0.61.0
const result = await SDK.api.get('emoji-custom');
let { emojis } = result;
emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);
......
......@@ -7,6 +7,7 @@ import defaultPermissions from '../../constants/permissions';
export default async function() {
try {
// RC 0.66.0
const result = await SDK.api.get('permissions.list');
if (!result.success) {
......
......@@ -18,6 +18,8 @@ export default function() {
return new Promise(async(resolve, reject) => {
try {
const updatedSince = lastMessage();
// subscriptions.get: Since RC 0.60.0
// rooms.get: Since RC 0.62.0
const [subscriptionsResult, roomsResult] = await (updatedSince
? Promise.all([SDK.api.get('subscriptions.get', { updatedSince }), SDK.api.get('rooms.get', { updatedSince })])
: Promise.all([SDK.api.get('subscriptions.get'), SDK.api.get('rooms.get')])
......
......@@ -16,6 +16,7 @@ function updateServer(param) {
export default async function() {
try {
const settingsParams = JSON.stringify(Object.keys(settings));
// RC 0.60.0
const result = await fetch(`${ SDK.api.url }settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
if (!result.success) {
......
......@@ -8,6 +8,7 @@ import log from '../../utils/log';
async function load({ rid: roomId, latest, t }) {
if (t === 'l') {
try {
// RC 0.51.0
const data = await SDK.driver.asyncCall('loadHistory', roomId, null, 50, latest);
if (!data || data.status === 'error') {
return [];
......@@ -23,6 +24,7 @@ async function load({ rid: roomId, latest, t }) {
if (latest) {
params = { ...params, latest: new Date(latest).toISOString() };
}
// RC 0.48.0
const data = await SDK.api.get(`${ this.roomTypeToApiType(t) }.history`, params);
if (!data || data.status === 'error') {
return [];
......
......@@ -12,6 +12,7 @@ async function load({ rid: roomId, lastOpen }) {
} else {
return [];
}
// RC 0.60.0
const { result } = await SDK.api.get('chat.syncMessages', { roomId, lastUpdate, count: 50 });
return result;
}
......
......@@ -6,6 +6,7 @@ import log from '../../utils/log';
export default async function readMessages(rid) {
const ls = new Date();
try {
// RC 0.61.0
const data = await SDK.api.post('subscriptions.read', { rid });
const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid);
database.write(() => {
......
......@@ -15,6 +15,7 @@ function _ufsComplete(fileId, store, token) {
}
function _sendFileMessage(rid, data, msg = {}) {
// RC 0.22.0
return SDK.driver.asyncCall('sendFileMessage', rid, null, data, msg);
}
......
......@@ -33,6 +33,7 @@ export const getMessage = (rid, msg = {}) => {
export async function sendMessageCall(message) {
const { _id, rid, msg } = message;
// RC 0.60.0
const data = await SDK.api.post('chat.sendMessage', { message: { _id, rid, msg } });
return data;
}
......
import { AsyncStorage, Platform } from 'react-native';
import foreach from 'lodash/forEach';
import * as SDK from '@rocket.chat/sdk';
import semver from 'semver';
import reduxStore from './createStore';
import defaultSettings from '../constants/settings';
......@@ -42,6 +43,7 @@ const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
const call = (method, ...params) => SDK.driver.asyncCall(method, ...params);
const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.66.0';
const RocketChat = {
TOKEN_KEY,
......@@ -51,6 +53,7 @@ const RocketChat = {
createChannel({
name, users, type, readOnly, broadcast
}) {
// RC 0.51.0