Improve RoomsList render time (#384)

<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative

<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->

<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
- [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
This commit is contained in:
Diego Mello 2018-08-01 16:35:06 -03:00 committed by Guilherme Gazzo
parent 90c777cd2b
commit 50eb03589a
62 changed files with 381 additions and 427 deletions

View File

@ -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

View File

@ -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"
}

View File

@ -1,7 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.reactnative"
android:versionCode="1"
android:versionName="1.0">
package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

View File

@ -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,

View File

@ -43,7 +43,7 @@ public class MainApplication extends NavigationApplication implements INotificat
@Override
public String getJSMainModuleName() {
return "index";
return "index.android";
}
protected List<ReactPackage> getPackages() {

View File

@ -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'
]);

View File

@ -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,

View File

@ -13,6 +13,7 @@ const colors = {
textColorSecondary: COLOR_TEXT
};
/* eslint-disable react-native/no-unused-styles */
const styles = StyleSheet.create({
container: {
paddingHorizontal: 15,

View File

@ -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() {

View File

@ -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 (
<ActionSheet
ref={o => this.ActionSheet = o}
ref={o => this.actionSheet = o}
title={I18n.t('Message_actions')}
testID='message-actions'
options={this.options}

View File

@ -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 (
<ActionSheet
ref={o => this.ActionSheet = o}
ref={o => this.actionSheet = o}
options={this.options}
cancelButtonIndex={this.CANCEL_INDEX}
onPress={this.handleActionPress}

View File

@ -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
}

View File

@ -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 (
<ActionSheet
ref={o => this.ActionSheet = o}
ref={o => this.actionSheet = o}
title={I18n.t('Message_actions')}
options={this.options}
cancelButtonIndex={this.CANCEL_INDEX}

View File

@ -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 (
<ScrollView style={{ backgroundColor: '#fff' }}>
<SafeAreaView testID='sidebar'>
<ScrollView style={styles.container}>
<SafeAreaView testID='sidebar' style={styles.container}>
<Touch
onPress={() => this.toggleServers()}
underlayColor='rgba(255, 255, 255, 0.5)'
@ -331,9 +322,9 @@ export default class Sidebar extends Component {
<View style={styles.headerTextContainer}>
<View style={styles.headerUsername}>
<Status style={styles.status} id={user.id} />
<Text>{user.username}</Text>
<Text numberOfLines={1}>{user.username}</Text>
</View>
<Text style={styles.currentServerText}>{server}</Text>
<Text style={styles.currentServerText} numberOfLines={1}>{server}</Text>
</View>
<Icon
name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}

View File

@ -1,25 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import FastImage from 'react-native-fast-image';
import { TouchableOpacity, StyleSheet } from 'react-native';
import { TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'column'
},
image: {
width: 320,
height: 200
// resizeMode: 'cover'
},
labelContainer: {
alignItems: 'flex-start'
}
});
import styles from './styles';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
@ -55,7 +42,7 @@ export default class extends React.PureComponent {
<TouchableOpacity
key='image'
onPress={() => this._onPressButton()}
style={styles.button}
style={styles.imageContainer}
>
<FastImage
style={styles.image}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Text, Platform } from 'react-native';
import { Text, Platform, Image } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
@ -66,6 +66,10 @@ export default class Markdown extends React.Component {
return null;
},
blocklink: () => {},
image: node => (
// TODO: should use Image component
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
),
...rules
}}
style={{

View File

@ -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'

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 || [];

View File

@ -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 {
<Text key={node.key}>
#{node.content}
</Text>
),
link: (node, children) => (
<Text key={node.key}>
{children}
</Text>
)
}}
/>

View File

@ -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;

View File

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

View File

@ -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) {

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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 {
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='create-channel-view'>
<RCTextInput
inputRef={ref => 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()}

View File

@ -77,8 +77,8 @@ export default class ForgotPasswordView extends LoggedView {
keyboardVerticalOffset={128}
>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='forgot-password-view'>
<View style={styles.loginView}>
<SafeAreaView style={styles.container} testID='forgot-password-view'>
<View>
<TextInput
inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
label={I18n.t('Email')}

View File

@ -1,12 +1,11 @@
import React from 'react';
import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { View, Text, SectionList, StyleSheet } from 'react-native';
import { View, Text, SectionList, StyleSheet, SafeAreaView } from 'react-native';
import { connect } from 'react-redux';
import LoggedView from './View';
import { selectServer } from '../actions/server';
import { selectServerRequest } from '../actions/server';
import database from '../lib/realm';
import Fade from '../animations/fade';
import Touch from '../utils/touch';
@ -14,27 +13,9 @@ import I18n from '../i18n';
import { iconsMap } from '../Icons';
const styles = StyleSheet.create({
view: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'stretch',
backgroundColor: '#fff'
},
input: {
height: 40,
borderColor: '#aaa',
margin: 20,
padding: 5,
borderWidth: 0,
backgroundColor: '#f8f8f8'
},
text: {
textAlign: 'center',
color: '#888'
},
container: {
flex: 1
flex: 1,
backgroundColor: '#fff'
},
separator: {
height: 1,
@ -67,19 +48,20 @@ const styles = StyleSheet.create({
login: state.login,
connected: state.meteor.connected
}), dispatch => ({
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 (
<View style={styles.view} testID='list-server-view'>
<SafeAreaView style={styles.container} testID='list-server-view'>
<SectionList
style={styles.list}
sections={this.state.sections}
@ -202,7 +199,7 @@ export default class ListServerView extends LoggedView {
keyExtractor={item => item.id}
ItemSeparatorComponent={this.renderSeparator}
/>
</View>
</SafeAreaView>
);
}
}

View File

@ -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}
>
<View testID='welcome-view'>
<SafeAreaView style={sharedStyles.container} testID='welcome-view'>
<View style={styles.container}>
<Image
source={require('../static/images/logo.png')}
@ -307,7 +307,7 @@ export default class LoginSignupView extends LoggedView {
{this.renderServices()}
</View>
<Loading visible={this.props.isFetching} />
</View>
</SafeAreaView>
</ScrollView>
);
}

View File

@ -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'
>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<View testID='login-view'>
<SafeAreaView style={styles.container} testID='login-view'>
<Text style={[styles.loginText, styles.loginTitle]}>Login</Text>
<TextInput
label={I18n.t('Username')}
@ -158,7 +158,7 @@ export default class LoginView extends LoggedView {
{this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null}
<Loading visible={this.props.isFetching} />
</View>
</SafeAreaView>
</ScrollView>
</KeyboardView>
);

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='mentioned-messages-view'>
<FlatList
key='mentioned-messages-view-list'
testID='mentioned-messages-view'
data={messages}
renderItem={this.renderItem}
style={styles.list}
@ -114,7 +112,7 @@ export default class MentionedMessagesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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}
>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<View testID='new-server-view'>
<SafeAreaView style={styles.container} testID='new-server-view'>
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_in_your_server')}</Text>
<TextInput
inputRef={e => this.input = e}
@ -129,7 +135,7 @@ export default class NewServerView extends LoggedView {
/>
</View>
<Loading visible={this.props.addingServer} />
</View>
</SafeAreaView>
</ScrollView>
</KeyboardView>
);

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='pinned-messages-view'>
<FlatList
key='pinned-messages-view-list'
testID='pinned-messages-view'
data={messages}
renderItem={this.renderItem}
style={styles.list}
@ -137,16 +137,15 @@ export default class PinnedMessagesView extends LoggedView {
onEndReached={this.moreData}
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>,
/>
<ActionSheet
key='pinned-messages-view-action-sheet'
ref={o => this.actionSheet = o}
title={I18n.t('Actions')}
options={options}
cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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 (
<WebView source={{ html: this.props.privacyPolicy }} />
<SafeAreaView style={styles.container}>
<WebView originWhitelist={['*']} source={{ html: this.props.privacyPolicy, baseUrl: '' }} />
</SafeAreaView>
);
}
}

View File

@ -378,7 +378,7 @@ export default class ProfileView extends LoggedView {
testID='profile-view-list'
{...scrollPersistTaps}
>
<SafeAreaView testID='profile-view'>
<SafeAreaView style={sharedStyles.container} testID='profile-view'>
<View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar
text={username}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text, View, ScrollView } from 'react-native';
import { Keyboard, Text, View, ScrollView, SafeAreaView } from 'react-native';
import { connect } from 'react-redux';
import { registerSubmit, setUsernameSubmit } from '../actions/login';
@ -212,7 +212,7 @@ export default class RegisterView extends LoggedView {
return (
<KeyboardView contentContainerStyle={styles.container}>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<View testID='register-view'>
<SafeAreaView style={styles.container} testID='register-view'>
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
{this._renderRegister()}
{this._renderUsername()}
@ -223,7 +223,7 @@ export default class RegisterView extends LoggedView {
: null
}
<Loading visible={this.props.login.isFetching} />
</View>
</SafeAreaView>
</ScrollView>
</KeyboardView>
);

View File

@ -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 (
<View testID='room-actions-view'>
<SafeAreaView style={styles.container} testID='room-actions-view'>
<SectionList
style={styles.container}
stickySectionHeadersEnabled={false}
@ -404,7 +404,7 @@ export default class RoomActionsView extends LoggedView {
keyExtractor={item => item.name}
testID='room-actions-list'
/>
</View>
</SafeAreaView>
);
}
}

View File

@ -2,6 +2,7 @@ import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F6F7F9'
},
sectionItem: {

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='room-files-view'>
<FlatList
key='room-files-view-list'
testID='room-files-view'
data={messages}
renderItem={this.renderItem}
style={styles.list}
@ -110,7 +108,7 @@ export default class RoomFilesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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}
>
<SafeAreaView testID='room-info-edit-view'>
<SafeAreaView style={sharedStyles.container} testID='room-info-edit-view'>
<RCTextInput
inputRef={(e) => { 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);

View File

@ -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 <View />;
}
return (
<ScrollView style={styles.container}>
<View style={styles.avatarContainer} testID='room-info-view'>
{this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
</View>
{!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}
<ScrollView style={styles.scroll}>
<SafeAreaView style={styles.container} testID='room-info-view'>
<View style={styles.avatarContainer}>
{this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
</View>
{!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}
</SafeAreaView>
</ScrollView>
);
}

View File

@ -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',

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='room-members-view'>
<FlatList
key='room-members-view-list'
testID='room-members-view'
data={filtering ? membersFiltered : members}
renderItem={this.renderItem}
style={styles.list}
@ -213,16 +213,15 @@ export default class RoomMembersView extends LoggedView {
ItemSeparatorComponent={this.renderSeparator}
ListHeaderComponent={this.renderSearchBar}
{...scrollPersistTaps}
/>,
/>
<ActionSheet
key='room-members-actionsheet'
ref={o => this.ActionSheet = o}
ref={o => this.actionSheet = o}
title={I18n.t('Actions')}
options={this.actionSheetOptions}
cancelButtonIndex={this.CANCEL_INDEX}
onPress={this.handleActionPress}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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 (
<View style={styles.container} testID='room-view'>
<SafeAreaView style={styles.container} testID='room-view'>
{this.renderList()}
{this.renderFooter()}
{this.state.room._id && this.props.showActions ?
@ -304,7 +304,7 @@ export default class RoomView extends LoggedView {
{this.props.showErrorActions ? <MessageErrorActions /> : null}
<ReactionPicker onEmojiSelected={this.onReactionPress} />
<UploadProgress rid={this.rid} />
</View>
</SafeAreaView>
);
}
}

View File

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

View File

@ -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 = () => (
<FlatList
data={this.state.search.length > 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 <ActivityIndicator style={styles.loading} />;
}
return (
<FlatList
data={this.state.search.length > 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 = () => (
<View style={styles.container} testID='rooms-list-view'>
<SafeAreaView style={styles.container} testID='rooms-list-view'>
{this.renderList()}
</View>)
</SafeAreaView>
)
}

View File

@ -37,5 +37,8 @@ export default StyleSheet.create({
padding: 5,
paddingLeft: 10,
color: '#aaa'
},
loading: {
flex: 1
}
});

View File

@ -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 (
<View
style={styles.container}
testID='search-messages-view'
>
<SafeAreaView style={styles.container} testID='search-messages-view'>
<View style={styles.searchContainer}>
<RCTextInput
inputRef={(e) => { this.name = e; }}
@ -140,7 +137,7 @@ export default class SearchMessagesView extends LoggedView {
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
{...scrollPersistTaps}
/>
</View>
</SafeAreaView>
);
}
}

View File

@ -297,11 +297,9 @@ export default class SelectedUsersView extends LoggedView {
/>
);
render = () => (
<View style={styles.container} testID='select-users-view'>
<SafeAreaView style={styles.safeAreaView}>
{this.renderList()}
<Loading visible={this.props.loading} />
</SafeAreaView>
</View>
<SafeAreaView style={styles.safeAreaView} testID='select-users-view'>
{this.renderList()}
<Loading visible={this.props.loading} />
</SafeAreaView>
);
}

View File

@ -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}
>
<SafeAreaView testID='settings-view'>
<SafeAreaView style={sharedStyles.container} testID='settings-view'>
<RNPickerSelect
items={languages}
onValueChange={(value) => {
@ -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'
/>
</RNPickerSelect>

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='snippeted-messages-view'>
<FlatList
key='snippeted-messages-view-list'
testID='snippeted-messages-view'
data={messages}
renderItem={this.renderItem}
style={styles.list}
@ -114,7 +112,7 @@ export default class SnippetedMessagesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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 (
[
<SafeAreaView style={styles.list} testID='starred-messages-view'>
<FlatList
key='starred-messages-view-list'
testID='starred-messages-view'
data={messages}
renderItem={this.renderItem}
style={styles.list}
@ -137,16 +137,15 @@ export default class StarredMessagesView extends LoggedView {
onEndReached={this.moreData}
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>,
/>
<ActionSheet
key='starred-messages-view-action-sheet'
ref={o => this.actionSheet = o}
title={I18n.t('Actions')}
options={options}
cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress}
/>
]
</SafeAreaView>
);
}
}

View File

@ -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 (
<WebView source={{ html: this.props.termsService }} />
<SafeAreaView style={styles.container}>
<WebView originWhitelist={['*']} source={{ html: this.props.termsService, baseUrl: '' }} />
</SafeAreaView>
);
}
}

View File

@ -1,6 +1,3 @@
import '@babel/polyfill';
import 'regenerator-runtime/runtime';
import './app/ReactotronConfig';
import './app/push';
import App from './app/index';

View File

@ -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

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>