From 50eb03589ac3afa7290ff1488ecf12876f3b4cee Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 1 Aug 2018 16:35:06 -0300 Subject: [PATCH] Improve RoomsList render time (#384) @RocketChat/ReactNative - [x] Added FlatList.getItemLayout() to improve list render time - [x] Some texts were breaking lines at sidebar - [x] Removed onPress from links at RoomsListView - [x] Added eslint rule to prevent unused styles - [x] Fixed auto focus bug at CreateChannel and NewServer - [x] Fix change server bug - [x] Fixed a bug when resuming in ListServer - [x] I18n fixed - [x] Fixed a bug on actionsheet ref not being created - [x] Reply wasn't showing on Android - [x] Use Notification.Builder.setColor/getColor only after Android SDK 23 - [x] Listen to app state only when inside app - [x] Switched register push token position in order to improve login performance - [x] When deep link changes server, it doesn't refresh rooms list - [x] Added SafeAreaView in all views to improve iPhone X experience - [x] Subpath regex #388 --- .eslintrc.js | 3 +- android/app/build.gradle | 6 +- android/app/src/main/AndroidManifest.xml | 4 +- .../reactnative/CustomPushNotification.java | 5 +- .../rocket/reactnative/MainApplication.java | 2 +- app/actions/actionsTypes.js | 3 +- app/actions/server.js | 12 ++- app/containers/Button/index.js | 1 + app/containers/Loading.js | 16 ++- app/containers/MessageActions.js | 6 +- app/containers/MessageBox/FilesActions.js | 6 +- app/containers/MessageBox/ReplyPreview.js | 6 -- app/containers/MessageErrorActions.js | 6 +- app/containers/Sidebar.js | 39 +++---- app/containers/message/Image.js | 21 +--- app/containers/message/Markdown.js | 6 +- app/containers/message/Reply.js | 7 -- app/containers/message/Url.js | 7 -- app/containers/message/styles.js | 14 +++ app/lib/ddp.js | 19 +++- app/lib/methods/getCustomEmojis.js | 3 + app/lib/methods/helpers/normalizeMessage.js | 5 + app/lib/realm.js | 44 -------- app/lib/rocketchat.js | 101 ++---------------- app/presentation/RoomItem.js | 15 +-- app/reducers/server.js | 14 ++- app/sagas/deepLinking.js | 4 +- app/sagas/init.js | 7 +- app/sagas/login.js | 31 +++--- app/sagas/selectServer.js | 18 ++-- app/sagas/state.js | 8 ++ app/views/CreateChannelView.js | 8 +- app/views/ForgotPasswordView.js | 4 +- app/views/ListServerView.js | 61 +++++------ app/views/LoginSignupView.js | 6 +- app/views/LoginView.js | 6 +- app/views/MentionedMessagesView/index.js | 8 +- app/views/NewServerView.js | 12 ++- app/views/PinnedMessagesView/index.js | 15 ++- app/views/PrivacyPolicyView.js | 8 +- app/views/ProfileView/index.js | 2 +- app/views/RegisterView.js | 6 +- app/views/RoomActionsView/index.js | 6 +- app/views/RoomActionsView/styles.js | 1 + app/views/RoomFilesView/index.js | 8 +- app/views/RoomInfoEditView/index.js | 13 +-- app/views/RoomInfoView/index.js | 26 ++--- app/views/RoomInfoView/styles.js | 4 + app/views/RoomMembersView/index.js | 17 ++- app/views/RoomView/index.js | 6 +- app/views/RoomsListView/Search/index.js | 11 +- app/views/RoomsListView/index.js | 64 +++++++---- app/views/RoomsListView/styles.js | 3 + app/views/SearchMessagesView/index.js | 9 +- app/views/SelectedUsersView.js | 10 +- app/views/SettingsView/index.js | 17 ++- app/views/SnippetedMessagesView/index.js | 8 +- app/views/StarredMessagesView/index.js | 15 ++- app/views/TermsServiceView.js | 8 +- index.js => index.ios.js | 3 - ios/RocketChatRN/AppDelegate.m | 2 +- ios/RocketChatRN/Info.plist | 2 +- 62 files changed, 381 insertions(+), 427 deletions(-) rename index.js => index.ios.js (67%) diff --git a/.eslintrc.js b/.eslintrc.js index 933fb9646..fa34f445a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -124,7 +124,8 @@ module.exports = { "prefer-const": 2, "object-shorthand": 2, "consistent-return": 0, - "global-require": "off" + "global-require": "off", + "react-native/no-unused-styles": 2 }, "globals": { "__DEV__": true diff --git a/android/app/build.gradle b/android/app/build.gradle index c230bae76..56fdfe8bb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -72,6 +72,10 @@ import com.android.build.OutputFile * ] */ +project.ext.react = [ + entryFile: "index.android.js" +] + apply from: "../../node_modules/react-native/react.gradle" /** @@ -98,7 +102,7 @@ android { minSdkVersion 19 targetSdkVersion 27 versionCode VERSIONCODE as Integer - versionName "1" + versionName "1.0.1" ndk { abiFilters "armeabi-v7a", "x86" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9d13f8947..99c2e02d8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,5 @@ + package="chat.rocket.reactnative"> diff --git a/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java index 3c06b53c4..2ad9d0062 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/CustomPushNotification.java @@ -46,10 +46,13 @@ public class CustomPushNotification extends PushNotification { .setContentText(message) .setStyle(new Notification.BigTextStyle().bigText(message)) .setPriority(Notification.PRIORITY_HIGH) - .setColor(mContext.getColor(R.color.notification_text)) .setDefaults(Notification.DEFAULT_ALL) .setAutoCancel(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notification.setColor(mContext.getColor(R.color.notification_text)); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java index fdd50ce07..8c54d8dd8 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java @@ -43,7 +43,7 @@ public class MainApplication extends NavigationApplication implements INotificat @Override public String getJSMainModuleName() { - return "index"; + return "index.android"; } protected List getPackages() { diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 961b88eee..f65142f66 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -75,7 +75,8 @@ export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']); export const SERVER = createRequestTypes('SERVER', [ ...defaultTypes, - 'SELECT', + 'SELECT_SUCCESS', + 'SELECT_REQUEST', 'CHANGED', 'ADD' ]); diff --git a/app/actions/server.js b/app/actions/server.js index 887f60a92..4bca6c236 100644 --- a/app/actions/server.js +++ b/app/actions/server.js @@ -1,11 +1,19 @@ import { SERVER } from './actionsTypes'; -export function selectServer(server) { +export function selectServerRequest(server) { return { - type: SERVER.SELECT, + type: SERVER.SELECT_REQUEST, server }; } + +export function selectServerSuccess(server) { + return { + type: SERVER.SELECT_SUCCESS, + server + }; +} + export function serverRequest(server) { return { type: SERVER.REQUEST, diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js index 32eb7de94..2709aae9f 100644 --- a/app/containers/Button/index.js +++ b/app/containers/Button/index.js @@ -13,6 +13,7 @@ const colors = { textColorSecondary: COLOR_TEXT }; +/* eslint-disable react-native/no-unused-styles */ const styles = StyleSheet.create({ container: { paddingHorizontal: 15, diff --git a/app/containers/Loading.js b/app/containers/Loading.js index 4e4f795c0..d9c8fe4ca 100644 --- a/app/containers/Loading.js +++ b/app/containers/Loading.js @@ -66,13 +66,21 @@ export default class Loading extends React.PureComponent { } componentWillUnmount() { - this.opacityAnimation.stop(); - this.scaleAnimation.stop(); + if (this.opacityAnimation && this.opacityAnimation.stop) { + this.opacityAnimation.stop(); + } + if (this.scaleAnimation && this.scaleAnimation.stop) { + this.scaleAnimation.stop(); + } } startAnimations() { - this.opacityAnimation.start(); - this.scaleAnimation.start(); + if (this.opacityAnimation && this.opacityAnimation.start) { + this.opacityAnimation.start(); + } + if (this.scaleAnimation && this.scaleAnimation.start) { + this.scaleAnimation.start(); + } } render() { diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index 7eec64339..dc249b426 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -121,7 +121,9 @@ export default class MessageActions extends React.Component { this.DELETE_INDEX = this.options.length - 1; } setTimeout(() => { - this.ActionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } Vibration.vibrate(50); }); } @@ -301,7 +303,7 @@ export default class MessageActions extends React.Component { render() { return ( this.ActionSheet = o} + ref={o => this.actionSheet = o} title={I18n.t('Message_actions')} testID='message-actions' options={this.options} diff --git a/app/containers/MessageBox/FilesActions.js b/app/containers/MessageBox/FilesActions.js index e7def299d..c5d53b16a 100644 --- a/app/containers/MessageBox/FilesActions.js +++ b/app/containers/MessageBox/FilesActions.js @@ -27,7 +27,9 @@ export default class FilesActions extends Component { this.LIBRARY_INDEX = 2; setTimeout(() => { - this.ActionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } }); } @@ -49,7 +51,7 @@ export default class FilesActions extends Component { render() { return ( this.ActionSheet = o} + ref={o => this.actionSheet = o} options={this.options} cancelButtonIndex={this.CANCEL_INDEX} onPress={this.handleActionPress} diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js index d71816c40..2463984ec 100644 --- a/app/containers/MessageBox/ReplyPreview.js +++ b/app/containers/MessageBox/ReplyPreview.js @@ -9,7 +9,6 @@ import Markdown from '../message/Markdown'; const styles = StyleSheet.create({ container: { - flex: 1, flexDirection: 'row' }, messageContainer: { @@ -35,11 +34,6 @@ const styles = StyleSheet.create({ lineHeight: 16, marginLeft: 5 }, - content: { - color: '#0C0D0F', - fontSize: 16, - lineHeight: 20 - }, close: { marginRight: 15 } diff --git a/app/containers/MessageErrorActions.js b/app/containers/MessageErrorActions.js index 3f89bf86d..cb68946ef 100644 --- a/app/containers/MessageErrorActions.js +++ b/app/containers/MessageErrorActions.js @@ -30,7 +30,9 @@ export default class MessageErrorActions extends React.Component { this.CANCEL_INDEX = 0; this.DELETE_INDEX = 1; this.RESEND_INDEX = 2; - this.ActionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } } handleResend = protectedFunction(() => RocketChat.resendMessage(this.props.actionMessage._id)); @@ -59,7 +61,7 @@ export default class MessageErrorActions extends React.Component { render() { return ( this.ActionSheet = o} + ref={o => this.actionSheet = o} title={I18n.t('Message_actions')} options={this.options} cancelButtonIndex={this.CANCEL_INDEX} diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index d8e4895ab..6a79a16d2 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -1,14 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, AsyncStorage, SafeAreaView } from 'react-native'; +import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import FastImage from 'react-native-fast-image'; import Icon from 'react-native-vector-icons/MaterialIcons'; import database from '../lib/realm'; -import { selectServer } from '../actions/server'; +import { selectServerRequest } from '../actions/server'; import { logout } from '../actions/login'; -import { appStart } from '../actions'; import Avatar from '../containers/Avatar'; import Status from '../containers/status'; import Touch from '../utils/touch'; @@ -19,8 +18,9 @@ import I18n from '../i18n'; import { NavigationActions } from '../Navigation'; const styles = StyleSheet.create({ - selected: { - backgroundColor: 'rgba(0, 0, 0, .04)' + container: { + flex: 1, + backgroundColor: '#fff' }, item: { flexDirection: 'row', @@ -31,9 +31,6 @@ const styles = StyleSheet.create({ width: 30, alignItems: 'center' }, - itemLeftOpacity: { - opacity: 0.62 - }, itemText: { marginVertical: 16, fontWeight: 'bold', @@ -88,18 +85,16 @@ const keyExtractor = item => item.id; username: state.login.user && state.login.user.username } }), dispatch => ({ - selectServer: server => dispatch(selectServer(server)), - logout: () => dispatch(logout()), - appStart: () => dispatch(appStart('outside')) + selectServerRequest: server => dispatch(selectServerRequest(server)), + logout: () => dispatch(logout()) })) export default class Sidebar extends Component { static propTypes = { navigator: PropTypes.object, server: PropTypes.string.isRequired, - selectServer: PropTypes.func.isRequired, + selectServerRequest: PropTypes.func.isRequired, user: PropTypes.object, - logout: PropTypes.func.isRequired, - appStart: PropTypes.func + logout: PropTypes.func.isRequired } constructor(props) { @@ -127,7 +122,7 @@ export default class Sidebar extends Component { } onPressItem = (item) => { - this.props.selectServer(item.id); + this.props.selectServerRequest(item.id); } setStatus = () => { @@ -230,11 +225,7 @@ export default class Sidebar extends Component { this.closeDrawer(); this.toggleServers(); if (this.props.server !== item.id) { - this.props.selectServer(item.id); - const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ item.id }`); - if (!token) { - this.props.appStart(); - } + this.props.selectServerRequest(item.id); } }, testID: `sidebar-${ item.id }` @@ -314,8 +305,8 @@ export default class Sidebar extends Component { return null; } return ( - - + + this.toggleServers()} underlayColor='rgba(255, 255, 255, 0.5)' @@ -331,9 +322,9 @@ export default class Sidebar extends Component { - {user.username} + {user.username} - {server} + {server} ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' @@ -55,7 +42,7 @@ export default class extends React.PureComponent { this._onPressButton()} - style={styles.button} + style={styles.imageContainer} > {}, + image: node => ( + // TODO: should use Image component + + ), ...rules }} style={{ diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js index 1b42c73c3..fd7654329 100644 --- a/app/containers/message/Reply.js +++ b/app/containers/message/Reply.js @@ -17,13 +17,6 @@ const styles = StyleSheet.create({ marginTop: 2, alignSelf: 'flex-end' }, - quoteSign: { - borderWidth: 2, - borderRadius: 4, - borderColor: '#a0a0a0', - height: '100%', - marginRight: 5 - }, attachmentContainer: { flex: 1, flexDirection: 'column' diff --git a/app/containers/message/Url.js b/app/containers/message/Url.js index 5ec48a999..43dd2542e 100644 --- a/app/containers/message/Url.js +++ b/app/containers/message/Url.js @@ -12,13 +12,6 @@ const styles = StyleSheet.create({ alignItems: 'center', marginVertical: 2 }, - quoteSign: { - borderWidth: 2, - borderRadius: 4, - borderColor: '#a0a0a0', - height: '100%', - marginRight: 5 - }, image: { height: 80, width: 80, diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 538114eef..7b8c1b8ac 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -107,5 +107,19 @@ export default StyleSheet.create({ flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'flex-start' + }, + imageContainer: { + flex: 1, + flexDirection: 'column' + }, + image: { + width: '100%', + maxWidth: 400, + height: 300 + }, + inlineImage: { + width: 300, + height: 300, + resizeMode: 'contain' } }); diff --git a/app/lib/ddp.js b/app/lib/ddp.js index 74e9fff27..c4c8ce108 100644 --- a/app/lib/ddp.js +++ b/app/lib/ddp.js @@ -73,6 +73,8 @@ export default class Socket extends EventEmitter { this.subscriptions = {}; this.ddp = new EventEmitter(); this._logged = false; + this.forceDisconnect = false; + this.connected = false; const waitTimeout = () => setTimeout(() => { // this.connection.ping(); this.send({ msg: 'ping' }).catch(e => log('ping', e)); @@ -164,8 +166,11 @@ export default class Socket extends EventEmitter { } } async send(obj, ignore) { - console.log('send'); + console.log('send', obj); return new Promise((resolve, reject) => { + if (!this.connected) { + return reject(); + } this.id += 1; const id = obj.id || `ddp-react-native-${ this.id }`; // console.log('send', { ...obj, id }); @@ -209,15 +214,19 @@ export default class Socket extends EventEmitter { this.connection = new WebSocket(`${ this.url }/websocket`, null); this.connection.onopen = async() => { + this.connected = true; + this.forceDisconnect = false; this.emit('open'); resolve(); this.ddp.emit('open'); + console.log(`Connected to: ${ this.url }`); if (this._login) { return this.login(this._login).catch(e => console.warn(e)); } }; this.connection.onclose = debounce((e) => { this.emit('disconnected', e); + this.connected = false; }, 300); this.connection.onmessage = (e) => { try { @@ -238,13 +247,17 @@ export default class Socket extends EventEmitter { .finally(() => this.subscriptions = {}); } disconnect() { - this._close(); this._logged = false; this._login = null; this.subscriptions = {}; + this.forceDisconnect = true; + this._close(); + if (this.timeout) { + clearTimeout(this.timeout); + } } async reconnect() { - if (this._timer) { + if (this._timer || this.forceDisconnect) { return; } this._close(); diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js index 48fea4e4c..a82aae586 100644 --- a/app/lib/methods/getCustomEmojis.js +++ b/app/lib/methods/getCustomEmojis.js @@ -15,6 +15,9 @@ const getLastMessage = () => { export default async function() { try { + if (!this.ddp.status) { + return; + } const lastMessage = getLastMessage(); let emojis = await this.ddp.call('listEmojiCustom'); emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage); diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js index 372074042..390a8e5a9 100644 --- a/app/lib/methods/helpers/normalizeMessage.js +++ b/app/lib/methods/helpers/normalizeMessage.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + import parseUrls from './parseUrls'; function normalizeAttachments(msg) { @@ -6,6 +8,9 @@ function normalizeAttachments(msg) { } msg.attachments = msg.attachments.map((att) => { att.fields = att.fields || []; + if (att.ts) { + att.ts = moment(att.ts).toDate(); + } att = normalizeAttachments(att); return att; }); diff --git a/app/lib/realm.js b/app/lib/realm.js index 94583fee0..f4677b030 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -299,44 +299,6 @@ const schema = [ uploadsSchema ]; -// class DebouncedDb { -// constructor(db) { -// this.database = db; -// } -// deleteAll(...args) { -// return this.database.write(() => this.database.deleteAll(...args)); -// } -// delete(...args) { -// return this.database.delete(...args); -// } -// write(fn) { -// return fn(); -// } -// create(...args) { -// this.queue = this.queue || []; -// if (this.timer) { -// clearTimeout(this.timer); -// this.timer = null; -// } -// this.timer = setTimeout(() => { -// alert(this.queue.length); -// this.database.write(() => { -// this.queue.forEach(({ db, args }) => this.database.create(...args)); -// }); -// -// this.timer = null; -// return this.roles = []; -// }, 1000); -// -// this.queue.push({ -// db: this.database, -// args -// }); -// } -// objects(...args) { -// return this.database.objects(...args); -// } -// } class DB { databases = { serversDB: new Realm({ @@ -376,9 +338,3 @@ class DB { } } export default new DB(); - -// realm.write(() => { -// realm.create('servers', { id: 'https://open.rocket.chat', current: false }, true); -// realm.create('servers', { id: 'http://localhost:3000', current: false }, true); -// realm.create('servers', { id: 'http://10.0.2.2:3000', current: false }, true); -// }); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 1d8eb7db9..fa792cfa6 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -85,16 +85,14 @@ const RocketChat = { return (headers['x-instance-id'] != null && headers['x-instance-id'].length > 0) || (headers['X-Instance-ID'] != null && headers['X-Instance-ID'].length > 0); }, async testServer(url) { - if (/^(https?:\/\/)?(((\w|[0-9-_])+(\.(\w|[0-9-_])+)+)|localhost)(:\d+)?$/.test(url)) { - try { - let response = await RNFetchBlob.fetch('HEAD', url); - response = response.respInfo; - if (response.status === 200 && RocketChat._hasInstanceId(response.headers)) { - return url; - } - } catch (e) { - log('testServer', e); + try { + let response = await RNFetchBlob.fetch('HEAD', url); + response = response.respInfo; + if (response.status === 200 && RocketChat._hasInstanceId(response.headers)) { + return url; } + } catch (e) { + log('testServer', e); } throw new Error({ error: 'invalid server' }); }, @@ -141,6 +139,7 @@ const RocketChat = { const userInfo = await this.userInfo({ token: user.token, userId: user.id }); user = { ...user, ...userInfo.user }; } + RocketChat.registerPushToken(user.id); return reduxStore.dispatch(loginSuccess(user)); } catch (e) { log('rocketchat.loginSuccess', e); @@ -154,9 +153,9 @@ const RocketChat = { } this.ddp = new Ddp(url, login); - if (login) { - protectedFunction(() => RocketChat.getRooms()); - } + // if (login) { + // protectedFunction(() => RocketChat.getRooms()); + // } this.ddp.on('login', protectedFunction(() => reduxStore.dispatch(loginRequest()))); @@ -198,84 +197,6 @@ const RocketChat = { return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); })); - // this.ddp.on('stream-notify-logged', (ddpMessage) => { - // // this entire logic needs a better solution - // // we're using it only because our image cache lib doesn't support clear cache - // if (ddpMessage.fields && ddpMessage.fields.eventName === 'updateAvatar') { - // const { args } = ddpMessage.fields; - // InteractionManager.runAfterInteractions(() => - // args.forEach((arg) => { - // const user = database.objects('users').filtered('username = $0', arg.username); - // if (user.length > 0) { - // database.write(() => { - // user[0].avatarVersion += 1; - // }); - // } - // })); - // } - // }); - - // this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => { - // console.warn('rc.stream-notify-user') - // const [type, data] = ddpMessage.fields.args; - // const [, ev] = ddpMessage.fields.eventName.split('/'); - // if (/subscriptions/.test(ev)) { - // if (data.roles) { - // data.roles = data.roles.map(role => ({ value: role })); - // } - // if (data.blocker) { - // data.blocked = true; - // } else { - // data.blocked = false; - // } - // if (data.mobilePushNotifications === 'nothing') { - // data.notifications = true; - // } else { - // data.notifications = false; - // } - // database.write(() => { - // database.create('subscriptions', data, true); - // }); - // } - // if (/rooms/.test(ev) && type === 'updated') { - // const sub = database.objects('subscriptions').filtered('rid == $0', data._id)[0]; - - // database.write(() => { - // sub.roomUpdatedAt = data._updatedAt; - // sub.lastMessage = normalizeMessage(data.lastMessage); - // sub.ro = data.ro; - // sub.description = data.description; - // sub.topic = data.topic; - // sub.announcement = data.announcement; - // sub.reactWhenReadOnly = data.reactWhenReadOnly; - // sub.archived = data.archived; - // sub.joinCodeRequired = data.joinCodeRequired; - // if (data.muted) { - // sub.muted = data.muted.map(m => ({ value: m })); - // } - // }); - // } - // if (/message/.test(ev)) { - // const [args] = ddpMessage.fields.args; - // const _id = Random.id(); - // const message = { - // _id, - // rid: args.rid, - // msg: args.msg, - // ts: new Date(), - // _updatedAt: new Date(), - // status: messagesStatus.SENT, - // u: { - // _id, - // username: 'rocket.cat' - // } - // }; - // requestAnimationFrame(() => database.write(() => { - // database.create('messages', message, true); - // })); - // } - // })); - this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.starredMessages = this.starredMessages || []; diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index 862093b99..df5ef0131 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -47,16 +47,6 @@ const styles = StyleSheet.create({ color: '#444', marginRight: 8 }, - lastMessage: { - flex: 1, - flexShrink: 1, - marginRight: 8, - maxHeight: 20, - overflow: 'hidden', - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'flex-start' - }, alert: { fontWeight: 'bold' }, @@ -268,6 +258,11 @@ export default class RoomItem extends React.Component { #{node.content} + ), + link: (node, children) => ( + + {children} + ) }} /> diff --git a/app/reducers/server.js b/app/reducers/server.js index c68e95dbc..6b9d8c29e 100644 --- a/app/reducers/server.js +++ b/app/reducers/server.js @@ -6,7 +6,8 @@ const initialState = { errorMessage: '', failure: false, server: '', - adding: false + adding: false, + loading: true }; @@ -38,11 +39,18 @@ export default function server(state = initialState, action) { ...state, adding: true }; - case SERVER.SELECT: + case SERVER.SELECT_REQUEST: return { ...state, server: action.server, - adding: false + loading: true + }; + case SERVER.SELECT_SUCCESS: + return { + ...state, + server: action.server, + adding: false, + loading: false }; default: return state; diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index e50fa23f5..11bbbd35b 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -3,7 +3,7 @@ import { takeLatest, take, select, put } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions'; -import { selectServer, addServer } from '../actions/server'; +import { selectServerRequest, addServer } from '../actions/server'; import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; import { NavigationActions } from '../Navigation'; @@ -68,7 +68,7 @@ const handleOpen = function* handleOpen({ params }) { if (!token) { yield put(appStart('outside')); } else { - yield put(selectServer(deepLinkServer)); + yield put(selectServerRequest(deepLinkServer)); yield take(types.METEOR.REQUEST); yield navigate({ params, sameServer: false }); } diff --git a/app/sagas/init.js b/app/sagas/init.js index 1f419d2f5..5eabde10c 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -2,7 +2,7 @@ import { AsyncStorage } from 'react-native'; import { call, put, takeLatest } from 'redux-saga/effects'; import * as actions from '../actions'; -import { selectServer } from '../actions/server'; +import { selectServerRequest } from '../actions/server'; import { restoreToken, setUser } from '../actions/login'; import { APP } from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; @@ -19,10 +19,7 @@ const restore = function* restore() { const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); if (currentServer) { - yield put(selectServer(currentServer)); - if (token) { - yield put(actions.appStart('inside')); - } + yield put(selectServerRequest(currentServer)); const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); if (login) { diff --git a/app/sagas/login.js b/app/sagas/login.js index 1e584d6c7..25ce45c52 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -17,13 +17,14 @@ import { setUsernameRequest, setUsernameSuccess, forgotPasswordSuccess, - forgotPasswordFailure + forgotPasswordFailure, + setUser } from '../actions/login'; import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; import I18n from '../i18n'; -const getUser = state => state.login; +const getUser = state => state.login.user; const getServer = state => state.server.server; const getIsConnected = state => state.meteor.connected; @@ -36,15 +37,10 @@ const forgotPasswordCall = args => RocketChat.forgotPassword(args); const handleLoginSuccess = function* handleLoginSuccess() { try { - const [server, user] = yield all([select(getServer), select(getUser)]); + const user = yield select(getUser); yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); - yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); - // const token = yield AsyncStorage.getItem('pushId'); - // if (token) { - // yield RocketChat.registerPushToken(user.user.id, token); - // } - yield RocketChat.registerPushToken(user.user.id); - if (!user.user.username || user.isRegistering) { + yield put(setUser(user)); + if (!user.username || user.isRegistering) { yield put(registerIncomplete()); } else { yield delay(300); @@ -127,17 +123,22 @@ const watchLoginOpen = function* watchLoginOpen() { } const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); yield take(types.LOGIN.CLOSE); - yield sub.unsubscribe().catch(err => console.warn(err)); + if (sub) { + yield sub.unsubscribe().catch(err => console.warn(err)); + } } catch (e) { log('watchLoginOpen', e); } }; -// eslint-disable-next-line require-yield -const handleSetUser = function* handleSetUser(params) { +const handleSetUser = function* handleSetUser() { const [server, user] = yield all([select(getServer), select(getUser)]); - if (params.language) { - I18n.locale = params.language; + if (user) { + // TODO: temporary... remove in future releases + delete user.user; + if (user.language) { + I18n.locale = user.language; + } } yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); }; diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 1de9f453d..ae30f4e9e 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -6,7 +6,7 @@ import { NavigationActions } from '../Navigation'; import { SERVER } from '../actions/actionsTypes'; import * as actions from '../actions'; import { connectRequest } from '../actions/connect'; -import { serverSuccess, serverFailure, selectServer } from '../actions/server'; +import { serverSuccess, serverFailure, selectServerRequest, selectServerSuccess } from '../actions/server'; import { setRoles } from '../actions/roles'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; @@ -20,11 +20,14 @@ const validate = function* validate(server) { const handleSelectServer = function* handleSelectServer({ server }) { try { yield database.setActiveDB(server); - - // yield RocketChat.disconnect(); - yield call([AsyncStorage, 'setItem'], 'currentServer', server); - // yield AsyncStorage.removeItem(RocketChat.TOKEN_KEY); + const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); + if (token) { + yield put(actions.appStart('inside')); + } else { + yield put(actions.appStart('outside')); + } + const settings = database.objects('settings'); yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); const emojis = database.objects('customEmojis'); @@ -36,6 +39,7 @@ const handleSelectServer = function* handleSelectServer({ server }) { }, {}))); yield put(connectRequest()); + yield put(selectServerSuccess(server)); } catch (e) { log('handleSelectServer', e); } @@ -59,7 +63,7 @@ const addServer = function* addServer({ server }) { database.databases.serversDB.write(() => { database.databases.serversDB.create('servers', { id: server, current: false }, true); }); - yield put(selectServer(server)); + yield put(selectServerRequest(server)); } catch (e) { log('addServer', e); } @@ -67,7 +71,7 @@ const addServer = function* addServer({ server }) { const root = function* root() { yield takeLatest(SERVER.REQUEST, validateServer); - yield takeLatest(SERVER.SELECT, handleSelectServer); + yield takeLatest(SERVER.SELECT_REQUEST, handleSelectServer); yield takeLatest(SERVER.ADD, addServer); }; export default root; diff --git a/app/sagas/state.js b/app/sagas/state.js index f995ff9e0..fbfb1f9de 100644 --- a/app/sagas/state.js +++ b/app/sagas/state.js @@ -5,6 +5,10 @@ import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; const appHasComeBackToForeground = function* appHasComeBackToForeground() { + const appRoot = yield select(state => state.app.root); + if (appRoot === 'outside') { + return; + } const auth = yield select(state => state.login.isAuthenticated); if (!auth) { return; @@ -17,6 +21,10 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() { }; const appHasComeBackToBackground = function* appHasComeBackToBackground() { + const appRoot = yield select(state => state.app.root); + if (appRoot === 'outside') { + return; + } const auth = yield select(state => state.login.isAuthenticated); if (!auth) { return; diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index addda0446..9f9a62cab 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -38,6 +38,12 @@ export default class CreateChannelView extends LoggedView { }; } + componentDidMount() { + setTimeout(() => { + this.channelNameRef.focus(); + }, 600); + } + submit = () => { if (!this.state.channelName.trim() || this.props.createChannel.isFetching) { return; @@ -138,12 +144,12 @@ export default class CreateChannelView extends LoggedView { this.channelNameRef = ref} label={I18n.t('Channel_Name')} value={this.state.channelName} onChangeText={channelName => this.setState({ channelName })} placeholder={I18n.t('Type_the_channel_name_here')} returnKeyType='done' - autoFocus testID='create-channel-name' /> {this.renderChannelNameError()} diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index f17e325c8..405ac3947 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -77,8 +77,8 @@ export default class ForgotPasswordView extends LoggedView { keyboardVerticalOffset={128} > - - + + ({ - selectServer: server => dispatch(selectServer(server)) + selectServerRequest: server => dispatch(selectServerRequest(server)) })) /** @extends React.Component */ export default class ListServerView extends LoggedView { static propTypes = { navigator: PropTypes.object, login: PropTypes.object.isRequired, - selectServer: PropTypes.func.isRequired, + selectServerRequest: PropTypes.func.isRequired, server: PropTypes.string } constructor(props) { super('ListServerView', props); + this.focused = true; this.state = { sections: [] }; @@ -102,8 +84,19 @@ export default class ListServerView extends LoggedView { this.jumpToSelectedServer(); } + componentWillReceiveProps(nextProps) { + if (this.props.server !== nextProps.server && nextProps.server && !this.props.login.isRegistering) { + this.timeout = setTimeout(() => { + this.openLogin(nextProps.server); + }, 1000); + } + } + componentWillUnmount() { this.data.removeAllListeners(); + if (this.timeout) { + clearTimeout(this.timeout); + } } onNavigatorEvent(event) { @@ -114,6 +107,8 @@ export default class ListServerView extends LoggedView { title: I18n.t('New_Server') }); } + } else if (event.type === 'ScreenChangedEvent') { + this.focused = event.id === 'didAppear' || event.id === 'onActivityResumed'; } } @@ -134,14 +129,16 @@ export default class ListServerView extends LoggedView { }; openLogin = (server) => { - this.props.navigator.push({ - screen: 'LoginSignupView', - title: server - }); + if (this.focused) { + this.props.navigator.push({ + screen: 'LoginSignupView', + title: server + }); + } } selectAndNavigateTo = (server) => { - this.props.selectServer(server); + this.props.selectServerRequest(server); this.openLogin(server); } @@ -193,7 +190,7 @@ export default class ListServerView extends LoggedView { render() { return ( - + item.id} ItemSeparatorComponent={this.renderSeparator} /> - + ); } } diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index df9869a73..2ef0ba760 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet } from 'react-native'; +import { Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; @@ -279,7 +279,7 @@ export default class LoginSignupView extends LoggedView { style={[sharedStyles.container, sharedStyles.containerScrollView]} {...scrollPersistTaps} > - + - + ); } diff --git a/app/views/LoginView.js b/app/views/LoginView.js index bd73fb934..0050d5592 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Keyboard, Text, ScrollView, View } from 'react-native'; +import { Keyboard, Text, ScrollView, View, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import { Answers } from 'react-native-fabric'; @@ -106,7 +106,7 @@ export default class LoginView extends LoggedView { key='login-view' > - + Login {this.props.reason} : null} - + ); diff --git a/app/views/MentionedMessagesView/index.js b/app/views/MentionedMessagesView/index.js index daae8b3c8..4bdf5e8ef 100644 --- a/app/views/MentionedMessagesView/index.js +++ b/app/views/MentionedMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, Text } from 'react-native'; +import { FlatList, View, Text, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import LoggedView from '../View'; @@ -102,10 +102,8 @@ export default class MentionedMessagesView extends LoggedView { } return ( - [ + : null} ListFooterComponent={loadingMore ? : null} /> - ] + ); } } diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index d6abe2b3a..6cab56f16 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, ScrollView, View, Keyboard } from 'react-native'; +import { Text, ScrollView, View, Keyboard, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import { serverRequest, addServer } from '../actions/server'; @@ -40,6 +40,12 @@ export default class NewServerView extends LoggedView { props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field } + componentDidMount() { + setTimeout(() => { + this.input.focus(); + }, 600); + } + onChangeText = (text) => { this.setState({ text }); this.props.validateServer(this.completeUrl(text)); @@ -106,7 +112,7 @@ export default class NewServerView extends LoggedView { keyboardVerticalOffset={128} > - + {I18n.t('Sign_in_your_server')} this.input = e} @@ -129,7 +135,7 @@ export default class NewServerView extends LoggedView { /> - + ); diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js index f18c9f6d7..649ce9b05 100644 --- a/app/views/PinnedMessagesView/index.js +++ b/app/views/PinnedMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, Text } from 'react-native'; +import { FlatList, View, Text, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import ActionSheet from 'react-native-actionsheet'; @@ -69,7 +69,9 @@ export default class PinnedMessagesView extends LoggedView { onLongPress = (message) => { this.setState({ message }); - this.actionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } } handleActionPress = (actionIndex) => { @@ -126,10 +128,8 @@ export default class PinnedMessagesView extends LoggedView { } return ( - [ + : null} ListFooterComponent={loadingMore ? : null} - />, + /> this.actionSheet = o} title={I18n.t('Actions')} options={options} cancelButtonIndex={CANCEL_INDEX} onPress={this.handleActionPress} /> - ] + ); } } diff --git a/app/views/PrivacyPolicyView.js b/app/views/PrivacyPolicyView.js index 2d815155c..aea3ef372 100644 --- a/app/views/PrivacyPolicyView.js +++ b/app/views/PrivacyPolicyView.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { WebView } from 'react-native'; +import { WebView, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; +import styles from './Styles'; + @connect(state => ({ privacyPolicy: state.settings.Layout_Privacy_Policy })) @@ -13,7 +15,9 @@ export default class PrivacyPolicyView extends React.PureComponent { render() { return ( - + + + ); } } diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 70dfafcee..3c5fb4ca2 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -378,7 +378,7 @@ export default class ProfileView extends LoggedView { testID='profile-view-list' {...scrollPersistTaps} > - + - + {I18n.t('Sign_Up')} {this._renderRegister()} {this._renderUsername()} @@ -223,7 +223,7 @@ export default class RegisterView extends LoggedView { : null } - + ); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index d48738c46..b0c22fe96 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, SectionList, Text, Alert } from 'react-native'; +import { View, SectionList, Text, Alert, SafeAreaView } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import { connect } from 'react-redux'; @@ -394,7 +394,7 @@ export default class RoomActionsView extends LoggedView { render() { return ( - + item.name} testID='room-actions-list' /> - + ); } } diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js index 1d4717f89..50182bee8 100644 --- a/app/views/RoomActionsView/styles.js +++ b/app/views/RoomActionsView/styles.js @@ -2,6 +2,7 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ container: { + flex: 1, backgroundColor: '#F6F7F9' }, sectionItem: { diff --git a/app/views/RoomFilesView/index.js b/app/views/RoomFilesView/index.js index 87d5e038b..cc77e95bc 100644 --- a/app/views/RoomFilesView/index.js +++ b/app/views/RoomFilesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, Text } from 'react-native'; +import { FlatList, View, Text, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import LoggedView from '../View'; @@ -98,10 +98,8 @@ export default class RoomFilesView extends LoggedView { const { loading, loadingMore } = this.state; return ( - [ + : null} ListFooterComponent={loadingMore ? : null} /> - ] + ); } } diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index eb74986a0..57b87eeb5 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -34,8 +34,11 @@ const PERMISSIONS_ARRAY = [ PERMISSION_DELETE_P ]; +@connect(null, dispatch => ({ + eraseRoom: rid => dispatch(eraseRoom(rid)) +})) /** @extends React.Component */ -class RoomInfoEditView extends LoggedView { +export default class RoomInfoEditView extends LoggedView { static propTypes = { rid: PropTypes.string, eraseRoom: PropTypes.func @@ -263,7 +266,7 @@ class RoomInfoEditView extends LoggedView { testID='room-info-edit-view-list' {...scrollPersistTaps} > - + { this.name = e; }} label={I18n.t('Name')} @@ -398,9 +401,3 @@ class RoomInfoEditView extends LoggedView { ); } } - -const mapDispatchToProps = dispatch => ({ - eraseRoom: rid => dispatch(eraseRoom(rid)) -}); - -export default connect(null, mapDispatchToProps)(RoomInfoEditView); diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index ea8f54bc0..11a72da63 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, Text, ScrollView } from 'react-native'; +import { View, Text, ScrollView, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import moment from 'moment'; @@ -211,17 +211,19 @@ export default class RoomInfoView extends LoggedView { return ; } return ( - - - {this.renderAvatar(room, roomUser)} - { getRoomTitle(room) } - - {!this.isDirect() ? this.renderItem('description', room) : null} - {!this.isDirect() ? this.renderItem('topic', room) : null} - {!this.isDirect() ? this.renderItem('announcement', room) : null} - {this.isDirect() ? this.renderRoles() : null} - {this.isDirect() ? this.renderTimezone(roomUser._id) : null} - {room.broadcast ? this.renderBroadcast() : null} + + + + {this.renderAvatar(room, roomUser)} + { getRoomTitle(room) } + + {!this.isDirect() ? this.renderItem('description', room) : null} + {!this.isDirect() ? this.renderItem('topic', room) : null} + {!this.isDirect() ? this.renderItem('announcement', room) : null} + {this.isDirect() ? this.renderRoles() : null} + {this.isDirect() ? this.renderTimezone(roomUser._id) : null} + {room.broadcast ? this.renderBroadcast() : null} + ); } diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js index 70c44dc17..6e0c81dd7 100644 --- a/app/views/RoomInfoView/styles.js +++ b/app/views/RoomInfoView/styles.js @@ -2,6 +2,10 @@ import { StyleSheet } from 'react-native'; export default StyleSheet.create({ container: { + flex: 1, + backgroundColor: '#ffffff' + }, + scroll: { flex: 1, flexDirection: 'column', backgroundColor: '#ffffff', diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index e67e62e7b..5c6ce2f6b 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, TextInput, Vibration } from 'react-native'; +import { FlatList, View, TextInput, Vibration, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import ActionSheet from 'react-native-actionsheet'; @@ -122,7 +122,9 @@ export default class RoomMembersView extends LoggedView { } this.setState({ userLongPressed: user }); Vibration.vibrate(50); - this.ActionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } } updateRoom = async() => { @@ -202,10 +204,8 @@ export default class RoomMembersView extends LoggedView { render() { const { filtering, members, membersFiltered } = this.state; return ( - [ + , + /> this.ActionSheet = o} + ref={o => this.actionSheet = o} title={I18n.t('Actions')} options={this.actionSheetOptions} cancelButtonIndex={this.CANCEL_INDEX} onPress={this.handleActionPress} /> - ] + ); } } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 2663229ba..362d23bff 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, LayoutAnimation, ActivityIndicator } from 'react-native'; +import { Text, View, LayoutAnimation, ActivityIndicator, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import equal from 'deep-equal'; @@ -295,7 +295,7 @@ export default class RoomView extends LoggedView { render() { return ( - + {this.renderList()} {this.renderFooter()} {this.state.room._id && this.props.showActions ? @@ -304,7 +304,7 @@ export default class RoomView extends LoggedView { {this.props.showErrorActions ? : null} - + ); } } diff --git a/app/views/RoomsListView/Search/index.js b/app/views/RoomsListView/Search/index.js index c8e551b03..cd69be4d8 100644 --- a/app/views/RoomsListView/Search/index.js +++ b/app/views/RoomsListView/Search/index.js @@ -7,7 +7,10 @@ import { setSearch } from '../../../actions/rooms'; import styles from './styles'; import I18n from '../../../i18n'; -class RoomsListSearchView extends React.Component { +@connect(null, dispatch => ({ + setSearch: searchText => dispatch(setSearch(searchText)) +})) +export default class RoomsListSearchView extends React.Component { static propTypes = { setSearch: PropTypes.func } @@ -39,9 +42,3 @@ class RoomsListSearchView extends React.Component { ); } } - -const mapDispatchToProps = dispatch => ({ - setSearch: searchText => dispatch(setSearch(searchText)) -}); - -export default connect(null, mapDispatchToProps)(RoomsListSearchView); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 9d0e8bef5..de7daa4d4 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Platform, View, TextInput, FlatList, BackHandler } from 'react-native'; +import { Platform, View, TextInput, FlatList, BackHandler, ActivityIndicator, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import { iconsMap } from '../../Icons'; @@ -13,11 +13,14 @@ import LoggedView from '../View'; import log from '../../utils/log'; import I18n from '../../i18n'; +const ROW_HEIGHT = 70.5; + @connect(state => ({ userId: state.login.user && state.login.user.id, server: state.server.server, Site_Url: state.settings.Site_Url, - searchText: state.rooms.searchText + searchText: state.rooms.searchText, + loadingServer: state.server.loading })) /** @extends React.Component */ export default class RoomsListView extends LoggedView { @@ -26,7 +29,8 @@ export default class RoomsListView extends LoggedView { userId: PropTypes.string, Site_Url: PropTypes.string, server: PropTypes.string, - searchText: PropTypes.string + searchText: PropTypes.string, + loadingServer: PropTypes.bool } constructor(props) { @@ -34,7 +38,8 @@ export default class RoomsListView extends LoggedView { this.state = { search: [], - rooms: [] + rooms: [], + loading: true }; props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); } @@ -48,7 +53,9 @@ export default class RoomsListView extends LoggedView { } componentWillReceiveProps(props) { - if (this.props.server !== props.server && props.server) { + if (props.server && props.loadingServer) { + this.setState({ loading: true }); + } else if (props.server && !props.loadingServer) { this.getSubscriptions(); } else if (this.props.searchText !== props.searchText) { this.search(props.searchText); @@ -60,6 +67,9 @@ export default class RoomsListView extends LoggedView { if (this.data) { this.data.removeAllListeners(); } + if (this.timeout) { + clearTimeout(this.timeout); + } } onNavigatorEvent(event) { @@ -104,6 +114,9 @@ export default class RoomsListView extends LoggedView { this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('roomUpdatedAt', true); this.data.addListener(this.updateState); } + this.timeout = setTimeout(() => { + this.setState({ loading: false }); + }, 200); } initDefaultHeader = () => { @@ -285,24 +298,31 @@ export default class RoomsListView extends LoggedView { />); } - renderList = () => ( - 0 ? this.state.search : this.state.rooms} - extraData={this.state.search.length > 0 ? this.state.search : this.state.rooms} - keyExtractor={item => item.rid} - style={styles.list} - renderItem={this.renderItem} - ListHeaderComponent={Platform.OS === 'ios' ? this.renderSearchBar : null} - contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}} - enableEmptySections - removeClippedSubviews - keyboardShouldPersistTaps='always' - testID='rooms-list-view-list' - /> - ) + renderList = () => { + if (this.state.loading) { + return ; + } + return ( + 0 ? this.state.search : this.state.rooms} + extraData={this.state.search.length > 0 ? this.state.search : this.state.rooms} + keyExtractor={item => item.rid} + style={styles.list} + renderItem={this.renderItem} + ListHeaderComponent={Platform.OS === 'ios' ? this.renderSearchBar : null} + contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}} + getItemLayout={(data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index })} + enableEmptySections + removeClippedSubviews + keyboardShouldPersistTaps='always' + testID='rooms-list-view-list' + /> + ); + } render = () => ( - + {this.renderList()} - ) + + ) } diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 07d4d6411..9f7d8dfd6 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -37,5 +37,8 @@ export default StyleSheet.create({ padding: 5, paddingLeft: 10, color: '#aaa' + }, + loading: { + flex: 1 } }); diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index eb13b6872..a56c58aa0 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, FlatList } from 'react-native'; +import { View, FlatList, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import LoggedView from '../View'; @@ -115,10 +115,7 @@ export default class SearchMessagesView extends LoggedView { render() { const { searching, loadingMore } = this.state; return ( - + { this.name = e; }} @@ -140,7 +137,7 @@ export default class SearchMessagesView extends LoggedView { ListFooterComponent={loadingMore ? : null} {...scrollPersistTaps} /> - + ); } } diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index 6bf63d874..4aca98c56 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -297,11 +297,9 @@ export default class SelectedUsersView extends LoggedView { /> ); render = () => ( - - - {this.renderList()} - - - + + {this.renderList()} + + ); } diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index f647c036f..e0afa28b4 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -76,6 +76,15 @@ export default class SettingsView extends LoggedView { } } + getLabel = (language) => { + const { languages } = this.state; + const l = languages.find(i => i.value === language); + if (l && l.label) { + return l.label; + } + return null; + } + formIsChanged = () => { const { language } = this.state; return !(this.props.userLanguage === language); @@ -107,6 +116,10 @@ export default class SettingsView extends LoggedView { this.setState({ saving: false }); setTimeout(() => { showToast(I18n.t('Preferences_saved')); + + if (params.language) { + this.props.navigator.setTitle({ title: I18n.t('Settings') }); + } }, 300); } catch (e) { this.setState({ saving: false }); @@ -132,7 +145,7 @@ export default class SettingsView extends LoggedView { testID='settings-view-list' {...scrollPersistTaps} > - + { @@ -145,7 +158,7 @@ export default class SettingsView extends LoggedView { inputRef={(e) => { this.name = e; }} label={I18n.t('Language')} placeholder={I18n.t('Language')} - value={languages.find(i => i.value === language).label} + value={this.getLabel(language)} testID='settings-view-language' /> diff --git a/app/views/SnippetedMessagesView/index.js b/app/views/SnippetedMessagesView/index.js index 7a5f4936c..7c4c897e2 100644 --- a/app/views/SnippetedMessagesView/index.js +++ b/app/views/SnippetedMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, Text } from 'react-native'; +import { FlatList, View, Text, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import LoggedView from '../View'; @@ -102,10 +102,8 @@ export default class SnippetedMessagesView extends LoggedView { } return ( - [ + : null} ListFooterComponent={loadingMore ? : null} /> - ] + ); } } diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js index a40bed71f..4e2c476b4 100644 --- a/app/views/StarredMessagesView/index.js +++ b/app/views/StarredMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, Text } from 'react-native'; +import { FlatList, View, Text, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; import ActionSheet from 'react-native-actionsheet'; @@ -69,7 +69,9 @@ export default class StarredMessagesView extends LoggedView { onLongPress = (message) => { this.setState({ message }); - this.actionSheet.show(); + if (this.actionSheet && this.actionSheet.show) { + this.actionSheet.show(); + } } handleActionPress = (actionIndex) => { @@ -126,10 +128,8 @@ export default class StarredMessagesView extends LoggedView { } return ( - [ + : null} ListFooterComponent={loadingMore ? : null} - />, + /> this.actionSheet = o} title={I18n.t('Actions')} options={options} cancelButtonIndex={CANCEL_INDEX} onPress={this.handleActionPress} /> - ] + ); } } diff --git a/app/views/TermsServiceView.js b/app/views/TermsServiceView.js index 75ab82b4d..aa906de63 100644 --- a/app/views/TermsServiceView.js +++ b/app/views/TermsServiceView.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { WebView } from 'react-native'; +import { WebView, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; +import styles from './Styles'; + @connect(state => ({ termsService: state.settings.Layout_Terms_of_Service })) @@ -13,7 +15,9 @@ export default class TermsServiceView extends React.PureComponent { render() { return ( - + + + ); } } diff --git a/index.js b/index.ios.js similarity index 67% rename from index.js rename to index.ios.js index 954c96016..07233ffda 100644 --- a/index.js +++ b/index.ios.js @@ -1,6 +1,3 @@ -import '@babel/polyfill'; -import 'regenerator-runtime/runtime'; - import './app/ReactotronConfig'; import './app/push'; import App from './app/index'; diff --git a/ios/RocketChatRN/AppDelegate.m b/ios/RocketChatRN/AppDelegate.m index a6468dc2a..1cf81bd03 100644 --- a/ios/RocketChatRN/AppDelegate.m +++ b/ios/RocketChatRN/AppDelegate.m @@ -25,7 +25,7 @@ { NSURL *jsCodeLocation; #ifdef DEBUG - jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; #else jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index 863c480db..68e12532d 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.0.1 CFBundleSignature ???? CFBundleURLTypes