From d23c0555842f5091dfb293e91745e25b883d6108 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 21 Dec 2018 08:55:35 -0200 Subject: [PATCH] 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 --- README.md | 2 + app/containers/Avatar.js | 4 +- app/containers/EmojiPicker/TabBar.js | 10 +- app/containers/EmojiPicker/index.js | 37 ++-- app/containers/MessageBox/FilesActions.js | 4 +- app/containers/MessageBox/ReplyPreview.js | 4 + app/containers/MessageBox/UploadModal.js | 26 ++- app/containers/MessageBox/index.js | 42 ++++- app/containers/Sidebar.js | 71 ++++++-- app/containers/message/Audio.js | 26 ++- app/containers/message/Image.js | 22 ++- app/i18n/locales/en.js | 3 + app/i18n/locales/pt-BR.js | 3 + app/lib/methods/canOpenRoom.js | 1 + app/lib/methods/getCustomEmojis.js | 1 + app/lib/methods/getPermissions.js | 1 + app/lib/methods/getRooms.js | 2 + app/lib/methods/getSettings.js | 1 + app/lib/methods/loadMessagesForRoom.js | 2 + app/lib/methods/loadMissedMessages.js | 1 + app/lib/methods/readMessages.js | 1 + app/lib/methods/sendFileMessage.js | 1 + app/lib/methods/sendMessage.js | 1 + app/lib/rocketchat.js | 82 ++++++++- app/sagas/deepLinking.js | 20 ++- app/sagas/messages.js | 8 +- app/sagas/rooms.js | 4 +- app/sagas/selectServer.js | 11 +- app/views/CreateChannelView.js | 43 ++++- app/views/ForgotPasswordView.js | 14 ++ app/views/LoginSignupView.js | 24 ++- app/views/LoginView.js | 40 +++++ app/views/MentionedMessagesView/index.js | 20 ++- app/views/NewMessageView.js | 19 ++- app/views/NewServerView.js | 19 ++- app/views/OnboardingView/index.js | 4 + app/views/PinnedMessagesView/index.js | 20 ++- app/views/ProfileView/index.js | 11 ++ app/views/RegisterView.js | 7 + app/views/RoomActionsView/index.js | 105 ++++++++---- app/views/RoomFilesView/index.js | 20 ++- app/views/RoomInfoEditView/index.js | 19 ++- app/views/RoomInfoView/index.js | 109 +++++++----- app/views/RoomMembersView/index.js | 94 +++++++--- app/views/RoomView/Header/index.js | 37 +++- app/views/RoomView/ListView.js | 69 +++++--- app/views/RoomView/UploadProgress.js | 18 +- app/views/RoomView/index.js | 92 ++++------ app/views/RoomsListView/Header/index.js | 4 +- app/views/RoomsListView/ServerDropdown.js | 26 ++- app/views/RoomsListView/SortDropdown.js | 8 +- app/views/RoomsListView/index.js | 171 +++++++++++++------ app/views/SearchMessagesView/index.js | 22 ++- app/views/SearchMessagesView/styles.js | 6 + app/views/SelectedUsersView.js | 16 ++ app/views/SetUsernameView.js | 11 ++ app/views/SettingsView/index.js | 15 ++ app/views/SnippetedMessagesView/index.js | 19 +++ app/views/StarredMessagesView/index.js | 20 ++- e2e/14-joinpublicroom.spec.js | 199 ++++++++++++++++++++++ e2e/mocha.opts | 2 +- package-lock.json | 20 +-- package.json | 3 +- 63 files changed, 1339 insertions(+), 378 deletions(-) create mode 100644 e2e/14-joinpublicroom.spec.js diff --git a/README.md b/README.md index dd5376ae6..aa58923ab 100644 --- a/README.md +++ b/README.md @@ -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 Download on Google Play diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index fc531de1f..ab858e456 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -1,9 +1,9 @@ -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, diff --git a/app/containers/EmojiPicker/TabBar.js b/app/containers/EmojiPicker/TabBar.js index 494d547fd..c298484ba 100644 --- a/app/containers/EmojiPicker/TabBar.js +++ b/app/containers/EmojiPicker/TabBar.js @@ -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 diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index d8cbdaf55..9860dea97 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -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.state = { - frequentlyUsed: [], - customEmojis: [] - }; this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true); this.customEmojis = database.objects('customEmojis'); + this.state = { + frequentlyUsed: [], + customEmojis: [], + show: false + }; 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() { diff --git a/app/containers/MessageBox/FilesActions.js b/app/containers/MessageBox/FilesActions.js index fee149457..8973595fe 100644 --- a/app/containers/MessageBox/FilesActions.js +++ b/app/containers/MessageBox/FilesActions.js @@ -1,10 +1,10 @@ -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, diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js index b0c8e0764..8246f0801 100644 --- a/app/containers/MessageBox/ReplyPreview.js +++ b/app/containers/MessageBox/ReplyPreview.js @@ -56,6 +56,10 @@ export default class ReplyPreview extends Component { username: PropTypes.string.isRequired } + shouldComponentUpdate() { + return false; + } + close = () => { const { close } = this.props; close(); diff --git a/app/containers/MessageBox/UploadModal.js b/app/containers/MessageBox/UploadModal.js index aa4ec42e0..06417a0b8 100644 --- a/app/containers/MessageBox/UploadModal.js +++ b/app/containers/MessageBox/UploadModal.js @@ -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 { this.setState({ name: value })} /> this.setState({ description: value })} /> diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index b92c68acd..8635ab914 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -1,4 +1,4 @@ -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; diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index a9ff4af78..711df2ffb 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -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') + }] }); } diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index 0037eb57e..1a4b84fec 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -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 }); } diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 6b81127dd..c9eea974c 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -1,13 +1,14 @@ +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({ diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 8e3487be4..fb7fdbd81 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -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', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index f7e49cf89..9f0b91c9c 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -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', diff --git a/app/lib/methods/canOpenRoom.js b/app/lib/methods/canOpenRoom.js index f4a16e8d2..12b28d628 100644 --- a/app/lib/methods/canOpenRoom.js +++ b/app/lib/methods/canOpenRoom.js @@ -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) { diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js index 45d5994f5..32c159240 100644 --- a/app/lib/methods/getCustomEmojis.js +++ b/app/lib/methods/getCustomEmojis.js @@ -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); diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 0c1e40e90..95939e4da 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -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) { diff --git a/app/lib/methods/getRooms.js b/app/lib/methods/getRooms.js index f43316a5a..fe1fbdec0 100644 --- a/app/lib/methods/getRooms.js +++ b/app/lib/methods/getRooms.js @@ -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')]) diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js index 38a79b09f..dd85f3761 100644 --- a/app/lib/methods/getSettings.js +++ b/app/lib/methods/getSettings.js @@ -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) { diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js index 1258769c0..c15891724 100644 --- a/app/lib/methods/loadMessagesForRoom.js +++ b/app/lib/methods/loadMessagesForRoom.js @@ -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 []; diff --git a/app/lib/methods/loadMissedMessages.js b/app/lib/methods/loadMissedMessages.js index 0ad952cdc..243b84a02 100644 --- a/app/lib/methods/loadMissedMessages.js +++ b/app/lib/methods/loadMissedMessages.js @@ -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; } diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js index 1f4562b86..01dd06371 100644 --- a/app/lib/methods/readMessages.js +++ b/app/lib/methods/readMessages.js @@ -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(() => { diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js index 57102eeb3..29a3005ba 100644 --- a/app/lib/methods/sendFileMessage.js +++ b/app/lib/methods/sendFileMessage.js @@ -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); } diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js index 21b620d97..cd9c56202 100644 --- a/app/lib/methods/sendMessage.js +++ b/app/lib/methods/sendMessage.js @@ -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; } diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index e5bab9919..21af17e27 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,6 +1,7 @@ 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 return call(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast }); }, async createDirectMessageAndWait(username) { @@ -81,12 +84,27 @@ const RocketChat = { try { const result = await fetch(`${ server }/api/v1/info`).then(response => response.json()); if (result.success && result.info) { - return server; + if (semver.lt(result.info.version, MIN_ROCKETCHAT_VERSION)) { + return { + success: false, + message: 'Invalid_server_version', + messageOptions: { + currentVersion: result.info.version, + minVersion: MIN_ROCKETCHAT_VERSION + } + }; + } + return { + success: true + }; } } catch (e) { log('testServer', e); } - throw new Error({ error: 'invalid server' }); + return { + success: false, + message: 'The_URL_is_invalid' + }; }, _setUser(ddpMessage) { this.activeUsers = this.activeUsers || {}; @@ -235,14 +253,17 @@ const RocketChat = { }, register(credentials) { + // RC 0.50.0 return SDK.api.post('users.register', credentials, false); }, setUsername(username) { + // RC 0.51.0 return call('setUsername', username); }, forgotPassword(email) { + // RC 0.64.0 return SDK.api.post('users.forgotPassword', { email }, false); }, @@ -288,6 +309,7 @@ const RocketChat = { async login(params) { try { + // RC 0.64.0 return await SDK.api.login(params); } catch (e) { reduxStore.dispatch(loginFailure(e)); @@ -302,6 +324,7 @@ const RocketChat = { console.log('logout -> removePushToken -> catch -> error', error); } try { + // RC 0.60.0 await SDK.api.logout(); } catch (error) { console.log('​logout -> api logout -> catch -> error', error); @@ -343,6 +366,7 @@ const RocketChat = { type, appName: 'chat.rocket.reactnative' // TODO: try to get from config file }; + // RC 0.60.0 return SDK.api.post('push.token', data); } return resolve(); @@ -351,6 +375,7 @@ const RocketChat = { removePushToken() { const token = getDeviceToken(); if (token) { + // RC 0.60.0 return SDK.api.del('push.token', { token }); } return Promise.resolve(); @@ -430,14 +455,17 @@ const RocketChat = { }, spotlight(search, usernames, type) { + // RC 0.51.0 return call('spotlight', search, usernames, type); }, createDirectMessage(username) { + // RC 0.59.0 return SDK.api.post('im.create', { username }); }, joinRoom(roomId) { // TODO: join code + // RC 0.48.0 return SDK.api.post('channels.join', { roomId }); }, sendFileMessage, @@ -471,22 +499,28 @@ const RocketChat = { }, deleteMessage(message) { const { _id, rid } = message; + // RC 0.48.0 return SDK.api.post('chat.delete', { roomId: rid, msgId: _id }); }, editMessage(message) { const { _id, msg, rid } = message; + // RC 0.49.0 return SDK.api.post('chat.update', { roomId: rid, msgId: _id, text: msg }); }, toggleStarMessage(message) { if (message.starred) { + // RC 0.59.0 return SDK.api.post('chat.unStarMessage', { messageId: message._id }); } + // RC 0.59.0 return SDK.api.post('chat.starMessage', { messageId: message._id }); }, togglePinMessage(message) { if (message.pinned) { + // RC 0.59.0 return SDK.api.post('chat.unPinMessage', { messageId: message._id }); } + // RC 0.59.0 return SDK.api.post('chat.pinMessage', { messageId: message._id }); }, getRoom(rid) { @@ -496,9 +530,6 @@ const RocketChat = { } return Promise.resolve(result); }, - getRoomInfo(roomId) { - return SDK.api.get('rooms.info', { roomId }); - }, async getPermalink(message) { let room; try { @@ -535,22 +566,30 @@ const RocketChat = { return call('UserPresence:setDefaultStatus', status); }, setReaction(emoji, messageId) { + // RC 0.62.2 return SDK.api.post('chat.react', { emoji, messageId }); }, toggleFavorite(roomId, favorite) { + // RC 0.64.0 return SDK.api.post('rooms.favorite', { roomId, favorite }); }, getRoomMembers(rid, allUsers) { + // RC 0.42.0 return call('getUsersOfRoom', rid, allUsers); }, getUserRoles() { + // RC 0.27.0 return call('getUserRoles'); }, getRoomCounters(roomId, t) { + // RC 0.65.0 return SDK.api.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId }); }, async getRoomMember(rid, currentUserId) { try { + if (rid === `${ currentUserId }${ currentUserId }`) { + return Promise.resolve(currentUserId); + } const membersResult = await RocketChat.getRoomMembers(rid, true); return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId)); } catch (error) { @@ -559,53 +598,71 @@ const RocketChat = { }, toggleBlockUser(rid, blocked, block) { if (block) { + // RC 0.49.0 return call('blockUser', { rid, blocked }); } + // RC 0.49.0 return call('unblockUser', { rid, blocked }); }, leaveRoom(roomId, t) { + // RC 0.48.0 return SDK.api.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId }); }, eraseRoom(roomId, t) { + // RC 0.49.0 return SDK.api.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId }); }, toggleMuteUserInRoom(rid, username, mute) { if (mute) { + // RC 0.51.0 return call('muteUserInRoom', { rid, username }); } + // RC 0.51.0 return call('unmuteUserInRoom', { rid, username }); }, toggleArchiveRoom(roomId, t, archive) { if (archive) { + // RC 0.48.0 return SDK.api.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId }); } + // RC 0.48.0 return SDK.api.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId }); }, saveRoomSettings(rid, params) { + // RC 0.55.0 return call('saveRoomSettings', rid, params); }, saveUserProfile(data) { + // RC 0.62.2 return SDK.api.post('users.updateOwnBasicInfo', { data }); }, saveUserPreferences(params) { + // RC 0.51.0 return call('saveUserPreferences', params); }, saveNotificationSettings(roomId, notifications) { + // RC 0.63.0 return SDK.api.post('rooms.saveNotification', { roomId, notifications }); }, addUsersToRoom(rid) { let { users } = reduxStore.getState().selectedUsers; users = users.map(u => u.name); + // RC 0.51.0 return call('addUsersToRoom', { rid, users }); }, hasPermission(permissions, rid) { - // get the room from realm - const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; + let roles = []; + try { + // get the room from realm + const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; + // get room roles + roles = room.roles; // eslint-disable-line prefer-destructuring + } catch (error) { + console.log('hasPermission -> error', error); + } // get permissions from realm const permissionsFiltered = database.objects('permissions') .filter(permission => permissions.includes(permission._id)); - // get room roles - const { roles } = room; // transform room roles to array const roomRoles = Array.from(Object.keys(roles), i => roles[i].value); // get user roles on the server from redux @@ -625,12 +682,15 @@ const RocketChat = { }, {}); }, getAvatarSuggestion() { + // RC 0.51.0 return call('getAvatarSuggestion'); }, resetAvatar(userId) { + // RC 0.55.0 return SDK.api.post('users.resetAvatar', { userId }); }, setAvatarFromService({ data, contentType = '', service = null }) { + // RC 0.51.0 return call('setAvatarFromService', data, contentType, service); }, async getSortPreferences() { @@ -668,6 +728,7 @@ const RocketChat = { } }, getUsernameSuggestion() { + // RC 0.65.0 return SDK.api.get('users.getUsernameSuggestion'); }, roomTypeToApiType(t) { @@ -677,6 +738,7 @@ const RocketChat = { return types[t]; }, getFiles(roomId, type, offset) { + // RC 0.59.0 return SDK.api.get(`${ this.roomTypeToApiType(type) }.files`, { roomId, offset, @@ -687,6 +749,7 @@ const RocketChat = { }); }, getMessages(roomId, type, query, offset) { + // RC 0.59.0 return SDK.api.get(`${ this.roomTypeToApiType(type) }.messages`, { roomId, query, @@ -695,6 +758,7 @@ const RocketChat = { }); }, searchMessages(roomId, searchText) { + // RC 0.60.0 return SDK.api.get('chat.search', { roomId, searchText diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index caa74f7d0..f46445b0d 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -1,7 +1,7 @@ import { AsyncStorage } from 'react-native'; import { delay } from 'redux-saga'; import { - takeLatest, take, select, put, all + takeLatest, take, select, put, all, race } from 'redux-saga/effects'; import { Navigation } from 'react-native-navigation'; @@ -12,6 +12,10 @@ import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; import EventEmitter from '../utils/events'; +const roomTypes = { + channel: 'c', direct: 'd', group: 'p' +}; + const navigate = function* navigate({ params, sameServer = true }) { if (!sameServer) { yield put(appStart('inside')); @@ -37,11 +41,12 @@ const navigate = function* navigate({ params, sameServer = true }) { } catch (error) { console.log(error); } + const [type, name] = params.path.split('/'); Navigation.push(stack, { component: { name: 'RoomView', passProps: { - rid: params.rid + rid: params.rid, name, t: roomTypes[type] } } }); @@ -78,15 +83,16 @@ const handleOpen = function* handleOpen({ params }) { // if deep link is from same server if (server === host) { if (user) { - yield take(types.SERVER.SELECT_SUCCESS); + yield race({ + typing: take(types.SERVER.SELECT_SUCCESS), + timeout: delay(3000) + }); yield navigate({ params }); } } else { // if deep link is from a different server - try { - // Verify if server is real - yield RocketChat.testServer(host); - } catch (error) { + const result = yield RocketChat.testServer(server); + if (!result.success) { return; } diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 1cdafc66f..e5d47afe5 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -74,13 +74,13 @@ const handleTogglePinRequest = function* handleTogglePinRequest({ message }) { } }; -const goRoom = function* goRoom({ rid }) { +const goRoom = function* goRoom({ rid, name }) { yield Navigation.popToRoot('RoomsListView'); Navigation.push('RoomsListView', { component: { name: 'RoomView', passProps: { - rid + rid, name, t: 'd' } } }); @@ -91,10 +91,10 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { const { username } = message.u; const subscriptions = database.objects('subscriptions').filtered('name = $0', username); if (subscriptions.length) { - yield goRoom({ rid: subscriptions[0].rid }); + yield goRoom({ rid: subscriptions[0].rid, name: username }); } else { const room = yield RocketChat.createDirectMessage(username); - yield goRoom({ rid: room.rid }); + yield goRoom({ rid: room.rid, name: username }); } yield delay(500); yield put(replyInit(message, false)); diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index defc7c563..4ea9736a3 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -120,7 +120,6 @@ const watchuserTyping = function* watchuserTyping({ status }) { const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { try { - sub.stop(); const result = yield RocketChat.leaveRoom(rid, t); if (result.success) { yield Navigation.popToRoot('RoomsListView'); @@ -136,7 +135,6 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { const handleEraseRoom = function* handleEraseRoom({ rid, t }) { try { - sub.stop(); const result = yield RocketChat.eraseRoom(rid, t); if (result.success) { yield Navigation.popToRoot('RoomsListView'); @@ -148,7 +146,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) { const root = function* root() { yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); - yield takeLatest(types.ROOM.OPEN, watchRoomOpen); + yield takeEvery(types.ROOM.OPEN, watchRoomOpen); yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom); diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index a2350ac66..189bb28d9 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -1,5 +1,5 @@ import { put, takeLatest } from 'redux-saga/effects'; -import { AsyncStorage } from 'react-native'; +import { AsyncStorage, Alert } from 'react-native'; import { Navigation } from 'react-native-navigation'; import { Provider } from 'react-redux'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; @@ -13,6 +13,7 @@ import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import log from '../utils/log'; import store from '../lib/createStore'; +import I18n from '../i18n'; let LoginSignupView = null; let LoginView = null; @@ -49,7 +50,13 @@ const handleSelectServer = function* handleSelectServer({ server }) { const handleServerRequest = function* handleServerRequest({ server }) { try { - yield RocketChat.testServer(server); + const result = yield RocketChat.testServer(server); + if (!result.success) { + Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions)); + yield put(serverFailure()); + return; + } + const loginServicesLength = yield RocketChat.getLoginServices(server); if (loginServicesLength === 0) { if (LoginView == null) { diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 193c1327f..b9a408158 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -6,6 +6,7 @@ import { } from 'react-native'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import Loading from '../containers/Loading'; import LoggedView from './View'; @@ -128,6 +129,43 @@ export default class CreateChannelView extends LoggedView { }, 600); } + shouldComponentUpdate(nextProps, nextState) { + const { + channelName, type, readOnly, broadcast + } = this.state; + const { + error, failure, isFetching, result, users + } = this.props; + if (nextState.channelName !== channelName) { + return true; + } + if (nextState.type !== type) { + return true; + } + if (nextState.readOnly !== readOnly) { + return true; + } + if (nextState.broadcast !== broadcast) { + return true; + } + if (nextProps.failure !== failure) { + return true; + } + if (nextProps.isFetching !== isFetching) { + return true; + } + if (!equal(nextProps.error, error)) { + return true; + } + if (!equal(nextProps.result, result)) { + return true; + } + if (!equal(nextProps.users, users)) { + return true; + } + return false; + } + componentDidUpdate(prevProps) { const { isFetching, failure, error, result, componentId @@ -139,13 +177,14 @@ export default class CreateChannelView extends LoggedView { const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); showErrorAlert(msg); } else { - const { rid } = result; + const { type } = this.state; + const { rid, name } = result; await Navigation.dismissModal(componentId); Navigation.push('RoomsListView', { component: { name: 'RoomView', passProps: { - rid + rid, name, t: type ? 'p' : 'c' } } }); diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index c099539ac..9a9e4dd71 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -44,6 +44,20 @@ export default class ForgotPasswordView extends LoggedView { }, 600); } + shouldComponentUpdate(nextProps, nextState) { + const { email, invalidEmail, isFetching } = this.state; + if (nextState.email !== email) { + return true; + } + if (nextState.invalidEmail !== invalidEmail) { + return true; + } + if (nextState.isFetching !== isFetching) { + return true; + } + return false; + } + componentWillUnmount() { if (this.timeout) { clearTimeout(this.timeout); diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index 084f9cc3c..7c54ad21e 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -8,6 +8,7 @@ import { Navigation } from 'react-native-navigation'; import { Base64 } from 'js-base64'; import SafeAreaView from 'react-native-safe-area-view'; import { gestureHandlerRootHOC, RectButton, BorderlessButton } from 'react-native-gesture-handler'; +import equal from 'deep-equal'; import LoggedView from './View'; import sharedStyles from './Styles'; @@ -94,7 +95,6 @@ const SERVICES_COLLAPSED_HEIGHT = 174; @connect(state => ({ server: state.server.server, - isFetching: state.login.isFetching, Site_Name: state.settings.Site_Name, services: state.login.services })) @@ -116,7 +116,6 @@ export default class LoginSignupView extends LoggedView { static propTypes = { componentId: PropTypes.string, - isFetching: PropTypes.bool, server: PropTypes.string, services: PropTypes.object, Site_Name: PropTypes.string @@ -133,6 +132,27 @@ export default class LoginSignupView extends LoggedView { this.setTitle(componentId, Site_Name); } + shouldComponentUpdate(nextProps, nextState) { + const { collapsed, servicesHeight } = this.state; + const { server, Site_Name, services } = this.props; + if (nextState.collapsed !== collapsed) { + return true; + } + if (nextState.servicesHeight !== servicesHeight) { + return true; + } + if (nextProps.server !== server) { + return true; + } + if (nextProps.Site_Name !== Site_Name) { + return true; + } + if (!equal(nextProps.services, services)) { + return true; + } + return false; + } + componentDidUpdate(prevProps) { const { componentId, Site_Name } = this.props; if (Site_Name && prevProps.Site_Name !== Site_Name) { diff --git a/app/views/LoginView.js b/app/views/LoginView.js index 1f6947f05..c4b6a7296 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -126,6 +126,46 @@ export default class LoginView extends LoggedView { } } + shouldComponentUpdate(nextProps, nextState) { + const { + user, password, code, showTOTP + } = this.state; + const { + isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder + } = this.props; + if (nextState.user !== user) { + return true; + } + if (nextState.password !== password) { + return true; + } + if (nextState.code !== code) { + return true; + } + if (nextState.showTOTP !== showTOTP) { + return true; + } + if (nextProps.isFetching !== isFetching) { + return true; + } + if (nextProps.failure !== failure) { + return true; + } + if (nextProps.Site_Name !== Site_Name) { + return true; + } + if (nextProps.Accounts_EmailOrUsernamePlaceholder !== Accounts_EmailOrUsernamePlaceholder) { + return true; + } + if (nextProps.Accounts_PasswordPlaceholder !== Accounts_PasswordPlaceholder) { + return true; + } + if (!equal(nextProps.error, error)) { + return true; + } + return false; + } + componentWillUnmount() { if (this.timeout) { clearTimeout(this.timeout); diff --git a/app/views/MentionedMessagesView/index.js b/app/views/MentionedMessagesView/index.js index 524b69b4e..e831c408b 100644 --- a/app/views/MentionedMessagesView/index.js +++ b/app/views/MentionedMessagesView/index.js @@ -12,11 +12,11 @@ import RCActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; import RocketChat from '../../lib/rocketchat'; -import database from '../../lib/realm'; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', customEmojis: state.customEmojis, + room: state.room, user: { id: state.login.user && state.login.user.id, username: state.login.user && state.login.user.username, @@ -39,18 +39,16 @@ export default class MentionedMessagesView extends LoggedView { } static propTypes = { - rid: PropTypes.string, user: PropTypes.object, baseUrl: PropTypes.string, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + room: PropTypes.object } constructor(props) { super('StarredMessagesView', props); - this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid); this.state = { loading: false, - room: this.rooms[0], messages: [] }; } @@ -60,12 +58,19 @@ export default class MentionedMessagesView extends LoggedView { } shouldComponentUpdate(nextProps, nextState) { - return !equal(this.state, nextState); + const { loading, messages } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; } load = async() => { const { - messages, total, loading, room + messages, total, loading } = this.state; const { user } = this.props; if (messages.length === total || loading) { @@ -75,6 +80,7 @@ export default class MentionedMessagesView extends LoggedView { this.setState({ loading: true }); try { + const { room } = this.props; const result = await RocketChat.getMessages( room.rid, room.t, diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index 288a1d0d2..31c6ef327 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; +import equal from 'deep-equal'; import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; @@ -85,6 +86,14 @@ export default class NewMessageView extends LoggedView { Navigation.events().bindComponent(this); } + shouldComponentUpdate(nextProps, nextState) { + const { search } = this.state; + if (!equal(nextState.search, search)) { + return true; + } + return false; + } + componentWillUnmount() { this.updateState.stop(); this.data.removeAllListeners(); @@ -94,12 +103,10 @@ export default class NewMessageView extends LoggedView { this.search(text); } - onPressItem = (item) => { + onPressItem = async(item) => { const { onPressItem } = this.props; - this.dismiss(); - setTimeout(() => { - onPressItem(item); - }, 600); + await this.dismiss(); + onPressItem(item); } navigationButtonPressed = ({ buttonId }) => { @@ -110,7 +117,7 @@ export default class NewMessageView extends LoggedView { dismiss = () => { const { componentId } = this.props; - Navigation.dismissModal(componentId); + return Navigation.dismissModal(componentId); } // eslint-disable-next-line react/sort-comp diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index f8c0c304c..8fc62cd8b 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - Text, ScrollView, Keyboard, Image, Alert, StyleSheet, TouchableOpacity + Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -58,8 +58,7 @@ const styles = StyleSheet.create({ const defaultServer = 'https://open.rocket.chat'; @connect(state => ({ - connecting: state.server.connecting, - failure: state.server.failure + connecting: state.server.connecting }), dispatch => ({ connectServer: server => dispatch(serverRequest(server)) })) @@ -79,7 +78,6 @@ export default class NewServerView extends LoggedView { componentId: PropTypes.string, server: PropTypes.string, connecting: PropTypes.bool.isRequired, - failure: PropTypes.bool.isRequired, connectServer: PropTypes.func.isRequired } @@ -103,11 +101,16 @@ export default class NewServerView extends LoggedView { } } - componentWillReceiveProps(nextProps) { - const { failure } = this.props; - if (nextProps.failure && nextProps.failure !== failure) { - Alert.alert(I18n.t('Oops'), I18n.t('The_URL_is_invalid')); + shouldComponentUpdate(nextProps, nextState) { + const { text } = this.state; + const { connecting } = this.props; + if (nextState.text !== text) { + return true; } + if (nextProps.connecting !== connecting) { + return true; + } + return false; } componentWillUnmount() { diff --git a/app/views/OnboardingView/index.js b/app/views/OnboardingView/index.js index 63eab53d4..a9746b54c 100644 --- a/app/views/OnboardingView/index.js +++ b/app/views/OnboardingView/index.js @@ -68,6 +68,10 @@ export default class OnboardingView extends LoggedView { EventEmitter.addEventListener('NewServer', this.handleNewServerEvent); } + shouldComponentUpdate() { + return false; + } + componentWillUnmount() { const { selectServer, previousServer, currentServer, adding, finishAdd diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js index 544ce1caf..91063abe7 100644 --- a/app/views/PinnedMessagesView/index.js +++ b/app/views/PinnedMessagesView/index.js @@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; import RocketChat from '../../lib/rocketchat'; -import database from '../../lib/realm'; const PIN_INDEX = 0; const CANCEL_INDEX = 1; @@ -22,6 +21,7 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')]; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', customEmojis: state.customEmojis, + room: state.room, user: { id: state.login.user && state.login.user.id, username: state.login.user && state.login.user.username, @@ -44,18 +44,16 @@ export default class PinnedMessagesView extends LoggedView { } static propTypes = { - rid: PropTypes.string, user: PropTypes.object, baseUrl: PropTypes.string, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + room: PropTypes.object } constructor(props) { super('PinnedMessagesView', props); - this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid); this.state = { loading: false, - room: this.rooms[0], messages: [] }; } @@ -65,7 +63,14 @@ export default class PinnedMessagesView extends LoggedView { } shouldComponentUpdate(nextProps, nextState) { - return !equal(this.state, nextState); + const { loading, messages } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; } onLongPress = (message) => { @@ -101,7 +106,7 @@ export default class PinnedMessagesView extends LoggedView { load = async() => { const { - messages, total, loading, room + messages, total, loading } = this.state; if (messages.length === total || loading) { return; @@ -110,6 +115,7 @@ export default class PinnedMessagesView extends LoggedView { this.setState({ loading: true }); try { + const { room } = this.props; const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length); if (result.success) { this.setState(prevState => ({ diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 527727447..181757ede 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -11,6 +11,7 @@ import ImagePicker from 'react-native-image-crop-picker'; import RNPickerSelect from 'react-native-picker-select'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import LoggedView from '../View'; import KeyboardView from '../../presentation/KeyboardView'; @@ -118,6 +119,16 @@ export default class ProfileView extends LoggedView { } } + shouldComponentUpdate(nextProps, nextState) { + if (!equal(nextState, this.state)) { + return true; + } + if (!equal(nextProps, this.props)) { + return true; + } + return false; + } + componentWillUnmount() { BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); } diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index debd84e93..7c71df3e3 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -25,6 +25,8 @@ let TermsServiceView = null; let PrivacyPolicyView = null; let LegalView = null; +const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving']; + @connect(null, dispatch => ({ loginRequest: params => dispatch(loginRequestAction(params)) })) @@ -67,6 +69,11 @@ export default class RegisterView extends LoggedView { }, 600); } + shouldComponentUpdate(nextProps, nextState) { + // eslint-disable-next-line react/destructuring-assignment + return shouldUpdateState.some(key => nextState[key] !== this.state[key]); + } + componentDidUpdate(prevProps) { const { componentId, Site_Name } = this.props; if (Site_Name && prevProps.Site_Name !== Site_Name) { diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 4a3a4c100..277319723 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -9,6 +9,7 @@ import { connect, Provider } from 'react-redux'; import { Navigation } from 'react-native-navigation'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import { leaveRoom as leaveRoomAction } from '../../actions/room'; import LoggedView from '../View'; @@ -33,7 +34,8 @@ const modules = {}; @connect(state => ({ userId: state.login.user && state.login.user.id, username: state.login.user && state.login.user.username, - baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', + room: state.room }), dispatch => ({ leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)) })) @@ -58,15 +60,16 @@ export default class RoomActionsView extends LoggedView { componentId: PropTypes.string, userId: PropTypes.string, username: PropTypes.string, + room: PropTypes.object, leaveRoom: PropTypes.func } constructor(props) { super('RoomActionsView', props); - const { rid } = props; + const { rid, room } = props; this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.state = { - room: this.rooms[0] || {}, + room, membersCount: 0, member: {}, joined: false, @@ -86,14 +89,34 @@ export default class RoomActionsView extends LoggedView { } catch (error) { console.log('RoomActionsView -> getRoomCounters -> error', error); } - } - - if (room.t === 'd') { + } else if (room.t === 'd') { this.updateRoomMember(); } this.rooms.addListener(this.updateRoom); } + shouldComponentUpdate(nextProps, nextState) { + const { + room, membersCount, member, joined, canViewMembers + } = this.state; + if (nextState.membersCount !== membersCount) { + return true; + } + if (nextState.joined !== joined) { + return true; + } + if (nextState.canViewMembers !== canViewMembers) { + return true; + } + if (!equal(nextState.room, room)) { + return true; + } + if (!equal(nextState.member, member)) { + return true; + } + return false; + } + componentWillUnmount() { this.rooms.removeAllListeners(); } @@ -109,7 +132,8 @@ export default class RoomActionsView extends LoggedView { Navigation.push(componentId, { component: { name: item.route, - passProps: item.params + passProps: item.params, + options: item.navigationOptions } }); } @@ -156,11 +180,20 @@ export default class RoomActionsView extends LoggedView { } get sections() { - const { room, membersCount, canViewMembers } = this.state; + const { + room, membersCount, canViewMembers, joined + } = this.state; const { rid, t, blocker, notifications } = room; + const notificationsAction = { + icon: `ios-notifications${ notifications ? '' : '-off' }`, + name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`), + event: () => this.toggleNotifications(), + testID: 'room-actions-notifications' + }; + const sections = [{ data: [{ icon: 'ios-star', @@ -193,7 +226,6 @@ export default class RoomActionsView extends LoggedView { icon: 'ios-attach', name: I18n.t('Files'), route: 'RoomFilesView', - params: { rid }, testID: 'room-actions-files', require: () => require('../RoomFilesView').default }, @@ -201,7 +233,6 @@ export default class RoomActionsView extends LoggedView { icon: 'ios-at', name: I18n.t('Mentions'), route: 'MentionedMessagesView', - params: { rid }, testID: 'room-actions-mentioned', require: () => require('../MentionedMessagesView').default }, @@ -209,7 +240,6 @@ export default class RoomActionsView extends LoggedView { icon: 'ios-star', name: I18n.t('Starred'), route: 'StarredMessagesView', - params: { rid }, testID: 'room-actions-starred', require: () => require('../StarredMessagesView').default }, @@ -231,7 +261,6 @@ export default class RoomActionsView extends LoggedView { icon: 'ios-pin', name: I18n.t('Pinned'), route: 'PinnedMessagesView', - params: { rid }, testID: 'room-actions-pinned', require: () => require('../PinnedMessagesView').default }, @@ -242,12 +271,6 @@ export default class RoomActionsView extends LoggedView { params: { rid }, testID: 'room-actions-snippeted', require: () => require('../SnippetedMessagesView').default - }, - { - icon: `ios-notifications${ notifications ? '' : '-off' }`, - name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`), - event: () => this.toggleNotifications(), - testID: 'room-actions-notifications' } ], renderItem: this.renderItem @@ -266,6 +289,7 @@ export default class RoomActionsView extends LoggedView { ], renderItem: this.renderItem }); + sections[2].data.push(notificationsAction); } else if (t === 'c' || t === 'p') { const actions = []; @@ -273,7 +297,7 @@ export default class RoomActionsView extends LoggedView { actions.push({ icon: 'ios-people', name: I18n.t('Members'), - description: `${ membersCount } ${ I18n.t('members') }`, + description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null, route: 'RoomMembersView', params: { rid }, testID: 'room-actions-members', @@ -290,29 +314,42 @@ export default class RoomActionsView extends LoggedView { nextAction: 'ADD_USER', rid }, + navigationOptions: { + topBar: { + title: { + text: I18n.t('Add_user') + } + } + }, testID: 'room-actions-add-user', require: () => require('../SelectedUsersView').default }); } sections[2].data = [...actions, ...sections[2].data]; - sections.push({ - data: [ - { - icon: 'block', - name: I18n.t('Leave_channel'), - type: 'danger', - event: () => this.leaveChannel(), - testID: 'room-actions-leave-channel' - } - ], - renderItem: this.renderItem - }); + + if (joined) { + sections[2].data.push(notificationsAction); + sections.push({ + data: [ + { + icon: 'block', + name: I18n.t('Leave_channel'), + type: 'danger', + event: () => this.leaveChannel(), + testID: 'room-actions-leave-channel' + } + ], + renderItem: this.renderItem + }); + } } return sections; } updateRoom = () => { - this.setState({ room: this.rooms[0] || {} }); + if (this.rooms.length > 0) { + this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) }); + } } updateRoomMember = async() => { @@ -322,10 +359,10 @@ export default class RoomActionsView extends LoggedView { try { const member = await RocketChat.getRoomMember(rid, userId); - this.setState({ member }); + this.setState({ member: member || {} }); } catch (e) { log('RoomActions updateRoomMember', e); - return {}; + this.setState({ member: {} }); } } diff --git a/app/views/RoomFilesView/index.js b/app/views/RoomFilesView/index.js index 354c772ca..f8f981c0d 100644 --- a/app/views/RoomFilesView/index.js +++ b/app/views/RoomFilesView/index.js @@ -11,12 +11,12 @@ import Message from '../../containers/message/Message'; import RCActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; -import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', customEmojis: state.customEmojis, + room: state.room, user: { id: state.login.user && state.login.user.id, username: state.login.user && state.login.user.username, @@ -39,18 +39,16 @@ export default class RoomFilesView extends LoggedView { } static propTypes = { - rid: PropTypes.string, user: PropTypes.object, baseUrl: PropTypes.string, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + room: PropTypes.object } constructor(props) { super('RoomFilesView', props); - this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid); this.state = { loading: false, - room: this.rooms[0], messages: [] }; } @@ -60,12 +58,19 @@ export default class RoomFilesView extends LoggedView { } shouldComponentUpdate(nextProps, nextState) { - return !equal(this.state, nextState); + const { loading, messages } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; } load = async() => { const { - messages, total, loading, room + messages, total, loading } = this.state; if (messages.length === total || loading) { return; @@ -74,6 +79,7 @@ export default class RoomFilesView extends LoggedView { this.setState({ loading: true }); try { + const { room } = this.props; const result = await RocketChat.getFiles(room.rid, room.t, messages.length); if (result.success) { this.setState(prevState => ({ diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index afc4a8b8a..f0b6701a2 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -5,6 +5,7 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import { eraseRoom as eraseRoomAction } from '../../actions/room'; import LoggedView from '../View'; @@ -67,7 +68,7 @@ export default class RoomInfoEditView extends LoggedView { this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.permissions = {}; this.state = { - room: this.rooms[0] || {}, + room: JSON.parse(JSON.stringify(this.rooms[0] || {})), name: '', description: '', topic: '', @@ -90,12 +91,26 @@ export default class RoomInfoEditView extends LoggedView { this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid); } + shouldComponentUpdate(nextProps, nextState) { + const { room } = this.state; + if (!equal(nextState, this.state)) { + return true; + } + if (!equal(nextState.room, room)) { + return true; + } + if (!equal(nextProps, this.props)) { + return true; + } + return false; + } + componentWillUnmount() { this.rooms.removeAllListeners(); } updateRoom = () => { - this.setState({ room: this.rooms[0] || {} }); + this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0] || {})) }); } init = () => { diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 00d41e966..1e9b3d8b4 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -6,6 +6,7 @@ import moment from 'moment'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; +import equal from 'deep-equal'; import LoggedView from '../View'; import Status from '../../containers/status'; @@ -40,9 +41,10 @@ let RoomInfoEditView = null; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', userId: state.login.user && state.login.user.id, - activeUsers: state.activeUsers, + activeUsers: state.activeUsers, // TODO: remove it Message_TimeFormat: state.settings.Message_TimeFormat, - allRoles: state.roles + allRoles: state.roles, + room: state.room })) /** @extends React.Component */ export default class RoomInfoView extends LoggedView { @@ -66,29 +68,34 @@ export default class RoomInfoView extends LoggedView { baseUrl: PropTypes.string, activeUsers: PropTypes.object, Message_TimeFormat: PropTypes.string, - allRoles: PropTypes.object + allRoles: PropTypes.object, + room: PropTypes.object } constructor(props) { super('RoomInfoView', props); - const { rid } = props; + const { rid, room } = props; this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.sub = { unsubscribe: () => {} }; this.state = { - room: {}, + room, roomUser: {}, roles: [] }; Navigation.events().bindComponent(this); } - componentDidMount() { - this.updateRoom(); + async componentDidMount() { this.rooms.addListener(this.updateRoom); - const [room] = this.rooms; + let room = {}; + if (this.rooms.length > 0) { + room = this.rooms[0]; // eslint-disable-line prefer-destructuring + } else { + room = this.state.room; // eslint-disable-line + } const { componentId } = this.props; const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); if (permissions[PERMISSION_EDIT_ROOM]) { @@ -102,6 +109,57 @@ export default class RoomInfoView extends LoggedView { } }); } + + // get user of room + if (room) { + if (room.t === 'd') { + try { + const { userId, activeUsers } = this.props; + const roomUser = await RocketChat.getRoomMember(room.rid, userId); + this.setState({ roomUser: roomUser || {} }); + const username = room.name; + + const activeUser = activeUsers[roomUser._id]; + if (!activeUser || !activeUser.utcOffset) { + // get full user data looking for utcOffset + // will be catched by .on('users) and saved on activeUsers reducer + this.getFullUserData(username); + } + + // get all users roles + // needs to be changed by a better method + const allUsersRoles = await RocketChat.getUserRoles(); + const userRoles = allUsersRoles.find(user => user.username === username); + if (userRoles) { + this.setState({ roles: userRoles.roles || [] }); + } + } catch (e) { + log('RoomInfoView.componentDidMount', e); + } + } + } + } + + shouldComponentUpdate(nextProps, nextState) { + const { + room, roomUser, roles + } = this.state; + const { activeUsers } = this.props; + if (!equal(nextState.room, room)) { + return true; + } + if (!equal(nextState.roomUser, roomUser)) { + return true; + } + if (!equal(nextState.roles, roles)) { + return true; + } + if (roomUser._id) { + if (nextProps.activeUsers[roomUser._id] !== activeUsers[roomUser._id]) { + return true; + } + } + return false; } componentWillUnmount() { @@ -143,38 +201,9 @@ export default class RoomInfoView extends LoggedView { return t === 'd'; } - updateRoom = async() => { - const { userId, activeUsers } = this.props; - - const [room] = this.rooms; - this.setState({ room }); - - // get user of room - if (room) { - if (room.t === 'd') { - try { - const roomUser = await RocketChat.getRoomMember(room.rid, userId); - this.setState({ roomUser: roomUser || {} }); - const username = room.name; - - const activeUser = activeUsers[roomUser._id]; - if (!activeUser || !activeUser.utcOffset) { - // get full user data looking for utcOffset - // will be catched by .on('users) and saved on activeUsers reducer - this.getFullUserData(username); - } - - // get all users roles - // needs to be changed by a better method - const allUsersRoles = await RocketChat.getUserRoles(); - const userRoles = allUsersRoles.find(user => user.username === username); - if (userRoles) { - this.setState({ roles: userRoles.roles || [] }); - } - } catch (e) { - log('RoomInfoView.componentDidMount', e); - } - } + updateRoom = () => { + if (this.rooms.length > 0) { + this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) }); } } diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index ad7b0e87c..7a82cfbad 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -7,6 +7,7 @@ import ActionSheet from 'react-native-actionsheet'; import { connect } from 'react-redux'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import LoggedView from '../View'; import styles from './styles'; @@ -21,7 +22,8 @@ import SearchBox from '../../containers/SearchBox'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; @connect(state => ({ - baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', + room: state.room })) /** @extends React.Component */ export default class RoomMembersView extends LoggedView { @@ -48,7 +50,8 @@ export default class RoomMembersView extends LoggedView { componentId: PropTypes.string, rid: PropTypes.string, members: PropTypes.array, - baseUrl: PropTypes.string + baseUrl: PropTypes.string, + room: PropTypes.object } constructor(props) { @@ -57,7 +60,7 @@ export default class RoomMembersView extends LoggedView { this.CANCEL_INDEX = 0; this.MUTE_INDEX = 1; this.actionSheetOptions = ['']; - const { rid, members } = props; + const { rid, members, room } = props; this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.permissions = RocketChat.hasPermission(['mute-user'], rid); this.state = { @@ -67,7 +70,8 @@ export default class RoomMembersView extends LoggedView { members, membersFiltered: [], userLongPressed: {}, - room: this.rooms[0] || {} + room, + options: [] }; Navigation.events().bindComponent(this); } @@ -77,6 +81,34 @@ export default class RoomMembersView extends LoggedView { this.rooms.addListener(this.updateRoom); } + shouldComponentUpdate(nextProps, nextState) { + const { + allUsers, filtering, members, membersFiltered, userLongPressed, room, options + } = this.state; + if (nextState.allUsers !== allUsers) { + return true; + } + if (nextState.filtering !== filtering) { + return true; + } + if (!equal(nextState.members, members)) { + return true; + } + if (!equal(nextState.options, options)) { + return true; + } + if (!equal(nextState.membersFiltered, membersFiltered)) { + return true; + } + if (!equal(nextState.userLongPressed, userLongPressed)) { + return true; + } + if (!equal(nextState.room.muted, room.muted)) { + return true; + } + return false; + } + componentWillUnmount() { this.rooms.removeAllListeners(); } @@ -118,11 +150,11 @@ export default class RoomMembersView extends LoggedView { try { const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username); if (subscriptions.length) { - this.goRoom({ rid: subscriptions[0].rid }); + this.goRoom({ rid: subscriptions[0].rid, name: item.username }); } else { const result = await RocketChat.createDirectMessage(item.username); if (result.success) { - this.goRoom({ rid: result.room._id }); + this.goRoom({ rid: result.room._id, name: item.username }); } } } catch (e) { @@ -134,21 +166,25 @@ export default class RoomMembersView extends LoggedView { if (!this.permissions['mute-user']) { return; } - const { room } = this.state; - const { muted } = room; + try { + const { room } = this.state; + const { muted } = room; - this.actionSheetOptions = [I18n.t('Cancel')]; - const userIsMuted = !!muted.find(m => m.value === user.username); - user.muted = userIsMuted; - if (userIsMuted) { - this.actionSheetOptions.push(I18n.t('Unmute')); - } else { - this.actionSheetOptions.push(I18n.t('Mute')); - } - this.setState({ userLongPressed: user }); - Vibration.vibrate(50); - if (this.actionSheet && this.actionSheet.show) { - this.actionSheet.show(); + const options = [I18n.t('Cancel')]; + const userIsMuted = !!muted.find(m => m.value === user.username); + user.muted = userIsMuted; + if (userIsMuted) { + options.push(I18n.t('Unmute')); + } else { + options.push(I18n.t('Mute')); + } + this.setState({ userLongPressed: user, options }); + Vibration.vibrate(50); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } + } catch (error) { + console.log('onLongPressUser -> catch -> error', error); } } @@ -159,19 +195,21 @@ export default class RoomMembersView extends LoggedView { this.setState({ allUsers: status, members }); } - updateRoom = async() => { - const [room] = this.rooms; - await this.setState({ room }); + updateRoom = () => { + if (this.rooms.length > 0) { + const [room] = this.rooms; + this.setState({ room }); + } } - goRoom = async({ rid }) => { + goRoom = async({ rid, name }) => { const { componentId } = this.props; await Navigation.popToRoot(componentId); Navigation.push('RoomsListView', { component: { name: 'RoomView', passProps: { - rid + rid, name, t: 'd' } } }); @@ -219,7 +257,9 @@ export default class RoomMembersView extends LoggedView { } render() { - const { filtering, members, membersFiltered } = this.state; + const { + filtering, members, membersFiltered, options + } = this.state; return ( this.actionSheet = o} title={I18n.t('Actions')} - options={this.actionSheetOptions} + options={options} cancelButtonIndex={this.CANCEL_INDEX} onPress={this.handleActionPress} /> diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index 813c145bc..5105d9461 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { View, Text, StyleSheet, Image, Platform, LayoutAnimation @@ -81,7 +81,7 @@ const styles = StyleSheet.create({ status }; }) -export default class RoomHeaderView extends PureComponent { +export default class RoomHeaderView extends Component { static propTypes = { title: PropTypes.string, type: PropTypes.string, @@ -90,10 +90,37 @@ export default class RoomHeaderView extends PureComponent { status: PropTypes.string }; + shouldComponentUpdate(nextProps) { + const { + type, title, status, usersTyping, window + } = this.props; + if (nextProps.type !== type) { + return true; + } + if (nextProps.title !== title) { + return true; + } + if (nextProps.status !== status) { + return true; + } + if (nextProps.window.width !== window.width) { + return true; + } + if (nextProps.window.height !== window.height) { + return true; + } + if (!equal(nextProps.usersTyping, usersTyping)) { + return true; + } + return false; + } + componentDidUpdate(prevProps) { - const { usersTyping } = this.props; - if (!equal(prevProps.usersTyping, usersTyping)) { - LayoutAnimation.easeInEaseOut(); + if (isIOS()) { + const { usersTyping } = this.props; + if (!equal(prevProps.usersTyping, usersTyping)) { + LayoutAnimation.easeInEaseOut(); + } } } diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js index 6f11d236c..28935f5ab 100644 --- a/app/views/RoomView/ListView.js +++ b/app/views/RoomView/ListView.js @@ -1,6 +1,8 @@ import { ListView as OldList } from 'realm/react-native'; import React from 'react'; -import { ScrollView, ListView as OldList2, ImageBackground } from 'react-native'; +import { + ScrollView, ListView as OldList2, ImageBackground, ActivityIndicator +} from 'react-native'; import moment from 'moment'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -9,7 +11,9 @@ import Separator from './Separator'; import styles from './styles'; import database from '../../lib/realm'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import throttle from '../../utils/throttle'; +import debounce from '../../utils/debounce'; +import RocketChat from '../../lib/rocketchat'; +import log from '../../utils/log'; const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100; @@ -24,34 +28,38 @@ export class DataSource extends OldList.DataSource { } } -const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id || r1._updatedAt.toISOString() !== r2._updatedAt.toISOString() }); +const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); export class List extends React.Component { static propTypes = { onEndReached: PropTypes.func, renderFooter: PropTypes.func, renderRow: PropTypes.func, - room: PropTypes.string, - end: PropTypes.bool, - loadingMore: PropTypes.bool + room: PropTypes.object }; constructor(props) { super(props); this.data = database .objects('messages') - .filtered('rid = $0', props.room) + .filtered('rid = $0', props.room.rid) .sorted('ts', true); + this.state = { + loading: true, + loadingMore: false, + end: false + }; this.dataSource = ds.cloneWithRows(this.data); } componentDidMount() { + this.updateState(); this.data.addListener(this.updateState); } - shouldComponentUpdate(nextProps) { - const { end, loadingMore } = this.props; - return end !== nextProps.end || loadingMore !== nextProps.loadingMore; + shouldComponentUpdate(nextProps, nextState) { + const { loadingMore, loading, end } = this.state; + return end !== nextState.end || loadingMore !== nextState.loadingMore || loading !== nextState.loading; } componentWillUnmount() { @@ -60,16 +68,39 @@ export class List extends React.Component { } // eslint-disable-next-line react/sort-comp - updateState = throttle(() => { - // this.setState({ + updateState = debounce(() => { + this.setState({ loading: true }); this.dataSource = this.dataSource.cloneWithRows(this.data); - // LayoutAnimation.easeInEaseOut(); - this.forceUpdate(); - // }); - }, 1000); + this.setState({ loading: false }); + }, 300); + + onEndReached = async() => { + const { loadingMore, end } = this.state; + if (loadingMore || end || this.data.length < 50) { + return; + } + + this.setState({ loadingMore: true }); + const { room } = this.props; + try { + const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts }); + this.setState({ end: result.length < 50, loadingMore: false }); + } catch (e) { + this.setState({ loadingMore: false }); + log('ListView.onEndReached', e); + } + } + + renderFooter = () => { + const { loadingMore, loading } = this.state; + if (loadingMore || loading) { + return ; + } + return null; + } render() { - const { renderFooter, onEndReached, renderRow } = this.props; + const { renderRow } = this.props; return ( item._id} onEndReachedThreshold={100} - renderFooter={renderFooter} - onEndReached={() => onEndReached(this.data[this.data.length - 1])} + renderFooter={this.renderFooter} + onEndReached={this.onEndReached} dataSource={this.dataSource} renderRow={(item, previousItem) => renderRow(item, previousItem)} initialListSize={1} diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.js index 306203963..becf386f1 100644 --- a/app/views/RoomView/UploadProgress.js +++ b/app/views/RoomView/UploadProgress.js @@ -5,6 +5,7 @@ import { import PropTypes from 'prop-types'; import Icon from 'react-native-vector-icons/MaterialIcons'; import { responsive } from 'react-native-responsive-ui'; +import equal from 'deep-equal'; import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; @@ -84,6 +85,18 @@ export default class UploadProgress extends Component { }); } + shouldComponentUpdate(nextProps, nextState) { + const { uploads } = this.state; + const { window } = this.props; + if (nextProps.window.width !== window.width) { + return true; + } + if (!equal(nextState.uploads, uploads)) { + return true; + } + return false; + } + componentWillUnmount() { this.uploads.removeAllListeners(); } @@ -107,14 +120,15 @@ export default class UploadProgress extends Component { database.write(() => { item.error = false; }); - await RocketChat.sendFileMessage(rid, JSON.parse(JSON.stringify(item))); + await RocketChat.sendFileMessage(rid, item); } catch (e) { log('UploadProgess.tryAgain', e); } } updateUploads = () => { - this.setState({ uploads: this.uploads }); + const uploads = this.uploads.map(item => JSON.parse(JSON.stringify(item))); + this.setState({ uploads }); } renderItemContent = (item) => { diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index fd0ecc4c9..ae5ec244b 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -84,6 +84,8 @@ export default class RoomView extends LoggedView { token: PropTypes.string.isRequired }), rid: PropTypes.string, + name: PropTypes.string, + t: PropTypes.string, showActions: PropTypes.bool, showErrorActions: PropTypes.bool, actionMessage: PropTypes.object, @@ -100,24 +102,20 @@ export default class RoomView extends LoggedView { this.state = { loaded: false, joined: this.rooms.length > 0, - room: {}, - end: false, - loadingMore: false + room: {} }; + this.focused = true; this.onReactionPress = this.onReactionPress.bind(this); Navigation.events().bindComponent(this); } - async componentDidMount() { + componentDidMount() { if (this.rooms.length === 0 && this.rid) { - const result = await RocketChat.getRoomInfo(this.rid); - if (result.success) { - const { room } = result; - this.setState( - { room: { rid: room._id, t: room.t, name: room.name } }, - () => this.updateRoom() - ); - } + const { rid, name, t } = this.props; + this.setState( + { room: { rid, name, t } }, + () => this.updateRoom() + ); } this.rooms.addListener(this.updateRoom); this.internalSetState({ loaded: true }); @@ -125,7 +123,7 @@ export default class RoomView extends LoggedView { shouldComponentUpdate(nextProps, nextState) { const { - room, loaded, joined, end, loadingMore + room, loaded, joined } = this.state; const { showActions, showErrorActions, appState } = this.props; @@ -143,10 +141,6 @@ export default class RoomView extends LoggedView { return true; } else if (joined !== nextState.joined) { return true; - } else if (end !== nextState.end) { - return true; - } else if (loadingMore !== nextState.loadingMore) { - return true; } else if (showActions !== nextProps.showActions) { return true; } else if (showErrorActions !== nextProps.showErrorActions) { @@ -187,32 +181,18 @@ export default class RoomView extends LoggedView { componentWillUnmount() { const { closeRoom } = this.props; - this.rooms.removeAllListeners(); - if (this.onEndReached && this.onEndReached.stop) { - this.onEndReached.stop(); - } closeRoom(); + this.rooms.removeAllListeners(); } - onEndReached = async(lastRowData) => { - if (!lastRowData) { - return; - } + // eslint-disable-next-line + componentDidAppear() { + this.focused = true; + } - const { loadingMore, end } = this.state; - if (loadingMore || end) { - return; - } - - this.setState({ loadingMore: true }); - const { room } = this.state; - try { - const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: room.t, latest: lastRowData.ts }); - this.internalSetState({ end: result.length < 50, loadingMore: false }); - } catch (e) { - this.internalSetState({ loadingMore: false }); - log('RoomView.onEndReached', e); - } + // eslint-disable-next-line + componentDidDisappear() { + this.focused = false; } onMessageLongPress = (message) => { @@ -269,15 +249,19 @@ export default class RoomView extends LoggedView { } } + // eslint-disable-next-line react/sort-comp updateRoom = () => { const { openRoom, setLastOpen } = this.props; + if (!this.focused) { + return; + } if (this.rooms.length > 0) { const { room: prevRoom } = this.state; const room = JSON.parse(JSON.stringify(this.rooms[0] || {})); this.internalSetState({ room }); - if (!prevRoom.rid) { + if (!prevRoom._id) { openRoom({ ...room }); @@ -289,8 +273,10 @@ export default class RoomView extends LoggedView { } } else { const { room } = this.state; - openRoom(room); - this.internalSetState({ joined: false }); + if (room.rid) { + openRoom(room); + this.internalSetState({ joined: false }); + } } } @@ -370,7 +356,7 @@ export default class RoomView extends LoggedView { if (!joined) { return ( - + {I18n.t('You_are_in_preview_mode')} - {I18n.t('Join')} + {I18n.t('Join')} ); @@ -400,28 +386,16 @@ export default class RoomView extends LoggedView { return ; }; - renderHeader = () => { - const { loadingMore } = this.state; - if (loadingMore) { - return ; - } - return null; - } - renderList = () => { - const { loaded, end, loadingMore } = this.state; - if (!loaded) { + const { loaded, room } = this.state; + if (!loaded || !room.rid) { return ; } return ( [ , this.renderFooter() diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index 24d45b3bb..88a452572 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -18,7 +18,7 @@ import Header from './Header'; closeSort: () => dispatch(closeSortDropdown()), setSearch: searchText => dispatch(setSearchAction(searchText)) })) -export default class RoomsListHeaderView extends Component { +export default class RoomsListHeaderView extends PureComponent { static propTypes = { showServerDropdown: PropTypes.bool, showSortDropdown: PropTypes.bool, diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index 81e2322af..c8cf3d691 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Navigation } from 'react-native-navigation'; import * as SDK from '@rocket.chat/sdk'; +import equal from 'deep-equal'; import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; @@ -39,10 +40,12 @@ export default class ServerDropdown extends Component { constructor(props) { super(props); + this.servers = database.databases.serversDB.objects('servers'); this.state = { - servers: [] + servers: this.servers }; this.animatedValue = new Animated.Value(0); + this.servers.addListener(this.updateState); } componentDidMount() { @@ -51,12 +54,25 @@ export default class ServerDropdown extends Component { { toValue: 1, duration: ANIMATION_DURATION, - easing: Easing.ease, + easing: Easing.inOut(Easing.quad), useNativeDriver: true }, ).start(); - this.servers = database.databases.serversDB.objects('servers'); - this.servers.addListener(this.updateState); + } + + shouldComponentUpdate(nextProps, nextState) { + const { servers } = this.state; + const { closeServerDropdown, server } = this.props; + if (nextProps.closeServerDropdown !== closeServerDropdown) { + return true; + } + if (nextProps.server !== server) { + return true; + } + if (!equal(nextState.servers, servers)) { + return true; + } + return false; } componentDidUpdate(prevProps) { @@ -78,7 +94,7 @@ export default class ServerDropdown extends Component { { toValue: 0, duration: ANIMATION_DURATION, - easing: Easing.ease, + easing: Easing.inOut(Easing.quad), useNativeDriver: true } ).start(() => toggleServerDropdown()); diff --git a/app/views/RoomsListView/SortDropdown.js b/app/views/RoomsListView/SortDropdown.js index f011d7168..094e12139 100644 --- a/app/views/RoomsListView/SortDropdown.js +++ b/app/views/RoomsListView/SortDropdown.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { View, Text, Animated, Easing, Image, TouchableWithoutFeedback } from 'react-native'; @@ -19,7 +19,7 @@ const ANIMATION_DURATION = 200; }), dispatch => ({ setSortPreference: preference => dispatch(setPreference(preference)) })) -export default class Sort extends Component { +export default class Sort extends PureComponent { static propTypes = { closeSortDropdown: PropTypes.bool, close: PropTypes.func, @@ -41,7 +41,7 @@ export default class Sort extends Component { { toValue: 1, duration: ANIMATION_DURATION, - easing: Easing.ease, + easing: Easing.inOut(Easing.quad), useNativeDriver: true }, ).start(); @@ -95,7 +95,7 @@ export default class Sort extends Component { { toValue: 0, duration: ANIMATION_DURATION, - easing: Easing.ease, + easing: Easing.inOut(Easing.quad), useNativeDriver: true }, ).start(() => close()); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 4cdb2060c..b33363313 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -26,10 +26,12 @@ import { appStart as appStartAction } from '../../actions'; import store from '../../lib/createStore'; import Drawer from '../../Drawer'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; +import debounce from '../../utils/debounce'; const ROW_HEIGHT = 70; const SCROLL_OFFSET = 56; +const shouldUpdateProps = ['searchText', 'loadingServer', 'showServerDropdown', 'showSortDropdown', 'sortBy', 'groupByType', 'showFavorites', 'showUnread', 'useRealName', 'appState']; const isAndroid = () => Platform.OS === 'android'; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const keyExtractor = item => item.rid; @@ -161,7 +163,61 @@ export default class RoomsListView extends LoggedView { } shouldComponentUpdate(nextProps, nextState) { - return !(isEqual(this.props, nextProps) && isEqual(this.state, nextState)); + // eslint-disable-next-line react/destructuring-assignment + const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]); + if (propsUpdated) { + return true; + } + + const { loading, searching } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (nextState.searching !== searching) { + return true; + } + + const { showUnread, showFavorites, groupByType } = this.props; + if (showUnread) { + const { unread } = this.state; + if (!isEqual(nextState.unread, unread)) { + return true; + } + } + if (showFavorites) { + const { favorites } = this.state; + if (!isEqual(nextState.favorites, favorites)) { + return true; + } + } + if (groupByType) { + const { + channels, privateGroup, direct, livechat + } = this.state; + if (!isEqual(nextState.channels, channels)) { + return true; + } + if (!isEqual(nextState.privateGroup, privateGroup)) { + return true; + } + if (!isEqual(nextState.direct, direct)) { + return true; + } + if (!isEqual(nextState.livechat, livechat)) { + return true; + } + } else { + const { chats } = this.state; + if (!isEqual(nextState.chats, chats)) { + return true; + } + } + + const { search } = this.state; + if (!isEqual(nextState.search, search)) { + return true; + } + return false; } componentDidUpdate(prevProps) { @@ -190,10 +246,6 @@ export default class RoomsListView extends LoggedView { this.removeListener(this.direct); this.removeListener(this.livechat); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); - - if (this.timeout) { - clearTimeout(this.timeout); - } } navigationButtonPressed = ({ buttonId }) => { @@ -262,9 +314,7 @@ export default class RoomsListView extends LoggedView { if (showUnread) { this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)'); unread = this.removeRealmInstance(this.unread); - setTimeout(() => { - this.unread.addListener(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) })); - }); + this.unread.addListener(debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300)); } else { this.removeListener(unread); } @@ -272,9 +322,7 @@ export default class RoomsListView extends LoggedView { if (showFavorites) { this.favorites = this.data.filtered('f == true'); favorites = this.removeRealmInstance(this.favorites); - setTimeout(() => { - this.favorites.addListener(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) })); - }); + this.favorites.addListener(debounce(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }), 300)); } else { this.removeListener(favorites); } @@ -296,12 +344,10 @@ export default class RoomsListView extends LoggedView { this.livechat = this.data.filtered('t == $0', 'l'); livechat = this.removeRealmInstance(this.livechat); - setTimeout(() => { - this.channels.addListener(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) })); - this.privateGroup.addListener(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) })); - this.direct.addListener(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) })); - this.livechat.addListener(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) })); - }); + this.channels.addListener(debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300)); + this.privateGroup.addListener(debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300)); + this.direct.addListener(debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300)); + this.livechat.addListener(debounce(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }), 300)); this.removeListener(this.chats); } else { // chats @@ -312,11 +358,7 @@ export default class RoomsListView extends LoggedView { } chats = this.removeRealmInstance(this.chats); - setTimeout(() => { - this.chats.addListener(() => { - this.internalSetState({ chats: this.removeRealmInstance(this.chats) }); - }); - }); + this.chats.addListener(debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300)); this.removeListener(this.channels); this.removeListener(this.privateGroup); this.removeListener(this.direct); @@ -325,12 +367,9 @@ export default class RoomsListView extends LoggedView { // setState this.internalSetState({ - chats, unread, favorites, channels, privateGroup, direct, livechat + chats, unread, favorites, channels, privateGroup, direct, livechat, loading: false }); } - this.timeout = setTimeout(() => { - this.internalSetState({ loading: false }); - }, 200); } removeRealmInstance = (data) => { @@ -399,13 +438,13 @@ export default class RoomsListView extends LoggedView { }); } - goRoom = (rid) => { + goRoom = ({ rid, name, t }) => { this.cancelSearchingAndroid(); Navigation.push('RoomsListView', { component: { name: 'RoomView', passProps: { - rid + rid, name, t } } }); @@ -413,8 +452,8 @@ export default class RoomsListView extends LoggedView { _onPressItem = async(item = {}) => { if (!item.search) { - const { rid } = item; - return this.goRoom(rid); + const { rid, name, t } = item; + return this.goRoom({ rid, name, t }); } if (item.t === 'd') { // if user is using the search we need first to join/create room @@ -422,24 +461,25 @@ export default class RoomsListView extends LoggedView { const { username } = item; const result = await RocketChat.createDirectMessage(username); if (result.success) { - return this.goRoom(result.room._id); + return this.goRoom({ rid: result.room._id, name: username, t: 'd' }); } } catch (e) { log('RoomsListView._onPressItem', e); } } else { - const { rid } = item; - return this.goRoom(rid); + const { rid, name, t } = item; + return this.goRoom({ rid, name, t }); } } toggleSort = () => { const { toggleSortDropdown } = this.props; - if (Platform.OS === 'ios') { - this.scroll.scrollTo({ x: 0, y: SCROLL_OFFSET, animated: true }); - } else { - this.scroll.scrollTo({ x: 0, y: 0, animated: true }); + const offset = isAndroid() ? 0 : SCROLL_OFFSET; + if (this.scroll.scrollTo) { + this.scroll.scrollTo({ x: 0, y: offset, animated: true }); + } else if (this.scroll.scrollToOffset) { + this.scroll.scrollToOffset({ offset }); } setTimeout(() => { toggleSortDropdown(); @@ -461,6 +501,7 @@ export default class RoomsListView extends LoggedView { return ( @@ -474,10 +515,17 @@ export default class RoomsListView extends LoggedView { renderSearchBar = () => { if (Platform.OS === 'ios') { - return ; + return ; } } + renderListHeader = () => ( + [ + this.renderSearchBar(), + this.renderHeader() + ] + ) + renderItem = ({ item }) => { const { useRealName, userId, baseUrl } = this.props; const id = item.rid.replace(userId, '').trim(); @@ -504,17 +552,11 @@ export default class RoomsListView extends LoggedView { renderSeparator = () => - renderSectionHeader = (header) => { - const { showUnread, showFavorites, groupByType } = this.props; - if (!(showUnread || showFavorites || groupByType)) { - return null; - } - return ( - - {I18n.t(header)} - - ); - } + renderSectionHeader = header => ( + + {I18n.t(header)} + + ) renderSection = (data, header) => { const { showUnread, showFavorites, groupByType } = this.props; @@ -542,6 +584,8 @@ export default class RoomsListView extends LoggedView { enableEmptySections removeClippedSubviews keyboardShouldPersistTaps='always' + initialNumToRender={12} + windowSize={7} /> ); } @@ -566,6 +610,8 @@ export default class RoomsListView extends LoggedView { enableEmptySections removeClippedSubviews keyboardShouldPersistTaps='always' + initialNumToRender={12} + windowSize={7} /> ); } @@ -590,6 +636,30 @@ export default class RoomsListView extends LoggedView { return ; } + const { showUnread, showFavorites, groupByType } = this.props; + if (!(showUnread || showFavorites || groupByType)) { + const { chats, search } = this.state; + return ( + + ); + } + return ( - {this.renderSearchBar()} - {this.renderHeader()} + {this.renderListHeader()} {this.renderList()} ); diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index 74510939b..65226a064 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { View, FlatList, Text } from 'react-native'; import { connect } from 'react-redux'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import LoggedView from '../View'; import RCTextInput from '../../containers/TextInput'; @@ -15,7 +16,6 @@ import Message from '../../containers/message/Message'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; import I18n from '../../i18n'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; -import database from '../../lib/realm'; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', @@ -50,10 +50,8 @@ export default class SearchMessagesView extends LoggedView { constructor(props) { super('SearchMessagesView', props); - this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid); this.state = { loading: false, - room: this.rooms[0], messages: [], searchText: '' }; @@ -63,17 +61,31 @@ export default class SearchMessagesView extends LoggedView { this.name.focus(); } + shouldComponentUpdate(nextProps, nextState) { + const { loading, searchText, messages } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (nextState.searchText !== searchText) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; + } + componentWillUnmount() { this.search.stop(); } // eslint-disable-next-line react/sort-comp search = debounce(async(searchText) => { - const { room } = this.state; + const { rid } = this.props; this.setState({ searchText, loading: true, messages: [] }); try { - const result = await RocketChat.searchMessages(room.rid, searchText); + const result = await RocketChat.searchMessages(rid, searchText); if (result.success) { this.setState({ messages: result.messages || [], diff --git a/app/views/SearchMessagesView/styles.js b/app/views/SearchMessagesView/styles.js index 09e8163ba..976453264 100644 --- a/app/views/SearchMessagesView/styles.js +++ b/app/views/SearchMessagesView/styles.js @@ -21,5 +21,11 @@ export default StyleSheet.create({ height: StyleSheet.hairlineWidth, backgroundColor: '#E7EBF2', marginVertical: 20 + }, + listEmptyContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'flex-start', + backgroundColor: '#ffffff' } }); diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index 9320de371..4d15893be 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux'; import { Navigation } from 'react-native-navigation'; import SafeAreaView from 'react-native-safe-area-view'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; +import equal from 'deep-equal'; import { addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction @@ -80,6 +81,21 @@ export default class SelectedUsersView extends LoggedView { Navigation.events().bindComponent(this); } + shouldComponentUpdate(nextProps, nextState) { + const { search } = this.state; + const { users, loading } = this.props; + if (nextProps.loading !== loading) { + return true; + } + if (!equal(nextProps.users, users)) { + return true; + } + if (!equal(nextState.search, search)) { + return true; + } + return false; + } + componentDidUpdate(prevProps) { const { componentId, users } = this.props; if (prevProps.users.length !== users.length) { diff --git a/app/views/SetUsernameView.js b/app/views/SetUsernameView.js index 486368707..39f48fdfd 100644 --- a/app/views/SetUsernameView.js +++ b/app/views/SetUsernameView.js @@ -72,6 +72,17 @@ export default class SetUsernameView extends LoggedView { } } + shouldComponentUpdate(nextProps, nextState) { + const { username, saving } = this.state; + if (nextState.username !== username) { + return true; + } + if (nextState.saving !== saving) { + return true; + } + return false; + } + componentWillUnmount() { if (this.timeout) { clearTimeout(this.timeout); diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index 10a29fb4b..53cf5e6da 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -89,6 +89,21 @@ export default class SettingsView extends LoggedView { BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); } + shouldComponentUpdate(nextProps, nextState) { + const { language, saving } = this.state; + const { userLanguage } = this.props; + if (nextState.language !== language) { + return true; + } + if (nextState.saving !== saving) { + return true; + } + if (nextProps.userLanguage !== userLanguage) { + return true; + } + return false; + } + componentWillUnmount() { BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); } diff --git a/app/views/SnippetedMessagesView/index.js b/app/views/SnippetedMessagesView/index.js index 556139cc1..ec814e6b1 100644 --- a/app/views/SnippetedMessagesView/index.js +++ b/app/views/SnippetedMessagesView/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FlatList, View, Text } from 'react-native'; import { connect } from 'react-redux'; import SafeAreaView from 'react-native-safe-area-view'; +import equal from 'deep-equal'; import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages'; import LoggedView from '../View'; @@ -68,6 +69,24 @@ export default class SnippetedMessagesView extends LoggedView { } } + shouldComponentUpdate(nextProps, nextState) { + const { loading, loadingMore } = this.state; + const { messages, ready } = this.props; + if (nextState.loading !== loading) { + return true; + } + if (nextState.loadingMore !== loadingMore) { + return true; + } + if (nextProps.ready !== ready) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; + } + componentWillUnmount() { const { closeSnippetedMessages } = this.props; closeSnippetedMessages(); diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js index a51fd84f4..bf5fdb637 100644 --- a/app/views/StarredMessagesView/index.js +++ b/app/views/StarredMessagesView/index.js @@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import { DEFAULT_HEADER } from '../../constants/headerOptions'; import RocketChat from '../../lib/rocketchat'; -import database from '../../lib/realm'; const STAR_INDEX = 0; const CANCEL_INDEX = 1; @@ -22,6 +21,7 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')]; @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', customEmojis: state.customEmojis, + room: state.room, user: { id: state.login.user && state.login.user.id, username: state.login.user && state.login.user.username, @@ -44,18 +44,16 @@ export default class StarredMessagesView extends LoggedView { } static propTypes = { - rid: PropTypes.string, user: PropTypes.object, baseUrl: PropTypes.string, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + room: PropTypes.object } constructor(props) { super('StarredMessagesView', props); - this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid); this.state = { loading: false, - room: this.rooms[0], messages: [] }; } @@ -65,7 +63,14 @@ export default class StarredMessagesView extends LoggedView { } shouldComponentUpdate(nextProps, nextState) { - return !equal(this.state, nextState); + const { loading, messages } = this.state; + if (nextState.loading !== loading) { + return true; + } + if (!equal(nextState.messages, messages)) { + return true; + } + return false; } onLongPress = (message) => { @@ -101,7 +106,7 @@ export default class StarredMessagesView extends LoggedView { load = async() => { const { - messages, total, loading, room + messages, total, loading } = this.state; const { user } = this.props; if (messages.length === total || loading) { @@ -111,6 +116,7 @@ export default class StarredMessagesView extends LoggedView { this.setState({ loading: true }); try { + const { room } = this.props; const result = await RocketChat.getMessages( room.rid, room.t, diff --git a/e2e/14-joinpublicroom.spec.js b/e2e/14-joinpublicroom.spec.js new file mode 100644 index 000000000..b61b7362f --- /dev/null +++ b/e2e/14-joinpublicroom.spec.js @@ -0,0 +1,199 @@ +const { + device, expect, element, by, waitFor +} = require('detox'); +const { takeScreenshot } = require('./helpers/screenshot'); +const data = require('./data'); +const { tapBack } = require('./helpers/app'); + +const room = 'detox-public'; + +async function mockMessage(message) { + await element(by.id('messagebox-input')).tap(); + await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`); + await element(by.id('messagebox-send-message')).tap(); + await waitFor(element(by.text(`${ data.random }${ message }`))).toExist().withTimeout(60000); +}; + +async function navigateToRoom() { + await element(by.id('rooms-list-view-search')).replaceText(room); + await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(60000); + await element(by.id(`rooms-list-view-item-${ room }`)).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); +} + +async function navigateToRoomActions() { + await element(by.id('room-view-header-actions')).tap(); + await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); +} + +describe('Join public room', () => { + before(async() => { + await device.reloadReactNative(); + await navigateToRoom(); + }); + + describe('Render', async() => { + it('should have room screen', async() => { + await expect(element(by.id('room-view'))).toBeVisible(); + }); + + it('should have messages list', async() => { + await expect(element(by.id('room-view-messages'))).toBeVisible(); + }); + + // Render - Header + describe('Header', async() => { + it('should have star button', async() => { + await expect(element(by.id('room-view-header-star'))).toBeVisible(); + }); + + it('should have actions button ', async() => { + await expect(element(by.id('room-view-header-actions'))).toBeVisible(); + }); + }); + + // Render - Join + describe('Join', async() => { + it('should have join', async() => { + await expect(element(by.id('room-view-join'))).toBeVisible(); + }); + + it('should have join text', async() => { + await expect(element(by.text('You are in preview mode'))).toBeVisible(); + }); + + it('should have join button', async() => { + await expect(element(by.id('room-view-join-button'))).toBeVisible(); + }); + + it('should not have messagebox', async() => { + await expect(element(by.id('messagebox'))).toBeNotVisible(); + }); + }); + + describe('Room Actions', async() => { + before(async() => { + await navigateToRoomActions('c'); + }); + + it('should have room actions screen', async() => { + await expect(element(by.id('room-actions-view'))).toBeVisible(); + }); + + it('should have info', async() => { + await expect(element(by.id('room-actions-info'))).toBeVisible(); + }); + + it('should have voice', async() => { + await expect(element(by.id('room-actions-voice'))).toBeVisible(); + }); + + it('should have video', async() => { + await expect(element(by.id('room-actions-video'))).toBeVisible(); + }); + + it('should have members', async() => { + await expect(element(by.id('room-actions-members'))).toBeVisible(); + }); + + it('should have files', async() => { + await expect(element(by.id('room-actions-files'))).toBeVisible(); + }); + + it('should have mentions', async() => { + await expect(element(by.id('room-actions-mentioned'))).toBeVisible(); + }); + + it('should have starred', async() => { + await expect(element(by.id('room-actions-starred'))).toBeVisible(); + }); + + it('should have search', async() => { + await expect(element(by.id('room-actions-search'))).toBeVisible(); + }); + + it('should have share', async() => { + await element(by.id('room-actions-list')).swipe('up'); + await expect(element(by.id('room-actions-share'))).toBeVisible(); + }); + + it('should have pinned', async() => { + await expect(element(by.id('room-actions-pinned'))).toBeVisible(); + }); + + it('should have snippeted', async() => { + await expect(element(by.id('room-actions-snippeted'))).toBeVisible(); + }); + + it('should not have notifications', async() => { + await expect(element(by.id('room-actions-notifications'))).toBeNotVisible(); + }); + + it('should not have leave channel', async() => { + await expect(element(by.id('room-actions-leave-channel'))).toBeNotVisible(); + }); + + after(async() => { + await tapBack(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000); + }) + }); + + after(async() => { + takeScreenshot(); + }); + }); + + describe('Usage', async() => { + it('should join room', async() => { + await element(by.id('room-view-join-button')).tap(); + await waitFor(element(by.id('messagebox'))).toBeVisible().withTimeout(60000); + await expect(element(by.id('messagebox'))).toBeVisible(); + await expect(element(by.id('room-view-join'))).toBeNotVisible(); + }); + + it('should send message', async() => { + await mockMessage('message'); + await expect(element(by.text(`${ data.random }message`))).toExist(); + }); + + it('should have disable notifications and leave channel', async() => { + await navigateToRoomActions('c'); + await expect(element(by.id('room-actions-view'))).toBeVisible(); + await expect(element(by.id('room-actions-info'))).toBeVisible(); + await expect(element(by.id('room-actions-voice'))).toBeVisible(); + await expect(element(by.id('room-actions-video'))).toBeVisible(); + await expect(element(by.id('room-actions-members'))).toBeVisible(); + await expect(element(by.id('room-actions-files'))).toBeVisible(); + await expect(element(by.id('room-actions-mentioned'))).toBeVisible(); + await expect(element(by.id('room-actions-starred'))).toBeVisible(); + await expect(element(by.id('room-actions-search'))).toBeVisible(); + await element(by.id('room-actions-list')).swipe('up'); + await expect(element(by.id('room-actions-share'))).toBeVisible(); + await expect(element(by.id('room-actions-pinned'))).toBeVisible(); + await expect(element(by.id('room-actions-snippeted'))).toBeVisible(); + await expect(element(by.id('room-actions-notifications'))).toBeVisible(); + await expect(element(by.id('room-actions-leave-channel'))).toBeVisible(); + }); + + it('should leave room', async() => { + await element(by.id('room-actions-leave-channel')).tap(); + await waitFor(element(by.text('Yes, leave it!'))).toBeVisible().withTimeout(5000); + await expect(element(by.text('Yes, leave it!'))).toBeVisible(); + await element(by.text('Yes, leave it!')).tap(); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-search')).replaceText(''); + await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000); + await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible(); + }); + + it('should navigate to room and user should be joined', async() => { + await navigateToRoom(); + await expect(element(by.id('room-view-join'))).toBeVisible(); + }) + + after(async() => { + takeScreenshot(); + }); + }); +}); diff --git a/e2e/mocha.opts b/e2e/mocha.opts index 31aa76cb2..4c47bee44 100644 --- a/e2e/mocha.opts +++ b/e2e/mocha.opts @@ -1 +1 @@ ---recursive --timeout 120000 \ No newline at end of file +--recursive --timeout 120000 -b \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0aebf2ce2..4c5621966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18282,21 +18282,21 @@ } }, "react-native-navigation": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.1.3.tgz", - "integrity": "sha512-CtjDhw7eaDWCqfhK6Fq6IUDq3agl3oGDd8SNaPF7318tLni1qTmmFdz/3CpoNlegNWhAMAjNu+ONDAbe7ksADw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.2.1.tgz", + "integrity": "sha512-m0RyVQMiNNIoMlcy3zADgazRRU5qeJNOpRhx9ERA/V5t2uPq1vTGoG3bvoYbkFmsrsgyWZax4l+5AdnygDzNKg==", "requires": { "hoist-non-react-statics": "3.x.x", - "lodash": "4.x.x", + "lodash": "4.17.x", "prop-types": "15.x.x", "react-lifecycles-compat": "2.0.0", "tslib": "1.9.3" }, "dependencies": { "hoist-non-react-statics": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz", - "integrity": "sha512-MYcYuROh7SBM69xHGqXEwQqDux34s9tz+sCnxJmN18kgWh6JFdTw/5YdZtqsOdZJXddE/wUpCzfEdDrJj8p0Iw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", + "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", "requires": { "react-is": "^16.3.2" } @@ -19958,9 +19958,9 @@ } }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "send": { "version": "0.16.2", diff --git a/package.json b/package.json index c43bf40e0..4a6b3ab41 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "react-native-keyboard-tracking-view": "^5.5.0", "react-native-markdown-renderer": "^3.2.8", "react-native-modal": "^7.0.0", - "react-native-navigation": "^2.1.3", + "react-native-navigation": "^2.2.1", "react-native-notifications": "^1.1.21", "react-native-optimized-flatlist": "^1.0.4", "react-native-picker-select": "^5.1.0", @@ -70,6 +70,7 @@ "redux-immutable-state-invariant": "^2.1.0", "redux-saga": "^0.16.2", "rn-fetch-blob": "^0.10.13", + "semver": "^5.6.0", "snyk": "^1.109.0", "strip-ansi": "^4.0.0" },