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)
[![Known Vulnerabilities](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative/badge.svg)](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative)
**Supported Server Versions:** 0.66.0+
## Download
<a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
<img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,6 +90,28 @@ export default class UploadModal extends Component {
return null;
}
shouldComponentUpdate(nextProps, nextState) {
const { name, description, file } = this.state;
const { window, isVisible } = this.props;
if (nextState.name !== name) {
return true;
}
if (nextState.description !== description) {
return true;
}
if (nextProps.isVisible !== isVisible) {
return true;
}
if (nextProps.window.width !== window.width) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
return false;
}
submit = () => {
const { file, submit } = this.props;
const { name, description } = this.state;
@ -162,12 +184,12 @@ export default class UploadModal extends Component {
<ScrollView style={styles.scrollView}>
<Image source={{ isStatic: true, uri: file.path }} style={styles.image} />
<TextInput
placeholder='File name'
placeholder={I18n.t('File_name')}
value={name}
onChangeText={value => this.setState({ name: value })}
/>
<TextInput
placeholder='File description'
placeholder={I18n.t('File_description')}
value={description}
onChangeText={value => this.setState({ description: value })}
/>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert, Image
@ -9,6 +9,7 @@ import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import { userTyping as userTypingAction } from '../../actions/room';
import {
@ -59,7 +60,7 @@ const imagePickerConfig = {
typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction())
}))
export default class MessageBox extends React.PureComponent {
export default class MessageBox extends Component {
static propTypes = {
rid: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired,
@ -108,6 +109,43 @@ export default class MessageBox extends React.PureComponent {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
} = this.state;
const {
roomType, replying, editing
} = this.props;
if (nextProps.roomType !== roomType) {
return true;
}
if (nextProps.replying !== replying) {
return true;
}
if (nextProps.editing !== editing) {
return true;
}
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true;
}
if (nextState.showFilesAction !== showFilesAction) {
return true;
}
if (nextState.showSend !== showSend) {
return true;
}
if (nextState.recording !== recording) {
return true;
}
if (!equal(nextState.mentions, mentions)) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
return false;
}
onChangeText(text) {
const { typing } = this.props;

View File

@ -6,6 +6,7 @@ import {
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Navigation } from 'react-native-navigation';
import equal from 'deep-equal';
import { setStackRoot as setStackRootAction } from '../actions';
import { logout as logoutAction } from '../actions/login';
@ -111,7 +112,8 @@ export default class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
showStatus: false
showStatus: false,
status: []
};
Navigation.events().bindComponent(this);
}
@ -127,6 +129,43 @@ export default class Sidebar extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const { status, showStatus } = this.state;
const {
Site_Name, stackRoot, user, baseUrl
} = this.props;
if (nextState.showStatus !== showStatus) {
return true;
}
if (nextProps.Site_Name !== Site_Name) {
return true;
}
if (nextProps.stackRoot !== stackRoot) {
return true;
}
if (nextProps.Site_Name !== Site_Name) {
return true;
}
if (nextProps.baseUrl !== baseUrl) {
return true;
}
if (nextProps.user && user) {
if (nextProps.user.language !== user.language) {
return true;
}
if (nextProps.user.status !== user.status) {
return true;
}
if (nextProps.user.username !== user.username) {
return true;
}
}
if (!equal(nextState.status, status)) {
return true;
}
return false;
}
handleChangeStack = (event) => {
const { stack } = event;
this.setStack(stack);
@ -140,22 +179,20 @@ export default class Sidebar extends Component {
}
setStatus = () => {
setTimeout(() => {
this.setState({
status: [{
id: 'online',
name: I18n.t('Online')
}, {
id: 'busy',
name: I18n.t('Busy')
}, {
id: 'away',
name: I18n.t('Away')
}, {
id: 'offline',
name: I18n.t('Invisible')
}]
});
this.setState({
status: [{
id: 'online',
name: I18n.t('Online')
}, {
id: 'busy',
name: I18n.t('Busy')
}, {
id: 'away',
name: I18n.t('Away')
}, {
id: 'offline',
name: I18n.t('Invisible')
}]
});
}

View File

@ -7,6 +7,7 @@ import Video from 'react-native-video';
import Slider from 'react-native-slider';
import moment from 'moment';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Markdown from './Markdown';
@ -47,7 +48,7 @@ const styles = StyleSheet.create({
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
export default class Audio extends React.PureComponent {
export default class Audio extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
@ -69,6 +70,29 @@ export default class Audio extends React.PureComponent {
};
}
shouldComponentUpdate(nextProps, nextState) {
const {
currentTime, duration, paused, uri
} = this.state;
const { file } = this.props;
if (nextState.currentTime !== currentTime) {
return true;
}
if (nextState.duration !== duration) {
return true;
}
if (nextState.paused !== paused) {
return true;
}
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
return false;
}
onLoad(data) {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
}

View File

@ -1,13 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
export default class extends React.PureComponent {
export default class extends Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
@ -18,7 +19,22 @@ export default class extends React.PureComponent {
])
}
state = { modalVisible: false };
state = { modalVisible: false, isPressed: false };
shouldComponentUpdate(nextProps, nextState) {
const { modalVisible, isPressed } = this.state;
const { file } = this.props;
if (nextState.modalVisible !== modalVisible) {
return true;
}
if (nextState.isPressed !== isPressed) {
return true;
}
if (!equal(nextProps.file, file)) {
return true;
}
return false;
}
onPressButton() {
this.setState({

View File

@ -155,6 +155,8 @@ export default {
Error_uploading: 'Error uploading',
Favorites: 'Favorites',
Files: 'Files',
File_description: 'File description',
File_name: 'File name',
Finish_recording: 'Finish recording',
For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
Forgot_my_password: 'Forgot my password',
@ -170,6 +172,7 @@ export default {
is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing',
Invalid_server_version: 'The server you\'re trying to connect is using a version that\'s not supported by the app anymore: {{currentVersion}}.\n\nWe require version {{minVersion}}',
Join_the_community: 'Join the community',
Join: 'Join',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',

View File

@ -162,6 +162,8 @@ export default {
Error_uploading: 'Erro subindo',
Favorites: 'Favoritos',
Files: 'Arquivos',
File_description: 'Descrição do arquivo',
File_name: 'Nome do arquivo',
Finish_recording: 'Encerrar gravação',
For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você precisa digitar sua senha',
Forgot_my_password: 'Esqueci minha senha',
@ -175,6 +177,7 @@ export default {
Invisible: 'Invisível',
Invite: 'Convidar',
is_typing: 'está digitando',
Invalid_server_version: 'O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.',
Join_the_community: 'Junte-se à comunidade',
Join: 'Entrar',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,5 +1,5 @@
import { put, takeLatest } from 'redux-saga/effects';
import { AsyncStorage } from 'react-native';
import { AsyncStorage, Alert } from 'react-native';
import { Navigation } from 'react-native-navigation';
import { Provider } from 'react-redux';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
@ -13,6 +13,7 @@ import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log';
import store from '../lib/createStore';
import I18n from '../i18n';
let LoginSignupView = null;
let LoginView = null;
@ -49,7 +50,13 @@ const handleSelectServer = function* handleSelectServer({ server }) {
const handleServerRequest = function* handleServerRequest({ server }) {
try {
yield RocketChat.testServer(server);
const result = yield RocketChat.testServer(server);
if (!result.success) {
Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions));
yield put(serverFailure());
return;
}
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
if (LoginView == null) {

View File

@ -6,6 +6,7 @@ import {
} from 'react-native';
import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import Loading from '../containers/Loading';
import LoggedView from './View';
@ -128,6 +129,43 @@ export default class CreateChannelView extends LoggedView {
}, 600);
}
shouldComponentUpdate(nextProps, nextState) {
const {
channelName, type, readOnly, broadcast
} = this.state;
const {
error, failure, isFetching, result, users
} = this.props;
if (nextState.channelName !== channelName) {
return true;
}
if (nextState.type !== type) {
return true;
}
if (nextState.readOnly !== readOnly) {
return true;
}
if (nextState.broadcast !== broadcast) {
return true;
}
if (nextProps.failure !== failure) {
return true;
}
if (nextProps.isFetching !== isFetching) {
return true;
}
if (!equal(nextProps.error, error)) {
return true;
}
if (!equal(nextProps.result, result)) {
return true;
}
if (!equal(nextProps.users, users)) {
return true;
}
return false;
}
componentDidUpdate(prevProps) {
const {
isFetching, failure, error, result, componentId
@ -139,13 +177,14 @@ export default class CreateChannelView extends LoggedView {
const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
showErrorAlert(msg);
} else {
const { rid } = result;
const { type } = this.state;
const { rid, name } = result;
await Navigation.dismissModal(componentId);
Navigation.push('RoomsListView', {
component: {
name: 'RoomView',
passProps: {
rid
rid, name, t: type ? 'p' : 'c'
}
}
});

View File

@ -44,6 +44,20 @@ export default class ForgotPasswordView extends LoggedView {
}, 600);
}
shouldComponentUpdate(nextProps, nextState) {
const { email, invalidEmail, isFetching } = this.state;
if (nextState.email !== email) {
return true;
}
if (nextState.invalidEmail !== invalidEmail) {
return true;
}
if (nextState.isFetching !== isFetching) {
return true;
}
return false;
}
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout);

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select';
import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import LoggedView from '../View';
import KeyboardView from '../../presentation/KeyboardView';
@ -118,6 +119,16 @@ export default class ProfileView extends LoggedView {
}
}
shouldComponentUpdate(nextProps, nextState) {
if (!equal(nextState, this.state)) {
return true;
}
if (!equal(nextProps, this.props)) {
return true;
}
return false;
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import moment from 'moment';
import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import LoggedView from '../View';
import Status from '../../containers/status';
@ -40,9 +41,10 @@ let RoomInfoEditView = null;
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers,
activeUsers: state.activeUsers, // TODO: remove it
Message_TimeFormat: state.settings.Message_TimeFormat,
allRoles: state.roles
allRoles: state.roles,
room: state.room
}))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView {
@ -66,29 +68,34 @@ export default class RoomInfoView extends LoggedView {
baseUrl: PropTypes.string,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
allRoles: PropTypes.object
allRoles: PropTypes.object,
room: PropTypes.object
}
constructor(props) {
super('RoomInfoView', props);
const { rid } = props;
const { rid, room } = props;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = {
unsubscribe: () => {}
};
this.state = {
room: {},
room,
roomUser: {},
roles: []
};
Navigation.events().bindComponent(this);
}
componentDidMount() {
this.updateRoom();
async componentDidMount() {
this.rooms.addListener(this.updateRoom);
const [room] = this.rooms;
let room = {};
if (this.rooms.length > 0) {
room = this.rooms[0]; // eslint-disable-line prefer-destructuring
} else {
room = this.state.room; // eslint-disable-line
}
const { componentId } = this.props;
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM]) {
@ -102,6 +109,57 @@ export default class RoomInfoView extends LoggedView {
}
});
}
// get user of room
if (room) {
if (room.t === 'd') {
try {
const { userId, activeUsers } = this.props;
const roomUser = await RocketChat.getRoomMember(room.rid, userId);
this.setState({ roomUser: roomUser || {} });
const username = room.name;
const activeUser = activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer
this.getFullUserData(username);
}
// get all users roles
// needs to be changed by a better method
const allUsersRoles = await RocketChat.getUserRoles();
const userRoles = allUsersRoles.find(user => user.username === username);
if (userRoles) {
this.setState({ roles: userRoles.roles || [] });
}
} catch (e) {
log('RoomInfoView.componentDidMount', e);
}
}
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
room, roomUser, roles
} = this.state;
const { activeUsers } = this.props;
if (!equal(nextState.room, room)) {
return true;
}
if (!equal(nextState.roomUser, roomUser)) {
return true;
}
if (!equal(nextState.roles, roles)) {
return true;
}
if (roomUser._id) {
if (nextProps.activeUsers[roomUser._id] !== activeUsers[roomUser._id]) {
return true;
}
}
return false;
}
componentWillUnmount() {
@ -143,38 +201,9 @@ export default class RoomInfoView extends LoggedView {
return t === 'd';
}
updateRoom = async() => {
const { userId, activeUsers } = this.props;
const [room] = this.rooms;
this.setState({ room });
// get user of room
if (room) {
if (room.t === 'd') {
try {
const roomUser = await RocketChat.getRoomMember(room.rid, userId);
this.setState({ roomUser: roomUser || {} });
const username = room.name;
const activeUser = activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer
this.getFullUserData(username);
}
// get all users roles
// needs to be changed by a better method
const allUsersRoles = await RocketChat.getUserRoles();
const userRoles = allUsersRoles.find(user => user.username === username);
if (userRoles) {
this.setState({ roles: userRoles.roles || [] });
}
} catch (e) {
log('RoomInfoView.componentDidMount', e);
}
}
updateRoom = () => {
if (this.rooms.length > 0) {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
}
}

View File

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

View File

@ -1,6 +1,8 @@
import { ListView as OldList } from 'realm/react-native';
import React from 'react';
import { ScrollView, ListView as OldList2, ImageBackground } from 'react-native';
import {
ScrollView, ListView as OldList2, ImageBackground, ActivityIndicator
} from 'react-native';
import moment from 'moment';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
@ -9,7 +11,9 @@ import Separator from './Separator';
import styles from './styles';
import database from '../../lib/realm';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import throttle from '../../utils/throttle';
import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100;
@ -24,34 +28,38 @@ export class DataSource extends OldList.DataSource {
}
}
const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id || r1._updatedAt.toISOString() !== r2._updatedAt.toISOString() });
const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
export class List extends React.Component {
static propTypes = {
onEndReached: PropTypes.func,
renderFooter: PropTypes.func,
renderRow: PropTypes.func,
room: PropTypes.string,
end: PropTypes.bool,
loadingMore: PropTypes.bool
room: PropTypes.object
};
constructor(props) {
super(props);
this.data = database
.objects('messages')
.filtered('rid = $0', props.room)
.filtered('rid = $0', props.room.rid)
.sorted('ts', true);
this.state = {
loading: true,
loadingMore: false,
end: false
};
this.dataSource = ds.cloneWithRows(this.data);
}
componentDidMount() {
this.updateState();
this.data.addListener(this.updateState);
}
shouldComponentUpdate(nextProps) {
const { end, loadingMore } = this.props;
return end !== nextProps.end || loadingMore !== nextProps.loadingMore;
shouldComponentUpdate(nextProps, nextState) {
const { loadingMore, loading, end } = this.state;
return end !== nextState.end || loadingMore !== nextState.loadingMore || loading !== nextState.loading;
}
componentWillUnmount() {
@ -60,16 +68,39 @@ export class List extends React.Component {
}
// eslint-disable-next-line react/sort-comp
updateState = throttle(() => {
// this.setState({
updateState = debounce(() => {
this.setState({ loading: true });
this.dataSource = this.dataSource.cloneWithRows(this.data);
// LayoutAnimation.easeInEaseOut();
this.forceUpdate();
// });
}, 1000);
this.setState({ loading: false });
}, 300);
onEndReached = async() => {
const { loadingMore, end } = this.state;
if (loadingMore || end || this.data.length < 50) {
return;
}
this.setState({ loadingMore: true });
const { room } = this.props;
try {
const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts });
this.setState({ end: result.length < 50, loadingMore: false });
} catch (e) {
this.setState({ loadingMore: false });
log('ListView.onEndReached', e);
}
}
renderFooter = () => {
const { loadingMore, loading } = this.state;
if (loadingMore || loading) {
return <ActivityIndicator style={styles.loadingMore} />;
}
return null;
}
render() {
const { renderFooter, onEndReached, renderRow } = this.props;
const { renderRow } = this.props;
return (
<ListView
@ -78,8 +109,8 @@ export class List extends React.Component {
data={this.data}
keyExtractor={item => item._id}
onEndReachedThreshold={100}
renderFooter={renderFooter}
onEndReached={() => onEndReached(this.data[this.data.length - 1])}
renderFooter={this.renderFooter}
onEndReached={this.onEndReached}
dataSource={this.dataSource}
renderRow={(item, previousItem) => renderRow(item, previousItem)}
initialListSize={1}

View File

@ -5,6 +5,7 @@ import {
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
@ -84,6 +85,18 @@ export default class UploadProgress extends Component {
});
}
shouldComponentUpdate(nextProps, nextState) {
const { uploads } = this.state;
const { window } = this.props;
if (nextProps.window.width !== window.width) {
return true;
}
if (!equal(nextState.uploads, uploads)) {
return true;
}
return false;
}
componentWillUnmount() {
this.uploads.removeAllListeners();
}
@ -107,14 +120,15 @@ export default class UploadProgress extends Component {
database.write(() => {
item.error = false;
});
await RocketChat.sendFileMessage(rid, JSON.parse(JSON.stringify(item)));
await RocketChat.sendFileMessage(rid, item);
} catch (e) {
log('UploadProgess.tryAgain', e);
}
}
updateUploads = () => {
this.setState({ uploads: this.uploads });
const uploads = this.uploads.map(item => JSON.parse(JSON.stringify(item)));
this.setState({ uploads });
}
renderItemContent = (item) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux';
import { Navigation } from 'react-native-navigation';
import SafeAreaView from 'react-native-safe-area-view';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import {
addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction
@ -80,6 +81,21 @@ export default class SelectedUsersView extends LoggedView {
Navigation.events().bindComponent(this);
}
shouldComponentUpdate(nextProps, nextState) {
const { search } = this.state;
const { users, loading } = this.props;
if (nextProps.loading !== loading) {
return true;
}
if (!equal(nextProps.users, users)) {
return true;
}
if (!equal(nextState.search, search)) {
return true;
}
return false;
}
componentDidUpdate(prevProps) {
const { componentId, users } = this.props;
if (prevProps.users.length !== users.length) {

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

View File

@ -89,6 +89,21 @@ export default class SettingsView extends LoggedView {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
shouldComponentUpdate(nextProps, nextState) {
const { language, saving } = this.state;
const { userLanguage } = this.props;
if (nextState.language !== language) {
return true;
}
if (nextState.saving !== saving) {
return true;
}
if (nextProps.userLanguage !== userLanguage) {
return true;
}
return false;
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { FlatList, View, Text } from 'react-native';
import { connect } from 'react-redux';
import SafeAreaView from 'react-native-safe-area-view';
import equal from 'deep-equal';
import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages';
import LoggedView from '../View';
@ -68,6 +69,24 @@ export default class SnippetedMessagesView extends LoggedView {
}
}
shouldComponentUpdate(nextProps, nextState) {
const { loading, loadingMore } = this.state;
const { messages, ready } = this.props;
if (nextState.loading !== loading) {
return true;
}
if (nextState.loadingMore !== loadingMore) {
return true;
}
if (nextProps.ready !== ready) {
return true;
}
if (!equal(nextState.messages, messages)) {
return true;
}
return false;
}
componentWillUnmount() {
const { closeSnippetedMessages } = this.props;
closeSnippetedMessages();

View File

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

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

View File

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