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
This commit is contained in:
Diego Mello 2018-12-21 08:55:35 -02:00 committed by GitHub
parent 8384d4eeff
commit d23c055584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1339 additions and 378 deletions

View File

@ -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) [![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) [![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
<a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative"> <a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43> <img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>

View File

@ -1,9 +1,9 @@
import React from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, ViewPropTypes } from 'react-native'; import { View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
export default class Avatar extends React.PureComponent { export default class Avatar extends PureComponent {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
style: ViewPropTypes.style, style: ViewPropTypes.style,

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native'; import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles'; import styles from './styles';
export default class TabBar extends React.PureComponent { export default class TabBar extends React.Component {
static propTypes = { static propTypes = {
goToPage: PropTypes.func, goToPage: PropTypes.func,
activeTab: PropTypes.number, activeTab: PropTypes.number,
@ -11,6 +11,14 @@ export default class TabBar extends React.PureComponent {
tabEmojiStyle: PropTypes.object tabEmojiStyle: PropTypes.object
} }
shouldComponentUpdate(nextProps) {
const { activeTab } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
return false;
}
render() { render() {
const { const {
tabs, goToPage, tabEmojiStyle, activeTab tabs, goToPage, tabEmojiStyle, activeTab

View File

@ -4,6 +4,8 @@ import { ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import map from 'lodash/map'; import map from 'lodash/map';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import equal from 'deep-equal';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import styles from './styles'; import styles from './styles';
@ -28,26 +30,41 @@ export default class EmojiPicker extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
frequentlyUsed: [],
customEmojis: []
};
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true); this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
this.customEmojis = database.objects('customEmojis'); this.customEmojis = database.objects('customEmojis');
this.state = {
frequentlyUsed: [],
customEmojis: [],
show: false
};
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this); this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
this.updateCustomEmojis = this.updateCustomEmojis.bind(this); this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
} }
//
// shouldComponentUpdate(nextProps) {
// return false;
// }
componentDidMount() { componentDidMount() {
this.updateFrequentlyUsed();
this.updateCustomEmojis();
requestAnimationFrame(() => this.setState({ show: true })); requestAnimationFrame(() => this.setState({ show: true }));
this.frequentlyUsed.addListener(this.updateFrequentlyUsed); this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
this.customEmojis.addListener(this.updateCustomEmojis); 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() { componentWillUnmount() {

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import I18n from '../../i18n'; import I18n from '../../i18n';
export default class FilesActions extends Component { export default class FilesActions extends PureComponent {
static propTypes = { static propTypes = {
hideActions: PropTypes.func.isRequired, hideActions: PropTypes.func.isRequired,
takePhoto: PropTypes.func.isRequired, takePhoto: PropTypes.func.isRequired,

View File

@ -56,6 +56,10 @@ export default class ReplyPreview extends Component {
username: PropTypes.string.isRequired username: PropTypes.string.isRequired
} }
shouldComponentUpdate() {
return false;
}
close = () => { close = () => {
const { close } = this.props; const { close } = this.props;
close(); close();

View File

@ -90,6 +90,28 @@ export default class UploadModal extends Component {
return null; 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 = () => { submit = () => {
const { file, submit } = this.props; const { file, submit } = this.props;
const { name, description } = this.state; const { name, description } = this.state;
@ -162,12 +184,12 @@ export default class UploadModal extends Component {
<ScrollView style={styles.scrollView}> <ScrollView style={styles.scrollView}>
<Image source={{ isStatic: true, uri: file.path }} style={styles.image} /> <Image source={{ isStatic: true, uri: file.path }} style={styles.image} />
<TextInput <TextInput
placeholder='File name' placeholder={I18n.t('File_name')}
value={name} value={name}
onChangeText={value => this.setState({ name: value })} onChangeText={value => this.setState({ name: value })}
/> />
<TextInput <TextInput
placeholder='File description' placeholder={I18n.t('File_description')}
value={description} value={description}
onChangeText={value => this.setState({ description: value })} onChangeText={value => this.setState({ description: value })}
/> />

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert, Image View, TextInput, FlatList, Text, TouchableOpacity, Alert, Image
@ -9,6 +9,7 @@ import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input'; import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker from 'react-native-image-crop-picker';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import { userTyping as userTypingAction } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import { import {
@ -59,7 +60,7 @@ const imagePickerConfig = {
typing: status => dispatch(userTypingAction(status)), typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction()) closeReply: () => dispatch(replyCancelAction())
})) }))
export default class MessageBox extends React.PureComponent { export default class MessageBox extends Component {
static propTypes = { static propTypes = {
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
baseUrl: 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) { onChangeText(text) {
const { typing } = this.props; const { typing } = this.props;

View File

@ -6,6 +6,7 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import equal from 'deep-equal';
import { setStackRoot as setStackRootAction } from '../actions'; import { setStackRoot as setStackRootAction } from '../actions';
import { logout as logoutAction } from '../actions/login'; import { logout as logoutAction } from '../actions/login';
@ -111,7 +112,8 @@ export default class Sidebar extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
showStatus: false showStatus: false,
status: []
}; };
Navigation.events().bindComponent(this); 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) => { handleChangeStack = (event) => {
const { stack } = event; const { stack } = event;
this.setStack(stack); this.setStack(stack);
@ -140,22 +179,20 @@ export default class Sidebar extends Component {
} }
setStatus = () => { setStatus = () => {
setTimeout(() => { this.setState({
this.setState({ status: [{
status: [{ id: 'online',
id: 'online', name: I18n.t('Online')
name: I18n.t('Online') }, {
}, { id: 'busy',
id: 'busy', name: I18n.t('Busy')
name: I18n.t('Busy') }, {
}, { id: 'away',
id: 'away', name: I18n.t('Away')
name: I18n.t('Away') }, {
}, { id: 'offline',
id: 'offline', name: I18n.t('Invisible')
name: I18n.t('Invisible') }]
}]
});
}); });
} }

View File

@ -7,6 +7,7 @@ import Video from 'react-native-video';
import Slider from 'react-native-slider'; import Slider from 'react-native-slider';
import moment from 'moment'; import moment from 'moment';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Markdown from './Markdown'; import Markdown from './Markdown';
@ -47,7 +48,7 @@ const styles = StyleSheet.create({
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss'); 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 = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.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) { onLoad(data) {
this.setState({ duration: data.duration > 0 ? data.duration : 0 }); this.setState({ duration: data.duration > 0 ? data.duration : 0 });
} }

View File

@ -1,13 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import PhotoModal from './PhotoModal'; import PhotoModal from './PhotoModal';
import Markdown from './Markdown'; import Markdown from './Markdown';
import styles from './styles'; import styles from './styles';
export default class extends React.PureComponent { export default class extends Component {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.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() { onPressButton() {
this.setState({ this.setState({

View File

@ -155,6 +155,8 @@ export default {
Error_uploading: 'Error uploading', Error_uploading: 'Error uploading',
Favorites: 'Favorites', Favorites: 'Favorites',
Files: 'Files', Files: 'Files',
File_description: 'File description',
File_name: 'File name',
Finish_recording: 'Finish recording', 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', 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', Forgot_my_password: 'Forgot my password',
@ -170,6 +172,7 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance', 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_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing', 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_the_community: 'Join the community',
Join: 'Join', Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel', Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',

View File

@ -162,6 +162,8 @@ export default {
Error_uploading: 'Erro subindo', Error_uploading: 'Erro subindo',
Favorites: 'Favoritos', Favorites: 'Favoritos',
Files: 'Arquivos', Files: 'Arquivos',
File_description: 'Descrição do arquivo',
File_name: 'Nome do arquivo',
Finish_recording: 'Encerrar gravação', 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', 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', Forgot_my_password: 'Esqueci minha senha',
@ -175,6 +177,7 @@ export default {
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',
is_typing: 'está digitando', 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_the_community: 'Junte-se à comunidade',
Join: 'Entrar', Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal', Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',

View File

@ -8,6 +8,7 @@ const restTypes = {
async function open({ type, rid }) { async function open({ type, rid }) {
try { try {
// RC 0.61.0
await SDK.api.post(`${ restTypes[type] }.open`, { roomId: rid }); await SDK.api.post(`${ restTypes[type] }.open`, { roomId: rid });
return true; return true;
} catch (e) { } catch (e) {

View File

@ -16,6 +16,7 @@ const getLastMessage = () => {
export default async function() { export default async function() {
try { try {
const lastMessage = getLastMessage(); const lastMessage = getLastMessage();
// RC 0.61.0
const result = await SDK.api.get('emoji-custom'); const result = await SDK.api.get('emoji-custom');
let { emojis } = result; let { emojis } = result;
emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage); emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);

View File

@ -7,6 +7,7 @@ import defaultPermissions from '../../constants/permissions';
export default async function() { export default async function() {
try { try {
// RC 0.66.0
const result = await SDK.api.get('permissions.list'); const result = await SDK.api.get('permissions.list');
if (!result.success) { if (!result.success) {

View File

@ -18,6 +18,8 @@ export default function() {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const updatedSince = lastMessage(); const updatedSince = lastMessage();
// subscriptions.get: Since RC 0.60.0
// rooms.get: Since RC 0.62.0
const [subscriptionsResult, roomsResult] = await (updatedSince 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', { updatedSince }), SDK.api.get('rooms.get', { updatedSince })])
: Promise.all([SDK.api.get('subscriptions.get'), SDK.api.get('rooms.get')]) : Promise.all([SDK.api.get('subscriptions.get'), SDK.api.get('rooms.get')])

View File

@ -16,6 +16,7 @@ function updateServer(param) {
export default async function() { export default async function() {
try { try {
const settingsParams = JSON.stringify(Object.keys(settings)); 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()); const result = await fetch(`${ SDK.api.url }settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
if (!result.success) { if (!result.success) {

View File

@ -8,6 +8,7 @@ import log from '../../utils/log';
async function load({ rid: roomId, latest, t }) { async function load({ rid: roomId, latest, t }) {
if (t === 'l') { if (t === 'l') {
try { try {
// RC 0.51.0
const data = await SDK.driver.asyncCall('loadHistory', roomId, null, 50, latest); const data = await SDK.driver.asyncCall('loadHistory', roomId, null, 50, latest);
if (!data || data.status === 'error') { if (!data || data.status === 'error') {
return []; return [];
@ -23,6 +24,7 @@ async function load({ rid: roomId, latest, t }) {
if (latest) { if (latest) {
params = { ...params, latest: new Date(latest).toISOString() }; params = { ...params, latest: new Date(latest).toISOString() };
} }
// RC 0.48.0
const data = await SDK.api.get(`${ this.roomTypeToApiType(t) }.history`, params); const data = await SDK.api.get(`${ this.roomTypeToApiType(t) }.history`, params);
if (!data || data.status === 'error') { if (!data || data.status === 'error') {
return []; return [];

View File

@ -12,6 +12,7 @@ async function load({ rid: roomId, lastOpen }) {
} else { } else {
return []; return [];
} }
// RC 0.60.0
const { result } = await SDK.api.get('chat.syncMessages', { roomId, lastUpdate, count: 50 }); const { result } = await SDK.api.get('chat.syncMessages', { roomId, lastUpdate, count: 50 });
return result; return result;
} }

View File

@ -6,6 +6,7 @@ import log from '../../utils/log';
export default async function readMessages(rid) { export default async function readMessages(rid) {
const ls = new Date(); const ls = new Date();
try { try {
// RC 0.61.0
const data = await SDK.api.post('subscriptions.read', { rid }); const data = await SDK.api.post('subscriptions.read', { rid });
const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid); const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid);
database.write(() => { database.write(() => {

View File

@ -15,6 +15,7 @@ function _ufsComplete(fileId, store, token) {
} }
function _sendFileMessage(rid, data, msg = {}) { function _sendFileMessage(rid, data, msg = {}) {
// RC 0.22.0
return SDK.driver.asyncCall('sendFileMessage', rid, null, data, msg); return SDK.driver.asyncCall('sendFileMessage', rid, null, data, msg);
} }

View File

@ -33,6 +33,7 @@ export const getMessage = (rid, msg = {}) => {
export async function sendMessageCall(message) { export async function sendMessageCall(message) {
const { _id, rid, msg } = message; const { _id, rid, msg } = message;
// RC 0.60.0
const data = await SDK.api.post('chat.sendMessage', { message: { _id, rid, msg } }); const data = await SDK.api.post('chat.sendMessage', { message: { _id, rid, msg } });
return data; return data;
} }

View File

@ -1,6 +1,7 @@
import { AsyncStorage, Platform } from 'react-native'; import { AsyncStorage, Platform } from 'react-native';
import foreach from 'lodash/forEach'; import foreach from 'lodash/forEach';
import * as SDK from '@rocket.chat/sdk'; import * as SDK from '@rocket.chat/sdk';
import semver from 'semver';
import reduxStore from './createStore'; import reduxStore from './createStore';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
@ -42,6 +43,7 @@ const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
const call = (method, ...params) => SDK.driver.asyncCall(method, ...params); const call = (method, ...params) => SDK.driver.asyncCall(method, ...params);
const returnAnArray = obj => obj || []; const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.66.0';
const RocketChat = { const RocketChat = {
TOKEN_KEY, TOKEN_KEY,
@ -51,6 +53,7 @@ const RocketChat = {
createChannel({ createChannel({
name, users, type, readOnly, broadcast name, users, type, readOnly, broadcast
}) { }) {
// RC 0.51.0
return call(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast }); return call(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
}, },
async createDirectMessageAndWait(username) { async createDirectMessageAndWait(username) {
@ -81,12 +84,27 @@ const RocketChat = {
try { try {
const result = await fetch(`${ server }/api/v1/info`).then(response => response.json()); const result = await fetch(`${ server }/api/v1/info`).then(response => response.json());
if (result.success && result.info) { 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) { } catch (e) {
log('testServer', e); log('testServer', e);
} }
throw new Error({ error: 'invalid server' }); return {
success: false,
message: 'The_URL_is_invalid'
};
}, },
_setUser(ddpMessage) { _setUser(ddpMessage) {
this.activeUsers = this.activeUsers || {}; this.activeUsers = this.activeUsers || {};
@ -235,14 +253,17 @@ const RocketChat = {
}, },
register(credentials) { register(credentials) {
// RC 0.50.0
return SDK.api.post('users.register', credentials, false); return SDK.api.post('users.register', credentials, false);
}, },
setUsername(username) { setUsername(username) {
// RC 0.51.0
return call('setUsername', username); return call('setUsername', username);
}, },
forgotPassword(email) { forgotPassword(email) {
// RC 0.64.0
return SDK.api.post('users.forgotPassword', { email }, false); return SDK.api.post('users.forgotPassword', { email }, false);
}, },
@ -288,6 +309,7 @@ const RocketChat = {
async login(params) { async login(params) {
try { try {
// RC 0.64.0
return await SDK.api.login(params); return await SDK.api.login(params);
} catch (e) { } catch (e) {
reduxStore.dispatch(loginFailure(e)); reduxStore.dispatch(loginFailure(e));
@ -302,6 +324,7 @@ const RocketChat = {
console.log('logout -> removePushToken -> catch -> error', error); console.log('logout -> removePushToken -> catch -> error', error);
} }
try { try {
// RC 0.60.0
await SDK.api.logout(); await SDK.api.logout();
} catch (error) { } catch (error) {
console.log('logout -> api logout -> catch -> error', error); console.log('logout -> api logout -> catch -> error', error);
@ -343,6 +366,7 @@ const RocketChat = {
type, type,
appName: 'chat.rocket.reactnative' // TODO: try to get from config file appName: 'chat.rocket.reactnative' // TODO: try to get from config file
}; };
// RC 0.60.0
return SDK.api.post('push.token', data); return SDK.api.post('push.token', data);
} }
return resolve(); return resolve();
@ -351,6 +375,7 @@ const RocketChat = {
removePushToken() { removePushToken() {
const token = getDeviceToken(); const token = getDeviceToken();
if (token) { if (token) {
// RC 0.60.0
return SDK.api.del('push.token', { token }); return SDK.api.del('push.token', { token });
} }
return Promise.resolve(); return Promise.resolve();
@ -430,14 +455,17 @@ const RocketChat = {
}, },
spotlight(search, usernames, type) { spotlight(search, usernames, type) {
// RC 0.51.0
return call('spotlight', search, usernames, type); return call('spotlight', search, usernames, type);
}, },
createDirectMessage(username) { createDirectMessage(username) {
// RC 0.59.0
return SDK.api.post('im.create', { username }); return SDK.api.post('im.create', { username });
}, },
joinRoom(roomId) { joinRoom(roomId) {
// TODO: join code // TODO: join code
// RC 0.48.0
return SDK.api.post('channels.join', { roomId }); return SDK.api.post('channels.join', { roomId });
}, },
sendFileMessage, sendFileMessage,
@ -471,22 +499,28 @@ const RocketChat = {
}, },
deleteMessage(message) { deleteMessage(message) {
const { _id, rid } = message; const { _id, rid } = message;
// RC 0.48.0
return SDK.api.post('chat.delete', { roomId: rid, msgId: _id }); return SDK.api.post('chat.delete', { roomId: rid, msgId: _id });
}, },
editMessage(message) { editMessage(message) {
const { _id, msg, rid } = message; const { _id, msg, rid } = message;
// RC 0.49.0
return SDK.api.post('chat.update', { roomId: rid, msgId: _id, text: msg }); return SDK.api.post('chat.update', { roomId: rid, msgId: _id, text: msg });
}, },
toggleStarMessage(message) { toggleStarMessage(message) {
if (message.starred) { if (message.starred) {
// RC 0.59.0
return SDK.api.post('chat.unStarMessage', { messageId: message._id }); return SDK.api.post('chat.unStarMessage', { messageId: message._id });
} }
// RC 0.59.0
return SDK.api.post('chat.starMessage', { messageId: message._id }); return SDK.api.post('chat.starMessage', { messageId: message._id });
}, },
togglePinMessage(message) { togglePinMessage(message) {
if (message.pinned) { if (message.pinned) {
// RC 0.59.0
return SDK.api.post('chat.unPinMessage', { messageId: message._id }); return SDK.api.post('chat.unPinMessage', { messageId: message._id });
} }
// RC 0.59.0
return SDK.api.post('chat.pinMessage', { messageId: message._id }); return SDK.api.post('chat.pinMessage', { messageId: message._id });
}, },
getRoom(rid) { getRoom(rid) {
@ -496,9 +530,6 @@ const RocketChat = {
} }
return Promise.resolve(result); return Promise.resolve(result);
}, },
getRoomInfo(roomId) {
return SDK.api.get('rooms.info', { roomId });
},
async getPermalink(message) { async getPermalink(message) {
let room; let room;
try { try {
@ -535,22 +566,30 @@ const RocketChat = {
return call('UserPresence:setDefaultStatus', status); return call('UserPresence:setDefaultStatus', status);
}, },
setReaction(emoji, messageId) { setReaction(emoji, messageId) {
// RC 0.62.2
return SDK.api.post('chat.react', { emoji, messageId }); return SDK.api.post('chat.react', { emoji, messageId });
}, },
toggleFavorite(roomId, favorite) { toggleFavorite(roomId, favorite) {
// RC 0.64.0
return SDK.api.post('rooms.favorite', { roomId, favorite }); return SDK.api.post('rooms.favorite', { roomId, favorite });
}, },
getRoomMembers(rid, allUsers) { getRoomMembers(rid, allUsers) {
// RC 0.42.0
return call('getUsersOfRoom', rid, allUsers); return call('getUsersOfRoom', rid, allUsers);
}, },
getUserRoles() { getUserRoles() {
// RC 0.27.0
return call('getUserRoles'); return call('getUserRoles');
}, },
getRoomCounters(roomId, t) { getRoomCounters(roomId, t) {
// RC 0.65.0
return SDK.api.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId }); return SDK.api.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId });
}, },
async getRoomMember(rid, currentUserId) { async getRoomMember(rid, currentUserId) {
try { try {
if (rid === `${ currentUserId }${ currentUserId }`) {
return Promise.resolve(currentUserId);
}
const membersResult = await RocketChat.getRoomMembers(rid, true); const membersResult = await RocketChat.getRoomMembers(rid, true);
return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId)); return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId));
} catch (error) { } catch (error) {
@ -559,53 +598,71 @@ const RocketChat = {
}, },
toggleBlockUser(rid, blocked, block) { toggleBlockUser(rid, blocked, block) {
if (block) { if (block) {
// RC 0.49.0
return call('blockUser', { rid, blocked }); return call('blockUser', { rid, blocked });
} }
// RC 0.49.0
return call('unblockUser', { rid, blocked }); return call('unblockUser', { rid, blocked });
}, },
leaveRoom(roomId, t) { leaveRoom(roomId, t) {
// RC 0.48.0
return SDK.api.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId }); return SDK.api.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
}, },
eraseRoom(roomId, t) { eraseRoom(roomId, t) {
// RC 0.49.0
return SDK.api.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId }); return SDK.api.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId });
}, },
toggleMuteUserInRoom(rid, username, mute) { toggleMuteUserInRoom(rid, username, mute) {
if (mute) { if (mute) {
// RC 0.51.0
return call('muteUserInRoom', { rid, username }); return call('muteUserInRoom', { rid, username });
} }
// RC 0.51.0
return call('unmuteUserInRoom', { rid, username }); return call('unmuteUserInRoom', { rid, username });
}, },
toggleArchiveRoom(roomId, t, archive) { toggleArchiveRoom(roomId, t, archive) {
if (archive) { if (archive) {
// RC 0.48.0
return SDK.api.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId }); return SDK.api.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId });
} }
// RC 0.48.0
return SDK.api.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId }); return SDK.api.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
}, },
saveRoomSettings(rid, params) { saveRoomSettings(rid, params) {
// RC 0.55.0
return call('saveRoomSettings', rid, params); return call('saveRoomSettings', rid, params);
}, },
saveUserProfile(data) { saveUserProfile(data) {
// RC 0.62.2
return SDK.api.post('users.updateOwnBasicInfo', { data }); return SDK.api.post('users.updateOwnBasicInfo', { data });
}, },
saveUserPreferences(params) { saveUserPreferences(params) {
// RC 0.51.0
return call('saveUserPreferences', params); return call('saveUserPreferences', params);
}, },
saveNotificationSettings(roomId, notifications) { saveNotificationSettings(roomId, notifications) {
// RC 0.63.0
return SDK.api.post('rooms.saveNotification', { roomId, notifications }); return SDK.api.post('rooms.saveNotification', { roomId, notifications });
}, },
addUsersToRoom(rid) { addUsersToRoom(rid) {
let { users } = reduxStore.getState().selectedUsers; let { users } = reduxStore.getState().selectedUsers;
users = users.map(u => u.name); users = users.map(u => u.name);
// RC 0.51.0
return call('addUsersToRoom', { rid, users }); return call('addUsersToRoom', { rid, users });
}, },
hasPermission(permissions, rid) { hasPermission(permissions, rid) {
// get the room from realm let roles = [];
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; 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 // get permissions from realm
const permissionsFiltered = database.objects('permissions') const permissionsFiltered = database.objects('permissions')
.filter(permission => permissions.includes(permission._id)); .filter(permission => permissions.includes(permission._id));
// get room roles
const { roles } = room;
// transform room roles to array // transform room roles to array
const roomRoles = Array.from(Object.keys(roles), i => roles[i].value); const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
// get user roles on the server from redux // get user roles on the server from redux
@ -625,12 +682,15 @@ const RocketChat = {
}, {}); }, {});
}, },
getAvatarSuggestion() { getAvatarSuggestion() {
// RC 0.51.0
return call('getAvatarSuggestion'); return call('getAvatarSuggestion');
}, },
resetAvatar(userId) { resetAvatar(userId) {
// RC 0.55.0
return SDK.api.post('users.resetAvatar', { userId }); return SDK.api.post('users.resetAvatar', { userId });
}, },
setAvatarFromService({ data, contentType = '', service = null }) { setAvatarFromService({ data, contentType = '', service = null }) {
// RC 0.51.0
return call('setAvatarFromService', data, contentType, service); return call('setAvatarFromService', data, contentType, service);
}, },
async getSortPreferences() { async getSortPreferences() {
@ -668,6 +728,7 @@ const RocketChat = {
} }
}, },
getUsernameSuggestion() { getUsernameSuggestion() {
// RC 0.65.0
return SDK.api.get('users.getUsernameSuggestion'); return SDK.api.get('users.getUsernameSuggestion');
}, },
roomTypeToApiType(t) { roomTypeToApiType(t) {
@ -677,6 +738,7 @@ const RocketChat = {
return types[t]; return types[t];
}, },
getFiles(roomId, type, offset) { getFiles(roomId, type, offset) {
// RC 0.59.0
return SDK.api.get(`${ this.roomTypeToApiType(type) }.files`, { return SDK.api.get(`${ this.roomTypeToApiType(type) }.files`, {
roomId, roomId,
offset, offset,
@ -687,6 +749,7 @@ const RocketChat = {
}); });
}, },
getMessages(roomId, type, query, offset) { getMessages(roomId, type, query, offset) {
// RC 0.59.0
return SDK.api.get(`${ this.roomTypeToApiType(type) }.messages`, { return SDK.api.get(`${ this.roomTypeToApiType(type) }.messages`, {
roomId, roomId,
query, query,
@ -695,6 +758,7 @@ const RocketChat = {
}); });
}, },
searchMessages(roomId, searchText) { searchMessages(roomId, searchText) {
// RC 0.60.0
return SDK.api.get('chat.search', { return SDK.api.get('chat.search', {
roomId, roomId,
searchText searchText

View File

@ -1,7 +1,7 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { import {
takeLatest, take, select, put, all takeLatest, take, select, put, all, race
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
@ -12,6 +12,10 @@ import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
const roomTypes = {
channel: 'c', direct: 'd', group: 'p'
};
const navigate = function* navigate({ params, sameServer = true }) { const navigate = function* navigate({ params, sameServer = true }) {
if (!sameServer) { if (!sameServer) {
yield put(appStart('inside')); yield put(appStart('inside'));
@ -37,11 +41,12 @@ const navigate = function* navigate({ params, sameServer = true }) {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
const [type, name] = params.path.split('/');
Navigation.push(stack, { Navigation.push(stack, {
component: { component: {
name: 'RoomView', name: 'RoomView',
passProps: { 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 deep link is from same server
if (server === host) { if (server === host) {
if (user) { if (user) {
yield take(types.SERVER.SELECT_SUCCESS); yield race({
typing: take(types.SERVER.SELECT_SUCCESS),
timeout: delay(3000)
});
yield navigate({ params }); yield navigate({ params });
} }
} else { } else {
// if deep link is from a different server // if deep link is from a different server
try { const result = yield RocketChat.testServer(server);
// Verify if server is real if (!result.success) {
yield RocketChat.testServer(host);
} catch (error) {
return; return;
} }

View File

@ -74,13 +74,13 @@ const handleTogglePinRequest = function* handleTogglePinRequest({ message }) {
} }
}; };
const goRoom = function* goRoom({ rid }) { const goRoom = function* goRoom({ rid, name }) {
yield Navigation.popToRoot('RoomsListView'); yield Navigation.popToRoot('RoomsListView');
Navigation.push('RoomsListView', { Navigation.push('RoomsListView', {
component: { component: {
name: 'RoomView', name: 'RoomView',
passProps: { passProps: {
rid rid, name, t: 'd'
} }
} }
}); });
@ -91,10 +91,10 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
const { username } = message.u; const { username } = message.u;
const subscriptions = database.objects('subscriptions').filtered('name = $0', username); const subscriptions = database.objects('subscriptions').filtered('name = $0', username);
if (subscriptions.length) { if (subscriptions.length) {
yield goRoom({ rid: subscriptions[0].rid }); yield goRoom({ rid: subscriptions[0].rid, name: username });
} else { } else {
const room = yield RocketChat.createDirectMessage(username); const room = yield RocketChat.createDirectMessage(username);
yield goRoom({ rid: room.rid }); yield goRoom({ rid: room.rid, name: username });
} }
yield delay(500); yield delay(500);
yield put(replyInit(message, false)); yield put(replyInit(message, false));

View File

@ -120,7 +120,6 @@ const watchuserTyping = function* watchuserTyping({ status }) {
const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
try { try {
sub.stop();
const result = yield RocketChat.leaveRoom(rid, t); const result = yield RocketChat.leaveRoom(rid, t);
if (result.success) { if (result.success) {
yield Navigation.popToRoot('RoomsListView'); yield Navigation.popToRoot('RoomsListView');
@ -136,7 +135,6 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
const handleEraseRoom = function* handleEraseRoom({ rid, t }) { const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
try { try {
sub.stop();
const result = yield RocketChat.eraseRoom(rid, t); const result = yield RocketChat.eraseRoom(rid, t);
if (result.success) { if (result.success) {
yield Navigation.popToRoot('RoomsListView'); yield Navigation.popToRoot('RoomsListView');
@ -148,7 +146,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
const root = function* root() { const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); 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 takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom);

View File

@ -1,5 +1,5 @@
import { put, takeLatest } from 'redux-saga/effects'; 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 { Navigation } from 'react-native-navigation';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
@ -13,6 +13,7 @@ import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import log from '../utils/log'; import log from '../utils/log';
import store from '../lib/createStore'; import store from '../lib/createStore';
import I18n from '../i18n';
let LoginSignupView = null; let LoginSignupView = null;
let LoginView = null; let LoginView = null;
@ -49,7 +50,13 @@ const handleSelectServer = function* handleSelectServer({ server }) {
const handleServerRequest = function* handleServerRequest({ server }) { const handleServerRequest = function* handleServerRequest({ server }) {
try { 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); const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) { if (loginServicesLength === 0) {
if (LoginView == null) { if (LoginView == null) {

View File

@ -6,6 +6,7 @@ import {
} from 'react-native'; } from 'react-native';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
import LoggedView from './View'; import LoggedView from './View';
@ -128,6 +129,43 @@ export default class CreateChannelView extends LoggedView {
}, 600); }, 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) { componentDidUpdate(prevProps) {
const { const {
isFetching, failure, error, result, componentId 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') }); const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
showErrorAlert(msg); showErrorAlert(msg);
} else { } else {
const { rid } = result; const { type } = this.state;
const { rid, name } = result;
await Navigation.dismissModal(componentId); await Navigation.dismissModal(componentId);
Navigation.push('RoomsListView', { Navigation.push('RoomsListView', {
component: { component: {
name: 'RoomView', name: 'RoomView',
passProps: { passProps: {
rid rid, name, t: type ? 'p' : 'c'
} }
} }
}); });

View File

@ -44,6 +44,20 @@ export default class ForgotPasswordView extends LoggedView {
}, 600); }, 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() { componentWillUnmount() {
if (this.timeout) { if (this.timeout) {
clearTimeout(this.timeout); clearTimeout(this.timeout);

View File

@ -8,6 +8,7 @@ import { Navigation } from 'react-native-navigation';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC, RectButton, BorderlessButton } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC, RectButton, BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import LoggedView from './View'; import LoggedView from './View';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
@ -94,7 +95,6 @@ const SERVICES_COLLAPSED_HEIGHT = 174;
@connect(state => ({ @connect(state => ({
server: state.server.server, server: state.server.server,
isFetching: state.login.isFetching,
Site_Name: state.settings.Site_Name, Site_Name: state.settings.Site_Name,
services: state.login.services services: state.login.services
})) }))
@ -116,7 +116,6 @@ export default class LoginSignupView extends LoggedView {
static propTypes = { static propTypes = {
componentId: PropTypes.string, componentId: PropTypes.string,
isFetching: PropTypes.bool,
server: PropTypes.string, server: PropTypes.string,
services: PropTypes.object, services: PropTypes.object,
Site_Name: PropTypes.string Site_Name: PropTypes.string
@ -133,6 +132,27 @@ export default class LoginSignupView extends LoggedView {
this.setTitle(componentId, Site_Name); 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) { componentDidUpdate(prevProps) {
const { componentId, Site_Name } = this.props; const { componentId, Site_Name } = this.props;
if (Site_Name && prevProps.Site_Name !== Site_Name) { if (Site_Name && prevProps.Site_Name !== Site_Name) {

View File

@ -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() { componentWillUnmount() {
if (this.timeout) { if (this.timeout) {
clearTimeout(this.timeout); clearTimeout(this.timeout);

View File

@ -12,11 +12,11 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database from '../../lib/realm';
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis, customEmojis: state.customEmojis,
room: state.room,
user: { user: {
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
@ -39,18 +39,16 @@ export default class MentionedMessagesView extends LoggedView {
} }
static propTypes = { static propTypes = {
rid: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object customEmojis: PropTypes.object,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
super('StarredMessagesView', props); super('StarredMessagesView', props);
this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
this.state = { this.state = {
loading: false, loading: false,
room: this.rooms[0],
messages: [] messages: []
}; };
} }
@ -60,12 +58,19 @@ export default class MentionedMessagesView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { 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() => { load = async() => {
const { const {
messages, total, loading, room messages, total, loading
} = this.state; } = this.state;
const { user } = this.props; const { user } = this.props;
if (messages.length === total || loading) { if (messages.length === total || loading) {
@ -75,6 +80,7 @@ export default class MentionedMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getMessages( const result = await RocketChat.getMessages(
room.rid, room.rid,
room.t, room.t,

View File

@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import database from '../lib/realm'; import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -85,6 +86,14 @@ export default class NewMessageView extends LoggedView {
Navigation.events().bindComponent(this); Navigation.events().bindComponent(this);
} }
shouldComponentUpdate(nextProps, nextState) {
const { search } = this.state;
if (!equal(nextState.search, search)) {
return true;
}
return false;
}
componentWillUnmount() { componentWillUnmount() {
this.updateState.stop(); this.updateState.stop();
this.data.removeAllListeners(); this.data.removeAllListeners();
@ -94,12 +103,10 @@ export default class NewMessageView extends LoggedView {
this.search(text); this.search(text);
} }
onPressItem = (item) => { onPressItem = async(item) => {
const { onPressItem } = this.props; const { onPressItem } = this.props;
this.dismiss(); await this.dismiss();
setTimeout(() => { onPressItem(item);
onPressItem(item);
}, 600);
} }
navigationButtonPressed = ({ buttonId }) => { navigationButtonPressed = ({ buttonId }) => {
@ -110,7 +117,7 @@ export default class NewMessageView extends LoggedView {
dismiss = () => { dismiss = () => {
const { componentId } = this.props; const { componentId } = this.props;
Navigation.dismissModal(componentId); return Navigation.dismissModal(componentId);
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, ScrollView, Keyboard, Image, Alert, StyleSheet, TouchableOpacity Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
@ -58,8 +58,7 @@ const styles = StyleSheet.create({
const defaultServer = 'https://open.rocket.chat'; const defaultServer = 'https://open.rocket.chat';
@connect(state => ({ @connect(state => ({
connecting: state.server.connecting, connecting: state.server.connecting
failure: state.server.failure
}), dispatch => ({ }), dispatch => ({
connectServer: server => dispatch(serverRequest(server)) connectServer: server => dispatch(serverRequest(server))
})) }))
@ -79,7 +78,6 @@ export default class NewServerView extends LoggedView {
componentId: PropTypes.string, componentId: PropTypes.string,
server: PropTypes.string, server: PropTypes.string,
connecting: PropTypes.bool.isRequired, connecting: PropTypes.bool.isRequired,
failure: PropTypes.bool.isRequired,
connectServer: PropTypes.func.isRequired connectServer: PropTypes.func.isRequired
} }
@ -103,11 +101,16 @@ export default class NewServerView extends LoggedView {
} }
} }
componentWillReceiveProps(nextProps) { shouldComponentUpdate(nextProps, nextState) {
const { failure } = this.props; const { text } = this.state;
if (nextProps.failure && nextProps.failure !== failure) { const { connecting } = this.props;
Alert.alert(I18n.t('Oops'), I18n.t('The_URL_is_invalid')); if (nextState.text !== text) {
return true;
} }
if (nextProps.connecting !== connecting) {
return true;
}
return false;
} }
componentWillUnmount() { componentWillUnmount() {

View File

@ -68,6 +68,10 @@ export default class OnboardingView extends LoggedView {
EventEmitter.addEventListener('NewServer', this.handleNewServerEvent); EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
} }
shouldComponentUpdate() {
return false;
}
componentWillUnmount() { componentWillUnmount() {
const { const {
selectServer, previousServer, currentServer, adding, finishAdd selectServer, previousServer, currentServer, adding, finishAdd

View File

@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database from '../../lib/realm';
const PIN_INDEX = 0; const PIN_INDEX = 0;
const CANCEL_INDEX = 1; const CANCEL_INDEX = 1;
@ -22,6 +21,7 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis, customEmojis: state.customEmojis,
room: state.room,
user: { user: {
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
@ -44,18 +44,16 @@ export default class PinnedMessagesView extends LoggedView {
} }
static propTypes = { static propTypes = {
rid: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object customEmojis: PropTypes.object,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
super('PinnedMessagesView', props); super('PinnedMessagesView', props);
this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
this.state = { this.state = {
loading: false, loading: false,
room: this.rooms[0],
messages: [] messages: []
}; };
} }
@ -65,7 +63,14 @@ export default class PinnedMessagesView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { 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) => { onLongPress = (message) => {
@ -101,7 +106,7 @@ export default class PinnedMessagesView extends LoggedView {
load = async() => { load = async() => {
const { const {
messages, total, loading, room messages, total, loading
} = this.state; } = this.state;
if (messages.length === total || loading) { if (messages.length === total || loading) {
return; return;
@ -110,6 +115,7 @@ export default class PinnedMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length); const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length);
if (result.success) { if (result.success) {
this.setState(prevState => ({ this.setState(prevState => ({

View File

@ -11,6 +11,7 @@ import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import LoggedView from '../View'; import LoggedView from '../View';
import KeyboardView from '../../presentation/KeyboardView'; 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() { componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }

View File

@ -25,6 +25,8 @@ let TermsServiceView = null;
let PrivacyPolicyView = null; let PrivacyPolicyView = null;
let LegalView = null; let LegalView = null;
const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving'];
@connect(null, dispatch => ({ @connect(null, dispatch => ({
loginRequest: params => dispatch(loginRequestAction(params)) loginRequest: params => dispatch(loginRequestAction(params))
})) }))
@ -67,6 +69,11 @@ export default class RegisterView extends LoggedView {
}, 600); }, 600);
} }
shouldComponentUpdate(nextProps, nextState) {
// eslint-disable-next-line react/destructuring-assignment
return shouldUpdateState.some(key => nextState[key] !== this.state[key]);
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { componentId, Site_Name } = this.props; const { componentId, Site_Name } = this.props;
if (Site_Name && prevProps.Site_Name !== Site_Name) { if (Site_Name && prevProps.Site_Name !== Site_Name) {

View File

@ -9,6 +9,7 @@ import { connect, Provider } from 'react-redux';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import { leaveRoom as leaveRoomAction } from '../../actions/room'; import { leaveRoom as leaveRoomAction } from '../../actions/room';
import LoggedView from '../View'; import LoggedView from '../View';
@ -33,7 +34,8 @@ const modules = {};
@connect(state => ({ @connect(state => ({
userId: state.login.user && state.login.user.id, userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, 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 => ({ }), dispatch => ({
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)) leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t))
})) }))
@ -58,15 +60,16 @@ export default class RoomActionsView extends LoggedView {
componentId: PropTypes.string, componentId: PropTypes.string,
userId: PropTypes.string, userId: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
room: PropTypes.object,
leaveRoom: PropTypes.func leaveRoom: PropTypes.func
} }
constructor(props) { constructor(props) {
super('RoomActionsView', props); super('RoomActionsView', props);
const { rid } = props; const { rid, room } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.state = { this.state = {
room: this.rooms[0] || {}, room,
membersCount: 0, membersCount: 0,
member: {}, member: {},
joined: false, joined: false,
@ -86,14 +89,34 @@ export default class RoomActionsView extends LoggedView {
} catch (error) { } catch (error) {
console.log('RoomActionsView -> getRoomCounters -> error', error); console.log('RoomActionsView -> getRoomCounters -> error', error);
} }
} } else if (room.t === 'd') {
if (room.t === 'd') {
this.updateRoomMember(); this.updateRoomMember();
} }
this.rooms.addListener(this.updateRoom); 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() { componentWillUnmount() {
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
} }
@ -109,7 +132,8 @@ export default class RoomActionsView extends LoggedView {
Navigation.push(componentId, { Navigation.push(componentId, {
component: { component: {
name: item.route, name: item.route,
passProps: item.params passProps: item.params,
options: item.navigationOptions
} }
}); });
} }
@ -156,11 +180,20 @@ export default class RoomActionsView extends LoggedView {
} }
get sections() { get sections() {
const { room, membersCount, canViewMembers } = this.state; const {
room, membersCount, canViewMembers, joined
} = this.state;
const { const {
rid, t, blocker, notifications rid, t, blocker, notifications
} = room; } = room;
const notificationsAction = {
icon: `ios-notifications${ notifications ? '' : '-off' }`,
name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`),
event: () => this.toggleNotifications(),
testID: 'room-actions-notifications'
};
const sections = [{ const sections = [{
data: [{ data: [{
icon: 'ios-star', icon: 'ios-star',
@ -193,7 +226,6 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-attach', icon: 'ios-attach',
name: I18n.t('Files'), name: I18n.t('Files'),
route: 'RoomFilesView', route: 'RoomFilesView',
params: { rid },
testID: 'room-actions-files', testID: 'room-actions-files',
require: () => require('../RoomFilesView').default require: () => require('../RoomFilesView').default
}, },
@ -201,7 +233,6 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-at', icon: 'ios-at',
name: I18n.t('Mentions'), name: I18n.t('Mentions'),
route: 'MentionedMessagesView', route: 'MentionedMessagesView',
params: { rid },
testID: 'room-actions-mentioned', testID: 'room-actions-mentioned',
require: () => require('../MentionedMessagesView').default require: () => require('../MentionedMessagesView').default
}, },
@ -209,7 +240,6 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-star', icon: 'ios-star',
name: I18n.t('Starred'), name: I18n.t('Starred'),
route: 'StarredMessagesView', route: 'StarredMessagesView',
params: { rid },
testID: 'room-actions-starred', testID: 'room-actions-starred',
require: () => require('../StarredMessagesView').default require: () => require('../StarredMessagesView').default
}, },
@ -231,7 +261,6 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-pin', icon: 'ios-pin',
name: I18n.t('Pinned'), name: I18n.t('Pinned'),
route: 'PinnedMessagesView', route: 'PinnedMessagesView',
params: { rid },
testID: 'room-actions-pinned', testID: 'room-actions-pinned',
require: () => require('../PinnedMessagesView').default require: () => require('../PinnedMessagesView').default
}, },
@ -242,12 +271,6 @@ export default class RoomActionsView extends LoggedView {
params: { rid }, params: { rid },
testID: 'room-actions-snippeted', testID: 'room-actions-snippeted',
require: () => require('../SnippetedMessagesView').default 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 renderItem: this.renderItem
@ -266,6 +289,7 @@ export default class RoomActionsView extends LoggedView {
], ],
renderItem: this.renderItem renderItem: this.renderItem
}); });
sections[2].data.push(notificationsAction);
} else if (t === 'c' || t === 'p') { } else if (t === 'c' || t === 'p') {
const actions = []; const actions = [];
@ -273,7 +297,7 @@ export default class RoomActionsView extends LoggedView {
actions.push({ actions.push({
icon: 'ios-people', icon: 'ios-people',
name: I18n.t('Members'), name: I18n.t('Members'),
description: `${ membersCount } ${ I18n.t('members') }`, description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
route: 'RoomMembersView', route: 'RoomMembersView',
params: { rid }, params: { rid },
testID: 'room-actions-members', testID: 'room-actions-members',
@ -290,29 +314,42 @@ export default class RoomActionsView extends LoggedView {
nextAction: 'ADD_USER', nextAction: 'ADD_USER',
rid rid
}, },
navigationOptions: {
topBar: {
title: {
text: I18n.t('Add_user')
}
}
},
testID: 'room-actions-add-user', testID: 'room-actions-add-user',
require: () => require('../SelectedUsersView').default require: () => require('../SelectedUsersView').default
}); });
} }
sections[2].data = [...actions, ...sections[2].data]; sections[2].data = [...actions, ...sections[2].data];
sections.push({
data: [ if (joined) {
{ sections[2].data.push(notificationsAction);
icon: 'block', sections.push({
name: I18n.t('Leave_channel'), data: [
type: 'danger', {
event: () => this.leaveChannel(), icon: 'block',
testID: 'room-actions-leave-channel' name: I18n.t('Leave_channel'),
} type: 'danger',
], event: () => this.leaveChannel(),
renderItem: this.renderItem testID: 'room-actions-leave-channel'
}); }
],
renderItem: this.renderItem
});
}
} }
return sections; return sections;
} }
updateRoom = () => { updateRoom = () => {
this.setState({ room: this.rooms[0] || {} }); if (this.rooms.length > 0) {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
}
} }
updateRoomMember = async() => { updateRoomMember = async() => {
@ -322,10 +359,10 @@ export default class RoomActionsView extends LoggedView {
try { try {
const member = await RocketChat.getRoomMember(rid, userId); const member = await RocketChat.getRoomMember(rid, userId);
this.setState({ member }); this.setState({ member: member || {} });
} catch (e) { } catch (e) {
log('RoomActions updateRoomMember', e); log('RoomActions updateRoomMember', e);
return {}; this.setState({ member: {} });
} }
} }

View File

@ -11,12 +11,12 @@ import Message from '../../containers/message/Message';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis, customEmojis: state.customEmojis,
room: state.room,
user: { user: {
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
@ -39,18 +39,16 @@ export default class RoomFilesView extends LoggedView {
} }
static propTypes = { static propTypes = {
rid: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object customEmojis: PropTypes.object,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
super('RoomFilesView', props); super('RoomFilesView', props);
this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
this.state = { this.state = {
loading: false, loading: false,
room: this.rooms[0],
messages: [] messages: []
}; };
} }
@ -60,12 +58,19 @@ export default class RoomFilesView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { 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() => { load = async() => {
const { const {
messages, total, loading, room messages, total, loading
} = this.state; } = this.state;
if (messages.length === total || loading) { if (messages.length === total || loading) {
return; return;
@ -74,6 +79,7 @@ export default class RoomFilesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getFiles(room.rid, room.t, messages.length); const result = await RocketChat.getFiles(room.rid, room.t, messages.length);
if (result.success) { if (result.success) {
this.setState(prevState => ({ this.setState(prevState => ({

View File

@ -5,6 +5,7 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import { eraseRoom as eraseRoomAction } from '../../actions/room'; import { eraseRoom as eraseRoomAction } from '../../actions/room';
import LoggedView from '../View'; import LoggedView from '../View';
@ -67,7 +68,7 @@ export default class RoomInfoEditView extends LoggedView {
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = {}; this.permissions = {};
this.state = { this.state = {
room: this.rooms[0] || {}, room: JSON.parse(JSON.stringify(this.rooms[0] || {})),
name: '', name: '',
description: '', description: '',
topic: '', topic: '',
@ -90,12 +91,26 @@ export default class RoomInfoEditView extends LoggedView {
this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid); 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() { componentWillUnmount() {
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
} }
updateRoom = () => { updateRoom = () => {
this.setState({ room: this.rooms[0] || {} }); this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0] || {})) });
} }
init = () => { init = () => {

View File

@ -6,6 +6,7 @@ import moment from 'moment';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import LoggedView from '../View'; import LoggedView from '../View';
import Status from '../../containers/status'; import Status from '../../containers/status';
@ -40,9 +41,10 @@ let RoomInfoEditView = null;
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
userId: state.login.user && state.login.user.id, userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers, activeUsers: state.activeUsers, // TODO: remove it
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
allRoles: state.roles allRoles: state.roles,
room: state.room
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomInfoView extends LoggedView { export default class RoomInfoView extends LoggedView {
@ -66,29 +68,34 @@ export default class RoomInfoView extends LoggedView {
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
activeUsers: PropTypes.object, activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
allRoles: PropTypes.object allRoles: PropTypes.object,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
super('RoomInfoView', props); super('RoomInfoView', props);
const { rid } = props; const { rid, room } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = { this.sub = {
unsubscribe: () => {} unsubscribe: () => {}
}; };
this.state = { this.state = {
room: {}, room,
roomUser: {}, roomUser: {},
roles: [] roles: []
}; };
Navigation.events().bindComponent(this); Navigation.events().bindComponent(this);
} }
componentDidMount() { async componentDidMount() {
this.updateRoom();
this.rooms.addListener(this.updateRoom); 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 { componentId } = this.props;
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM]) { 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() { componentWillUnmount() {
@ -143,38 +201,9 @@ export default class RoomInfoView extends LoggedView {
return t === 'd'; return t === 'd';
} }
updateRoom = async() => { updateRoom = () => {
const { userId, activeUsers } = this.props; if (this.rooms.length > 0) {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
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);
}
}
} }
} }

View File

@ -7,6 +7,7 @@ import ActionSheet from 'react-native-actionsheet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import LoggedView from '../View'; import LoggedView from '../View';
import styles from './styles'; import styles from './styles';
@ -21,7 +22,8 @@ import SearchBox from '../../containers/SearchBox';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
@connect(state => ({ @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 */ /** @extends React.Component */
export default class RoomMembersView extends LoggedView { export default class RoomMembersView extends LoggedView {
@ -48,7 +50,8 @@ export default class RoomMembersView extends LoggedView {
componentId: PropTypes.string, componentId: PropTypes.string,
rid: PropTypes.string, rid: PropTypes.string,
members: PropTypes.array, members: PropTypes.array,
baseUrl: PropTypes.string baseUrl: PropTypes.string,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -57,7 +60,7 @@ export default class RoomMembersView extends LoggedView {
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1; this.MUTE_INDEX = 1;
this.actionSheetOptions = ['']; this.actionSheetOptions = [''];
const { rid, members } = props; const { rid, members, room } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = RocketChat.hasPermission(['mute-user'], rid); this.permissions = RocketChat.hasPermission(['mute-user'], rid);
this.state = { this.state = {
@ -67,7 +70,8 @@ export default class RoomMembersView extends LoggedView {
members, members,
membersFiltered: [], membersFiltered: [],
userLongPressed: {}, userLongPressed: {},
room: this.rooms[0] || {} room,
options: []
}; };
Navigation.events().bindComponent(this); Navigation.events().bindComponent(this);
} }
@ -77,6 +81,34 @@ export default class RoomMembersView extends LoggedView {
this.rooms.addListener(this.updateRoom); 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() { componentWillUnmount() {
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
} }
@ -118,11 +150,11 @@ export default class RoomMembersView extends LoggedView {
try { try {
const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username); const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username);
if (subscriptions.length) { if (subscriptions.length) {
this.goRoom({ rid: subscriptions[0].rid }); this.goRoom({ rid: subscriptions[0].rid, name: item.username });
} else { } else {
const result = await RocketChat.createDirectMessage(item.username); const result = await RocketChat.createDirectMessage(item.username);
if (result.success) { if (result.success) {
this.goRoom({ rid: result.room._id }); this.goRoom({ rid: result.room._id, name: item.username });
} }
} }
} catch (e) { } catch (e) {
@ -134,21 +166,25 @@ export default class RoomMembersView extends LoggedView {
if (!this.permissions['mute-user']) { if (!this.permissions['mute-user']) {
return; return;
} }
const { room } = this.state; try {
const { muted } = room; const { room } = this.state;
const { muted } = room;
this.actionSheetOptions = [I18n.t('Cancel')]; const options = [I18n.t('Cancel')];
const userIsMuted = !!muted.find(m => m.value === user.username); const userIsMuted = !!muted.find(m => m.value === user.username);
user.muted = userIsMuted; user.muted = userIsMuted;
if (userIsMuted) { if (userIsMuted) {
this.actionSheetOptions.push(I18n.t('Unmute')); options.push(I18n.t('Unmute'));
} else { } else {
this.actionSheetOptions.push(I18n.t('Mute')); options.push(I18n.t('Mute'));
} }
this.setState({ userLongPressed: user }); this.setState({ userLongPressed: user, options });
Vibration.vibrate(50); Vibration.vibrate(50);
if (this.actionSheet && this.actionSheet.show) { if (this.actionSheet && this.actionSheet.show) {
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 }); this.setState({ allUsers: status, members });
} }
updateRoom = async() => { updateRoom = () => {
const [room] = this.rooms; if (this.rooms.length > 0) {
await this.setState({ room }); const [room] = this.rooms;
this.setState({ room });
}
} }
goRoom = async({ rid }) => { goRoom = async({ rid, name }) => {
const { componentId } = this.props; const { componentId } = this.props;
await Navigation.popToRoot(componentId); await Navigation.popToRoot(componentId);
Navigation.push('RoomsListView', { Navigation.push('RoomsListView', {
component: { component: {
name: 'RoomView', name: 'RoomView',
passProps: { passProps: {
rid rid, name, t: 'd'
} }
} }
}); });
@ -219,7 +257,9 @@ export default class RoomMembersView extends LoggedView {
} }
render() { render() {
const { filtering, members, membersFiltered } = this.state; const {
filtering, members, membersFiltered, options
} = this.state;
return ( return (
<SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ bottom: 'never' }}> <SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ bottom: 'never' }}>
<FlatList <FlatList
@ -234,7 +274,7 @@ export default class RoomMembersView extends LoggedView {
<ActionSheet <ActionSheet
ref={o => this.actionSheet = o} ref={o => this.actionSheet = o}
title={I18n.t('Actions')} title={I18n.t('Actions')}
options={this.actionSheetOptions} options={options}
cancelButtonIndex={this.CANCEL_INDEX} cancelButtonIndex={this.CANCEL_INDEX}
onPress={this.handleActionPress} onPress={this.handleActionPress}
/> />

View File

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, Text, StyleSheet, Image, Platform, LayoutAnimation View, Text, StyleSheet, Image, Platform, LayoutAnimation
@ -81,7 +81,7 @@ const styles = StyleSheet.create({
status status
}; };
}) })
export default class RoomHeaderView extends PureComponent { export default class RoomHeaderView extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
@ -90,10 +90,37 @@ export default class RoomHeaderView extends PureComponent {
status: PropTypes.string 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) { componentDidUpdate(prevProps) {
const { usersTyping } = this.props; if (isIOS()) {
if (!equal(prevProps.usersTyping, usersTyping)) { const { usersTyping } = this.props;
LayoutAnimation.easeInEaseOut(); if (!equal(prevProps.usersTyping, usersTyping)) {
LayoutAnimation.easeInEaseOut();
}
} }
} }

View File

@ -1,6 +1,8 @@
import { ListView as OldList } from 'realm/react-native'; import { ListView as OldList } from 'realm/react-native';
import React from 'react'; 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 moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -9,7 +11,9 @@ import Separator from './Separator';
import styles from './styles'; import styles from './styles';
import database from '../../lib/realm'; import database from '../../lib/realm';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; 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; 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 { export class List extends React.Component {
static propTypes = { static propTypes = {
onEndReached: PropTypes.func, onEndReached: PropTypes.func,
renderFooter: PropTypes.func, renderFooter: PropTypes.func,
renderRow: PropTypes.func, renderRow: PropTypes.func,
room: PropTypes.string, room: PropTypes.object
end: PropTypes.bool,
loadingMore: PropTypes.bool
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this.data = database this.data = database
.objects('messages') .objects('messages')
.filtered('rid = $0', props.room) .filtered('rid = $0', props.room.rid)
.sorted('ts', true); .sorted('ts', true);
this.state = {
loading: true,
loadingMore: false,
end: false
};
this.dataSource = ds.cloneWithRows(this.data); this.dataSource = ds.cloneWithRows(this.data);
} }
componentDidMount() { componentDidMount() {
this.updateState();
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps, nextState) {
const { end, loadingMore } = this.props; const { loadingMore, loading, end } = this.state;
return end !== nextProps.end || loadingMore !== nextProps.loadingMore; return end !== nextState.end || loadingMore !== nextState.loadingMore || loading !== nextState.loading;
} }
componentWillUnmount() { componentWillUnmount() {
@ -60,16 +68,39 @@ export class List extends React.Component {
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
updateState = throttle(() => { updateState = debounce(() => {
// this.setState({ this.setState({ loading: true });
this.dataSource = this.dataSource.cloneWithRows(this.data); this.dataSource = this.dataSource.cloneWithRows(this.data);
// LayoutAnimation.easeInEaseOut(); this.setState({ loading: false });
this.forceUpdate(); }, 300);
// });
}, 1000); 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 <ActivityIndicator style={styles.loadingMore} />;
}
return null;
}
render() { render() {
const { renderFooter, onEndReached, renderRow } = this.props; const { renderRow } = this.props;
return ( return (
<ListView <ListView
@ -78,8 +109,8 @@ export class List extends React.Component {
data={this.data} data={this.data}
keyExtractor={item => item._id} keyExtractor={item => item._id}
onEndReachedThreshold={100} onEndReachedThreshold={100}
renderFooter={renderFooter} renderFooter={this.renderFooter}
onEndReached={() => onEndReached(this.data[this.data.length - 1])} onEndReached={this.onEndReached}
dataSource={this.dataSource} dataSource={this.dataSource}
renderRow={(item, previousItem) => renderRow(item, previousItem)} renderRow={(item, previousItem) => renderRow(item, previousItem)}
initialListSize={1} initialListSize={1}

View File

@ -5,6 +5,7 @@ import {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; 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() { componentWillUnmount() {
this.uploads.removeAllListeners(); this.uploads.removeAllListeners();
} }
@ -107,14 +120,15 @@ export default class UploadProgress extends Component {
database.write(() => { database.write(() => {
item.error = false; item.error = false;
}); });
await RocketChat.sendFileMessage(rid, JSON.parse(JSON.stringify(item))); await RocketChat.sendFileMessage(rid, item);
} catch (e) { } catch (e) {
log('UploadProgess.tryAgain', e); log('UploadProgess.tryAgain', e);
} }
} }
updateUploads = () => { updateUploads = () => {
this.setState({ uploads: this.uploads }); const uploads = this.uploads.map(item => JSON.parse(JSON.stringify(item)));
this.setState({ uploads });
} }
renderItemContent = (item) => { renderItemContent = (item) => {

View File

@ -84,6 +84,8 @@ export default class RoomView extends LoggedView {
token: PropTypes.string.isRequired token: PropTypes.string.isRequired
}), }),
rid: PropTypes.string, rid: PropTypes.string,
name: PropTypes.string,
t: PropTypes.string,
showActions: PropTypes.bool, showActions: PropTypes.bool,
showErrorActions: PropTypes.bool, showErrorActions: PropTypes.bool,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
@ -100,24 +102,20 @@ export default class RoomView extends LoggedView {
this.state = { this.state = {
loaded: false, loaded: false,
joined: this.rooms.length > 0, joined: this.rooms.length > 0,
room: {}, room: {}
end: false,
loadingMore: false
}; };
this.focused = true;
this.onReactionPress = this.onReactionPress.bind(this); this.onReactionPress = this.onReactionPress.bind(this);
Navigation.events().bindComponent(this); Navigation.events().bindComponent(this);
} }
async componentDidMount() { componentDidMount() {
if (this.rooms.length === 0 && this.rid) { if (this.rooms.length === 0 && this.rid) {
const result = await RocketChat.getRoomInfo(this.rid); const { rid, name, t } = this.props;
if (result.success) { this.setState(
const { room } = result; { room: { rid, name, t } },
this.setState( () => this.updateRoom()
{ room: { rid: room._id, t: room.t, name: room.name } }, );
() => this.updateRoom()
);
}
} }
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
this.internalSetState({ loaded: true }); this.internalSetState({ loaded: true });
@ -125,7 +123,7 @@ export default class RoomView extends LoggedView {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
room, loaded, joined, end, loadingMore room, loaded, joined
} = this.state; } = this.state;
const { showActions, showErrorActions, appState } = this.props; const { showActions, showErrorActions, appState } = this.props;
@ -143,10 +141,6 @@ export default class RoomView extends LoggedView {
return true; return true;
} else if (joined !== nextState.joined) { } else if (joined !== nextState.joined) {
return true; return true;
} else if (end !== nextState.end) {
return true;
} else if (loadingMore !== nextState.loadingMore) {
return true;
} else if (showActions !== nextProps.showActions) { } else if (showActions !== nextProps.showActions) {
return true; return true;
} else if (showErrorActions !== nextProps.showErrorActions) { } else if (showErrorActions !== nextProps.showErrorActions) {
@ -187,32 +181,18 @@ export default class RoomView extends LoggedView {
componentWillUnmount() { componentWillUnmount() {
const { closeRoom } = this.props; const { closeRoom } = this.props;
this.rooms.removeAllListeners();
if (this.onEndReached && this.onEndReached.stop) {
this.onEndReached.stop();
}
closeRoom(); closeRoom();
this.rooms.removeAllListeners();
} }
onEndReached = async(lastRowData) => { // eslint-disable-next-line
if (!lastRowData) { componentDidAppear() {
return; this.focused = true;
} }
const { loadingMore, end } = this.state; // eslint-disable-next-line
if (loadingMore || end) { componentDidDisappear() {
return; this.focused = false;
}
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);
}
} }
onMessageLongPress = (message) => { onMessageLongPress = (message) => {
@ -269,15 +249,19 @@ export default class RoomView extends LoggedView {
} }
} }
// eslint-disable-next-line react/sort-comp
updateRoom = () => { updateRoom = () => {
const { openRoom, setLastOpen } = this.props; const { openRoom, setLastOpen } = this.props;
if (!this.focused) {
return;
}
if (this.rooms.length > 0) { if (this.rooms.length > 0) {
const { room: prevRoom } = this.state; const { room: prevRoom } = this.state;
const room = JSON.parse(JSON.stringify(this.rooms[0] || {})); const room = JSON.parse(JSON.stringify(this.rooms[0] || {}));
this.internalSetState({ room }); this.internalSetState({ room });
if (!prevRoom.rid) { if (!prevRoom._id) {
openRoom({ openRoom({
...room ...room
}); });
@ -289,8 +273,10 @@ export default class RoomView extends LoggedView {
} }
} else { } else {
const { room } = this.state; const { room } = this.state;
openRoom(room); if (room.rid) {
this.internalSetState({ joined: false }); openRoom(room);
this.internalSetState({ joined: false });
}
} }
} }
@ -370,7 +356,7 @@ export default class RoomView extends LoggedView {
if (!joined) { if (!joined) {
return ( return (
<View style={styles.joinRoomContainer} key='room-view-join'> <View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'>
<Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text> <Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text>
<RectButton <RectButton
onPress={this.joinRoom} onPress={this.joinRoom}
@ -378,7 +364,7 @@ export default class RoomView extends LoggedView {
activeOpacity={0.5} activeOpacity={0.5}
underlayColor='#fff' underlayColor='#fff'
> >
<Text style={styles.joinRoomText}>{I18n.t('Join')}</Text> <Text style={styles.joinRoomText} testID='room-view-join-button'>{I18n.t('Join')}</Text>
</RectButton> </RectButton>
</View> </View>
); );
@ -400,28 +386,16 @@ export default class RoomView extends LoggedView {
return <MessageBox key='room-view-messagebox' onSubmit={this.sendMessage} rid={this.rid} />; return <MessageBox key='room-view-messagebox' onSubmit={this.sendMessage} rid={this.rid} />;
}; };
renderHeader = () => {
const { loadingMore } = this.state;
if (loadingMore) {
return <ActivityIndicator style={styles.loadingMore} />;
}
return null;
}
renderList = () => { renderList = () => {
const { loaded, end, loadingMore } = this.state; const { loaded, room } = this.state;
if (!loaded) { if (!loaded || !room.rid) {
return <ActivityIndicator style={styles.loading} />; return <ActivityIndicator style={styles.loading} />;
} }
return ( return (
[ [
<List <List
key='room-view-messages' key='room-view-messages'
end={end} room={room}
loadingMore={loadingMore}
room={this.rid}
renderFooter={this.renderHeader}
onEndReached={this.onEndReached}
renderRow={this.renderItem} renderRow={this.renderItem}
/>, />,
this.renderFooter() this.renderFooter()

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -18,7 +18,7 @@ import Header from './Header';
closeSort: () => dispatch(closeSortDropdown()), closeSort: () => dispatch(closeSortDropdown()),
setSearch: searchText => dispatch(setSearchAction(searchText)) setSearch: searchText => dispatch(setSearchAction(searchText))
})) }))
export default class RoomsListHeaderView extends Component { export default class RoomsListHeaderView extends PureComponent {
static propTypes = { static propTypes = {
showServerDropdown: PropTypes.bool, showServerDropdown: PropTypes.bool,
showSortDropdown: PropTypes.bool, showSortDropdown: PropTypes.bool,

View File

@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import * as SDK from '@rocket.chat/sdk'; import * as SDK from '@rocket.chat/sdk';
import equal from 'deep-equal';
import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms'; import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
@ -39,10 +40,12 @@ export default class ServerDropdown extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.servers = database.databases.serversDB.objects('servers');
this.state = { this.state = {
servers: [] servers: this.servers
}; };
this.animatedValue = new Animated.Value(0); this.animatedValue = new Animated.Value(0);
this.servers.addListener(this.updateState);
} }
componentDidMount() { componentDidMount() {
@ -51,12 +54,25 @@ export default class ServerDropdown extends Component {
{ {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION, duration: ANIMATION_DURATION,
easing: Easing.ease, easing: Easing.inOut(Easing.quad),
useNativeDriver: true useNativeDriver: true
}, },
).start(); ).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) { componentDidUpdate(prevProps) {
@ -78,7 +94,7 @@ export default class ServerDropdown extends Component {
{ {
toValue: 0, toValue: 0,
duration: ANIMATION_DURATION, duration: ANIMATION_DURATION,
easing: Easing.ease, easing: Easing.inOut(Easing.quad),
useNativeDriver: true useNativeDriver: true
} }
).start(() => toggleServerDropdown()); ).start(() => toggleServerDropdown());

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import { import {
View, Text, Animated, Easing, Image, TouchableWithoutFeedback View, Text, Animated, Easing, Image, TouchableWithoutFeedback
} from 'react-native'; } from 'react-native';
@ -19,7 +19,7 @@ const ANIMATION_DURATION = 200;
}), dispatch => ({ }), dispatch => ({
setSortPreference: preference => dispatch(setPreference(preference)) setSortPreference: preference => dispatch(setPreference(preference))
})) }))
export default class Sort extends Component { export default class Sort extends PureComponent {
static propTypes = { static propTypes = {
closeSortDropdown: PropTypes.bool, closeSortDropdown: PropTypes.bool,
close: PropTypes.func, close: PropTypes.func,
@ -41,7 +41,7 @@ export default class Sort extends Component {
{ {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION, duration: ANIMATION_DURATION,
easing: Easing.ease, easing: Easing.inOut(Easing.quad),
useNativeDriver: true useNativeDriver: true
}, },
).start(); ).start();
@ -95,7 +95,7 @@ export default class Sort extends Component {
{ {
toValue: 0, toValue: 0,
duration: ANIMATION_DURATION, duration: ANIMATION_DURATION,
easing: Easing.ease, easing: Easing.inOut(Easing.quad),
useNativeDriver: true useNativeDriver: true
}, },
).start(() => close()); ).start(() => close());

View File

@ -26,10 +26,12 @@ import { appStart as appStartAction } from '../../actions';
import store from '../../lib/createStore'; import store from '../../lib/createStore';
import Drawer from '../../Drawer'; import Drawer from '../../Drawer';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import debounce from '../../utils/debounce';
const ROW_HEIGHT = 70; const ROW_HEIGHT = 70;
const SCROLL_OFFSET = 56; const SCROLL_OFFSET = 56;
const shouldUpdateProps = ['searchText', 'loadingServer', 'showServerDropdown', 'showSortDropdown', 'sortBy', 'groupByType', 'showFavorites', 'showUnread', 'useRealName', 'appState'];
const isAndroid = () => Platform.OS === 'android'; const isAndroid = () => Platform.OS === 'android';
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
const keyExtractor = item => item.rid; const keyExtractor = item => item.rid;
@ -161,7 +163,61 @@ export default class RoomsListView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { 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) { componentDidUpdate(prevProps) {
@ -190,10 +246,6 @@ export default class RoomsListView extends LoggedView {
this.removeListener(this.direct); this.removeListener(this.direct);
this.removeListener(this.livechat); this.removeListener(this.livechat);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
if (this.timeout) {
clearTimeout(this.timeout);
}
} }
navigationButtonPressed = ({ buttonId }) => { navigationButtonPressed = ({ buttonId }) => {
@ -262,9 +314,7 @@ export default class RoomsListView extends LoggedView {
if (showUnread) { if (showUnread) {
this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)'); this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)');
unread = this.removeRealmInstance(this.unread); unread = this.removeRealmInstance(this.unread);
setTimeout(() => { this.unread.addListener(debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300));
this.unread.addListener(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }));
});
} else { } else {
this.removeListener(unread); this.removeListener(unread);
} }
@ -272,9 +322,7 @@ export default class RoomsListView extends LoggedView {
if (showFavorites) { if (showFavorites) {
this.favorites = this.data.filtered('f == true'); this.favorites = this.data.filtered('f == true');
favorites = this.removeRealmInstance(this.favorites); favorites = this.removeRealmInstance(this.favorites);
setTimeout(() => { this.favorites.addListener(debounce(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }), 300));
this.favorites.addListener(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }));
});
} else { } else {
this.removeListener(favorites); this.removeListener(favorites);
} }
@ -296,12 +344,10 @@ export default class RoomsListView extends LoggedView {
this.livechat = this.data.filtered('t == $0', 'l'); this.livechat = this.data.filtered('t == $0', 'l');
livechat = this.removeRealmInstance(this.livechat); livechat = this.removeRealmInstance(this.livechat);
setTimeout(() => { this.channels.addListener(debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300));
this.channels.addListener(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) })); this.privateGroup.addListener(debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300));
this.privateGroup.addListener(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) })); this.direct.addListener(debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300));
this.direct.addListener(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) })); this.livechat.addListener(debounce(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }), 300));
this.livechat.addListener(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }));
});
this.removeListener(this.chats); this.removeListener(this.chats);
} else { } else {
// chats // chats
@ -312,11 +358,7 @@ export default class RoomsListView extends LoggedView {
} }
chats = this.removeRealmInstance(this.chats); chats = this.removeRealmInstance(this.chats);
setTimeout(() => { this.chats.addListener(debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300));
this.chats.addListener(() => {
this.internalSetState({ chats: this.removeRealmInstance(this.chats) });
});
});
this.removeListener(this.channels); this.removeListener(this.channels);
this.removeListener(this.privateGroup); this.removeListener(this.privateGroup);
this.removeListener(this.direct); this.removeListener(this.direct);
@ -325,12 +367,9 @@ export default class RoomsListView extends LoggedView {
// setState // setState
this.internalSetState({ 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) => { removeRealmInstance = (data) => {
@ -399,13 +438,13 @@ export default class RoomsListView extends LoggedView {
}); });
} }
goRoom = (rid) => { goRoom = ({ rid, name, t }) => {
this.cancelSearchingAndroid(); this.cancelSearchingAndroid();
Navigation.push('RoomsListView', { Navigation.push('RoomsListView', {
component: { component: {
name: 'RoomView', name: 'RoomView',
passProps: { passProps: {
rid rid, name, t
} }
} }
}); });
@ -413,8 +452,8 @@ export default class RoomsListView extends LoggedView {
_onPressItem = async(item = {}) => { _onPressItem = async(item = {}) => {
if (!item.search) { if (!item.search) {
const { rid } = item; const { rid, name, t } = item;
return this.goRoom(rid); return this.goRoom({ rid, name, t });
} }
if (item.t === 'd') { if (item.t === 'd') {
// if user is using the search we need first to join/create room // 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 { username } = item;
const result = await RocketChat.createDirectMessage(username); const result = await RocketChat.createDirectMessage(username);
if (result.success) { if (result.success) {
return this.goRoom(result.room._id); return this.goRoom({ rid: result.room._id, name: username, t: 'd' });
} }
} catch (e) { } catch (e) {
log('RoomsListView._onPressItem', e); log('RoomsListView._onPressItem', e);
} }
} else { } else {
const { rid } = item; const { rid, name, t } = item;
return this.goRoom(rid); return this.goRoom({ rid, name, t });
} }
} }
toggleSort = () => { toggleSort = () => {
const { toggleSortDropdown } = this.props; const { toggleSortDropdown } = this.props;
if (Platform.OS === 'ios') { const offset = isAndroid() ? 0 : SCROLL_OFFSET;
this.scroll.scrollTo({ x: 0, y: SCROLL_OFFSET, animated: true }); if (this.scroll.scrollTo) {
} else { this.scroll.scrollTo({ x: 0, y: offset, animated: true });
this.scroll.scrollTo({ x: 0, y: 0, animated: true }); } else if (this.scroll.scrollToOffset) {
this.scroll.scrollToOffset({ offset });
} }
setTimeout(() => { setTimeout(() => {
toggleSortDropdown(); toggleSortDropdown();
@ -461,6 +501,7 @@ export default class RoomsListView extends LoggedView {
return ( return (
<Touch <Touch
key='rooms-list-view-sort'
onPress={this.toggleSort} onPress={this.toggleSort}
style={styles.dropdownContainerHeader} style={styles.dropdownContainerHeader}
> >
@ -474,10 +515,17 @@ export default class RoomsListView extends LoggedView {
renderSearchBar = () => { renderSearchBar = () => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
return <SearchBox onChangeText={this.search} testID='rooms-list-view-search' />; return <SearchBox onChangeText={this.search} testID='rooms-list-view-search' key='rooms-list-view-search' />;
} }
} }
renderListHeader = () => (
[
this.renderSearchBar(),
this.renderHeader()
]
)
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { useRealName, userId, baseUrl } = this.props; const { useRealName, userId, baseUrl } = this.props;
const id = item.rid.replace(userId, '').trim(); const id = item.rid.replace(userId, '').trim();
@ -504,17 +552,11 @@ export default class RoomsListView extends LoggedView {
renderSeparator = () => <View style={styles.separator} /> renderSeparator = () => <View style={styles.separator} />
renderSectionHeader = (header) => { renderSectionHeader = header => (
const { showUnread, showFavorites, groupByType } = this.props; <View style={styles.groupTitleContainer}>
if (!(showUnread || showFavorites || groupByType)) { <Text style={styles.groupTitle}>{I18n.t(header)}</Text>
return null; </View>
} )
return (
<View style={styles.groupTitleContainer}>
<Text style={styles.groupTitle}>{I18n.t(header)}</Text>
</View>
);
}
renderSection = (data, header) => { renderSection = (data, header) => {
const { showUnread, showFavorites, groupByType } = this.props; const { showUnread, showFavorites, groupByType } = this.props;
@ -542,6 +584,8 @@ export default class RoomsListView extends LoggedView {
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={12}
windowSize={7}
/> />
); );
} }
@ -566,6 +610,8 @@ export default class RoomsListView extends LoggedView {
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={12}
windowSize={7}
/> />
); );
} }
@ -590,6 +636,30 @@ export default class RoomsListView extends LoggedView {
return <ActivityIndicator style={styles.loading} />; return <ActivityIndicator style={styles.loading} />;
} }
const { showUnread, showFavorites, groupByType } = this.props;
if (!(showUnread || showFavorites || groupByType)) {
const { chats, search } = this.state;
return (
<FlatList
ref={this.getScrollRef}
data={search.length ? search : chats}
extraData={search.length ? search : chats}
contentOffset={Platform.OS === 'ios' ? { x: 0, y: SCROLL_OFFSET } : {}}
keyExtractor={keyExtractor}
style={styles.list}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
ListHeaderComponent={this.renderListHeader}
getItemLayout={getItemLayout}
enableEmptySections
removeClippedSubviews
keyboardShouldPersistTaps='always'
initialNumToRender={12}
windowSize={7}
/>
);
}
return ( return (
<ScrollView <ScrollView
ref={this.getScrollRef} ref={this.getScrollRef}
@ -597,8 +667,7 @@ export default class RoomsListView extends LoggedView {
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
testID='rooms-list-view-list' testID='rooms-list-view-list'
> >
{this.renderSearchBar()} {this.renderListHeader()}
{this.renderHeader()}
{this.renderList()} {this.renderList()}
</ScrollView> </ScrollView>
); );

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { View, FlatList, Text } from 'react-native'; import { View, FlatList, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import LoggedView from '../View'; import LoggedView from '../View';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
@ -15,7 +16,6 @@ import Message from '../../containers/message/Message';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import database from '../../lib/realm';
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -50,10 +50,8 @@ export default class SearchMessagesView extends LoggedView {
constructor(props) { constructor(props) {
super('SearchMessagesView', props); super('SearchMessagesView', props);
this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
this.state = { this.state = {
loading: false, loading: false,
room: this.rooms[0],
messages: [], messages: [],
searchText: '' searchText: ''
}; };
@ -63,17 +61,31 @@ export default class SearchMessagesView extends LoggedView {
this.name.focus(); 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() { componentWillUnmount() {
this.search.stop(); this.search.stop();
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
search = debounce(async(searchText) => { search = debounce(async(searchText) => {
const { room } = this.state; const { rid } = this.props;
this.setState({ searchText, loading: true, messages: [] }); this.setState({ searchText, loading: true, messages: [] });
try { try {
const result = await RocketChat.searchMessages(room.rid, searchText); const result = await RocketChat.searchMessages(rid, searchText);
if (result.success) { if (result.success) {
this.setState({ this.setState({
messages: result.messages || [], messages: result.messages || [],

View File

@ -21,5 +21,11 @@ export default StyleSheet.create({
height: StyleSheet.hairlineWidth, height: StyleSheet.hairlineWidth,
backgroundColor: '#E7EBF2', backgroundColor: '#E7EBF2',
marginVertical: 20 marginVertical: 20
},
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
backgroundColor: '#ffffff'
} }
}); });

View File

@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux';
import { Navigation } from 'react-native-navigation'; import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import { import {
addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction 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); 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) { componentDidUpdate(prevProps) {
const { componentId, users } = this.props; const { componentId, users } = this.props;
if (prevProps.users.length !== users.length) { if (prevProps.users.length !== users.length) {

View File

@ -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() { componentWillUnmount() {
if (this.timeout) { if (this.timeout) {
clearTimeout(this.timeout); clearTimeout(this.timeout);

View File

@ -89,6 +89,21 @@ export default class SettingsView extends LoggedView {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); 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() { componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FlatList, View, Text } from 'react-native'; import { FlatList, View, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages'; import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages';
import LoggedView from '../View'; 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() { componentWillUnmount() {
const { closeSnippetedMessages } = this.props; const { closeSnippetedMessages } = this.props;
closeSnippetedMessages(); closeSnippetedMessages();

View File

@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DEFAULT_HEADER } from '../../constants/headerOptions'; import { DEFAULT_HEADER } from '../../constants/headerOptions';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database from '../../lib/realm';
const STAR_INDEX = 0; const STAR_INDEX = 0;
const CANCEL_INDEX = 1; const CANCEL_INDEX = 1;
@ -22,6 +21,7 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')];
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis, customEmojis: state.customEmojis,
room: state.room,
user: { user: {
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
@ -44,18 +44,16 @@ export default class StarredMessagesView extends LoggedView {
} }
static propTypes = { static propTypes = {
rid: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object customEmojis: PropTypes.object,
room: PropTypes.object
} }
constructor(props) { constructor(props) {
super('StarredMessagesView', props); super('StarredMessagesView', props);
this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
this.state = { this.state = {
loading: false, loading: false,
room: this.rooms[0],
messages: [] messages: []
}; };
} }
@ -65,7 +63,14 @@ export default class StarredMessagesView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { 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) => { onLongPress = (message) => {
@ -101,7 +106,7 @@ export default class StarredMessagesView extends LoggedView {
load = async() => { load = async() => {
const { const {
messages, total, loading, room messages, total, loading
} = this.state; } = this.state;
const { user } = this.props; const { user } = this.props;
if (messages.length === total || loading) { if (messages.length === total || loading) {
@ -111,6 +116,7 @@ export default class StarredMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getMessages( const result = await RocketChat.getMessages(
room.rid, room.rid,
room.t, room.t,

View File

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

View File

@ -1 +1 @@
--recursive --timeout 120000 --recursive --timeout 120000 -b

20
package-lock.json generated
View File

@ -18282,21 +18282,21 @@
} }
}, },
"react-native-navigation": { "react-native-navigation": {
"version": "2.1.3", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.1.3.tgz", "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.2.1.tgz",
"integrity": "sha512-CtjDhw7eaDWCqfhK6Fq6IUDq3agl3oGDd8SNaPF7318tLni1qTmmFdz/3CpoNlegNWhAMAjNu+ONDAbe7ksADw==", "integrity": "sha512-m0RyVQMiNNIoMlcy3zADgazRRU5qeJNOpRhx9ERA/V5t2uPq1vTGoG3bvoYbkFmsrsgyWZax4l+5AdnygDzNKg==",
"requires": { "requires": {
"hoist-non-react-statics": "3.x.x", "hoist-non-react-statics": "3.x.x",
"lodash": "4.x.x", "lodash": "4.17.x",
"prop-types": "15.x.x", "prop-types": "15.x.x",
"react-lifecycles-compat": "2.0.0", "react-lifecycles-compat": "2.0.0",
"tslib": "1.9.3" "tslib": "1.9.3"
}, },
"dependencies": { "dependencies": {
"hoist-non-react-statics": { "hoist-non-react-statics": {
"version": "3.1.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz",
"integrity": "sha512-MYcYuROh7SBM69xHGqXEwQqDux34s9tz+sCnxJmN18kgWh6JFdTw/5YdZtqsOdZJXddE/wUpCzfEdDrJj8p0Iw==", "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==",
"requires": { "requires": {
"react-is": "^16.3.2" "react-is": "^16.3.2"
} }
@ -19958,9 +19958,9 @@
} }
}, },
"semver": { "semver": {
"version": "5.4.1", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
}, },
"send": { "send": {
"version": "0.16.2", "version": "0.16.2",

View File

@ -51,7 +51,7 @@
"react-native-keyboard-tracking-view": "^5.5.0", "react-native-keyboard-tracking-view": "^5.5.0",
"react-native-markdown-renderer": "^3.2.8", "react-native-markdown-renderer": "^3.2.8",
"react-native-modal": "^7.0.0", "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-notifications": "^1.1.21",
"react-native-optimized-flatlist": "^1.0.4", "react-native-optimized-flatlist": "^1.0.4",
"react-native-picker-select": "^5.1.0", "react-native-picker-select": "^5.1.0",
@ -70,6 +70,7 @@
"redux-immutable-state-invariant": "^2.1.0", "redux-immutable-state-invariant": "^2.1.0",
"redux-saga": "^0.16.2", "redux-saga": "^0.16.2",
"rn-fetch-blob": "^0.10.13", "rn-fetch-blob": "^0.10.13",
"semver": "^5.6.0",
"snyk": "^1.109.0", "snyk": "^1.109.0",
"strip-ansi": "^4.0.0" "strip-ansi": "^4.0.0"
}, },