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, "prefer-const": 2,
"object-shorthand": 2, "object-shorthand": 2,
"consistent-return": 0, "consistent-return": 0,
"global-require": "off" "global-require": "off",
"react-native/no-unused-styles": 2
}, },
"globals": { "globals": {
"__DEV__": true "__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" apply from: "../../node_modules/react-native/react.gradle"
/** /**
@ -98,7 +102,7 @@ android {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 27 targetSdkVersion 27
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "1" versionName "1.0.1"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }

View File

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

View File

@ -46,10 +46,13 @@ public class CustomPushNotification extends PushNotification {
.setContentText(message) .setContentText(message)
.setStyle(new Notification.BigTextStyle().bigText(message)) .setStyle(new Notification.BigTextStyle().bigText(message))
.setPriority(Notification.PRIORITY_HIGH) .setPriority(Notification.PRIORITY_HIGH)
.setColor(mContext.getColor(R.color.notification_text))
.setDefaults(Notification.DEFAULT_ALL) .setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true); .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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME, CHANNEL_NAME,

View File

@ -43,7 +43,7 @@ public class MainApplication extends NavigationApplication implements INotificat
@Override @Override
public String getJSMainModuleName() { public String getJSMainModuleName() {
return "index"; return "index.android";
} }
protected List<ReactPackage> getPackages() { 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 NAVIGATION = createRequestTypes('NAVIGATION', ['SET']);
export const SERVER = createRequestTypes('SERVER', [ export const SERVER = createRequestTypes('SERVER', [
...defaultTypes, ...defaultTypes,
'SELECT', 'SELECT_SUCCESS',
'SELECT_REQUEST',
'CHANGED', 'CHANGED',
'ADD' 'ADD'
]); ]);

View File

@ -1,11 +1,19 @@
import { SERVER } from './actionsTypes'; import { SERVER } from './actionsTypes';
export function selectServer(server) { export function selectServerRequest(server) {
return { return {
type: SERVER.SELECT, type: SERVER.SELECT_REQUEST,
server server
}; };
} }
export function selectServerSuccess(server) {
return {
type: SERVER.SELECT_SUCCESS,
server
};
}
export function serverRequest(server) { export function serverRequest(server) {
return { return {
type: SERVER.REQUEST, type: SERVER.REQUEST,

View File

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

View File

@ -66,14 +66,22 @@ export default class Loading extends React.PureComponent {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.opacityAnimation && this.opacityAnimation.stop) {
this.opacityAnimation.stop(); this.opacityAnimation.stop();
}
if (this.scaleAnimation && this.scaleAnimation.stop) {
this.scaleAnimation.stop(); this.scaleAnimation.stop();
} }
}
startAnimations() { startAnimations() {
if (this.opacityAnimation && this.opacityAnimation.start) {
this.opacityAnimation.start(); this.opacityAnimation.start();
}
if (this.scaleAnimation && this.scaleAnimation.start) {
this.scaleAnimation.start(); this.scaleAnimation.start();
} }
}
render() { render() {
const scale = this.state.scale.interpolate({ const scale = this.state.scale.interpolate({

View File

@ -121,7 +121,9 @@ export default class MessageActions extends React.Component {
this.DELETE_INDEX = this.options.length - 1; this.DELETE_INDEX = this.options.length - 1;
} }
setTimeout(() => { setTimeout(() => {
this.ActionSheet.show(); if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show();
}
Vibration.vibrate(50); Vibration.vibrate(50);
}); });
} }
@ -301,7 +303,7 @@ export default class MessageActions extends React.Component {
render() { render() {
return ( return (
<ActionSheet <ActionSheet
ref={o => this.ActionSheet = o} ref={o => this.actionSheet = o}
title={I18n.t('Message_actions')} title={I18n.t('Message_actions')}
testID='message-actions' testID='message-actions'
options={this.options} options={this.options}

View File

@ -27,7 +27,9 @@ export default class FilesActions extends Component {
this.LIBRARY_INDEX = 2; this.LIBRARY_INDEX = 2;
setTimeout(() => { setTimeout(() => {
this.ActionSheet.show(); if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show();
}
}); });
} }
@ -49,7 +51,7 @@ export default class FilesActions extends Component {
render() { render() {
return ( return (
<ActionSheet <ActionSheet
ref={o => this.ActionSheet = o} ref={o => this.actionSheet = o}
options={this.options} options={this.options}
cancelButtonIndex={this.CANCEL_INDEX} cancelButtonIndex={this.CANCEL_INDEX}
onPress={this.handleActionPress} onPress={this.handleActionPress}

View File

@ -9,7 +9,6 @@ import Markdown from '../message/Markdown';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1,
flexDirection: 'row' flexDirection: 'row'
}, },
messageContainer: { messageContainer: {
@ -35,11 +34,6 @@ const styles = StyleSheet.create({
lineHeight: 16, lineHeight: 16,
marginLeft: 5 marginLeft: 5
}, },
content: {
color: '#0C0D0F',
fontSize: 16,
lineHeight: 20
},
close: { close: {
marginRight: 15 marginRight: 15
} }

View File

@ -30,7 +30,9 @@ export default class MessageErrorActions extends React.Component {
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
this.DELETE_INDEX = 1; this.DELETE_INDEX = 1;
this.RESEND_INDEX = 2; this.RESEND_INDEX = 2;
this.ActionSheet.show(); if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show();
}
} }
handleResend = protectedFunction(() => RocketChat.resendMessage(this.props.actionMessage._id)); handleResend = protectedFunction(() => RocketChat.resendMessage(this.props.actionMessage._id));
@ -59,7 +61,7 @@ export default class MessageErrorActions extends React.Component {
render() { render() {
return ( return (
<ActionSheet <ActionSheet
ref={o => this.ActionSheet = o} ref={o => this.actionSheet = o}
title={I18n.t('Message_actions')} title={I18n.t('Message_actions')}
options={this.options} options={this.options}
cancelButtonIndex={this.CANCEL_INDEX} cancelButtonIndex={this.CANCEL_INDEX}

View File

@ -1,14 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import database from '../lib/realm'; import database from '../lib/realm';
import { selectServer } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import { logout } from '../actions/login'; import { logout } from '../actions/login';
import { appStart } from '../actions';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';
import Status from '../containers/status'; import Status from '../containers/status';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
@ -19,8 +18,9 @@ import I18n from '../i18n';
import { NavigationActions } from '../Navigation'; import { NavigationActions } from '../Navigation';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
selected: { container: {
backgroundColor: 'rgba(0, 0, 0, .04)' flex: 1,
backgroundColor: '#fff'
}, },
item: { item: {
flexDirection: 'row', flexDirection: 'row',
@ -31,9 +31,6 @@ const styles = StyleSheet.create({
width: 30, width: 30,
alignItems: 'center' alignItems: 'center'
}, },
itemLeftOpacity: {
opacity: 0.62
},
itemText: { itemText: {
marginVertical: 16, marginVertical: 16,
fontWeight: 'bold', fontWeight: 'bold',
@ -88,18 +85,16 @@ const keyExtractor = item => item.id;
username: state.login.user && state.login.user.username username: state.login.user && state.login.user.username
} }
}), dispatch => ({ }), dispatch => ({
selectServer: server => dispatch(selectServer(server)), selectServerRequest: server => dispatch(selectServerRequest(server)),
logout: () => dispatch(logout()), logout: () => dispatch(logout())
appStart: () => dispatch(appStart('outside'))
})) }))
export default class Sidebar extends Component { export default class Sidebar extends Component {
static propTypes = { static propTypes = {
navigator: PropTypes.object, navigator: PropTypes.object,
server: PropTypes.string.isRequired, server: PropTypes.string.isRequired,
selectServer: PropTypes.func.isRequired, selectServerRequest: PropTypes.func.isRequired,
user: PropTypes.object, user: PropTypes.object,
logout: PropTypes.func.isRequired, logout: PropTypes.func.isRequired
appStart: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -127,7 +122,7 @@ export default class Sidebar extends Component {
} }
onPressItem = (item) => { onPressItem = (item) => {
this.props.selectServer(item.id); this.props.selectServerRequest(item.id);
} }
setStatus = () => { setStatus = () => {
@ -230,11 +225,7 @@ export default class Sidebar extends Component {
this.closeDrawer(); this.closeDrawer();
this.toggleServers(); this.toggleServers();
if (this.props.server !== item.id) { if (this.props.server !== item.id) {
this.props.selectServer(item.id); this.props.selectServerRequest(item.id);
const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ item.id }`);
if (!token) {
this.props.appStart();
}
} }
}, },
testID: `sidebar-${ item.id }` testID: `sidebar-${ item.id }`
@ -314,8 +305,8 @@ export default class Sidebar extends Component {
return null; return null;
} }
return ( return (
<ScrollView style={{ backgroundColor: '#fff' }}> <ScrollView style={styles.container}>
<SafeAreaView testID='sidebar'> <SafeAreaView testID='sidebar' style={styles.container}>
<Touch <Touch
onPress={() => this.toggleServers()} onPress={() => this.toggleServers()}
underlayColor='rgba(255, 255, 255, 0.5)' underlayColor='rgba(255, 255, 255, 0.5)'
@ -331,9 +322,9 @@ export default class Sidebar extends Component {
<View style={styles.headerTextContainer}> <View style={styles.headerTextContainer}>
<View style={styles.headerUsername}> <View style={styles.headerUsername}>
<Status style={styles.status} id={user.id} /> <Status style={styles.status} id={user.id} />
<Text>{user.username}</Text> <Text numberOfLines={1}>{user.username}</Text>
</View> </View>
<Text style={styles.currentServerText}>{server}</Text> <Text style={styles.currentServerText} numberOfLines={1}>{server}</Text>
</View> </View>
<Icon <Icon
name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'} name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}

View File

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

View File

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

View File

@ -17,13 +17,6 @@ const styles = StyleSheet.create({
marginTop: 2, marginTop: 2,
alignSelf: 'flex-end' alignSelf: 'flex-end'
}, },
quoteSign: {
borderWidth: 2,
borderRadius: 4,
borderColor: '#a0a0a0',
height: '100%',
marginRight: 5
},
attachmentContainer: { attachmentContainer: {
flex: 1, flex: 1,
flexDirection: 'column' flexDirection: 'column'

View File

@ -12,13 +12,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginVertical: 2 marginVertical: 2
}, },
quoteSign: {
borderWidth: 2,
borderRadius: 4,
borderColor: '#a0a0a0',
height: '100%',
marginRight: 5
},
image: { image: {
height: 80, height: 80,
width: 80, width: 80,

View File

@ -107,5 +107,19 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: '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.subscriptions = {};
this.ddp = new EventEmitter(); this.ddp = new EventEmitter();
this._logged = false; this._logged = false;
this.forceDisconnect = false;
this.connected = false;
const waitTimeout = () => setTimeout(() => { const waitTimeout = () => setTimeout(() => {
// this.connection.ping(); // this.connection.ping();
this.send({ msg: 'ping' }).catch(e => log('ping', e)); this.send({ msg: 'ping' }).catch(e => log('ping', e));
@ -164,8 +166,11 @@ export default class Socket extends EventEmitter {
} }
} }
async send(obj, ignore) { async send(obj, ignore) {
console.log('send'); console.log('send', obj);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.connected) {
return reject();
}
this.id += 1; this.id += 1;
const id = obj.id || `ddp-react-native-${ this.id }`; const id = obj.id || `ddp-react-native-${ this.id }`;
// console.log('send', { ...obj, 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 = new WebSocket(`${ this.url }/websocket`, null);
this.connection.onopen = async() => { this.connection.onopen = async() => {
this.connected = true;
this.forceDisconnect = false;
this.emit('open'); this.emit('open');
resolve(); resolve();
this.ddp.emit('open'); this.ddp.emit('open');
console.log(`Connected to: ${ this.url }`);
if (this._login) { if (this._login) {
return this.login(this._login).catch(e => console.warn(e)); return this.login(this._login).catch(e => console.warn(e));
} }
}; };
this.connection.onclose = debounce((e) => { this.connection.onclose = debounce((e) => {
this.emit('disconnected', e); this.emit('disconnected', e);
this.connected = false;
}, 300); }, 300);
this.connection.onmessage = (e) => { this.connection.onmessage = (e) => {
try { try {
@ -238,13 +247,17 @@ export default class Socket extends EventEmitter {
.finally(() => this.subscriptions = {}); .finally(() => this.subscriptions = {});
} }
disconnect() { disconnect() {
this._close();
this._logged = false; this._logged = false;
this._login = null; this._login = null;
this.subscriptions = {}; this.subscriptions = {};
this.forceDisconnect = true;
this._close();
if (this.timeout) {
clearTimeout(this.timeout);
}
} }
async reconnect() { async reconnect() {
if (this._timer) { if (this._timer || this.forceDisconnect) {
return; return;
} }
this._close(); this._close();

View File

@ -15,6 +15,9 @@ const getLastMessage = () => {
export default async function() { export default async function() {
try { try {
if (!this.ddp.status) {
return;
}
const lastMessage = getLastMessage(); const lastMessage = getLastMessage();
let emojis = await this.ddp.call('listEmojiCustom'); let emojis = await this.ddp.call('listEmojiCustom');
emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage); emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);

View File

@ -1,3 +1,5 @@
import moment from 'moment';
import parseUrls from './parseUrls'; import parseUrls from './parseUrls';
function normalizeAttachments(msg) { function normalizeAttachments(msg) {
@ -6,6 +8,9 @@ function normalizeAttachments(msg) {
} }
msg.attachments = msg.attachments.map((att) => { msg.attachments = msg.attachments.map((att) => {
att.fields = att.fields || []; att.fields = att.fields || [];
if (att.ts) {
att.ts = moment(att.ts).toDate();
}
att = normalizeAttachments(att); att = normalizeAttachments(att);
return att; return att;
}); });

View File

@ -299,44 +299,6 @@ const schema = [
uploadsSchema 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 { class DB {
databases = { databases = {
serversDB: new Realm({ serversDB: new Realm({
@ -376,9 +338,3 @@ class DB {
} }
} }
export default new 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,7 +85,6 @@ 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); 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) { async testServer(url) {
if (/^(https?:\/\/)?(((\w|[0-9-_])+(\.(\w|[0-9-_])+)+)|localhost)(:\d+)?$/.test(url)) {
try { try {
let response = await RNFetchBlob.fetch('HEAD', url); let response = await RNFetchBlob.fetch('HEAD', url);
response = response.respInfo; response = response.respInfo;
@ -95,7 +94,6 @@ const RocketChat = {
} catch (e) { } catch (e) {
log('testServer', e); log('testServer', e);
} }
}
throw new Error({ error: 'invalid server' }); throw new Error({ error: 'invalid server' });
}, },
_setUser(ddpMessage) { _setUser(ddpMessage) {
@ -141,6 +139,7 @@ const RocketChat = {
const userInfo = await this.userInfo({ token: user.token, userId: user.id }); const userInfo = await this.userInfo({ token: user.token, userId: user.id });
user = { ...user, ...userInfo.user }; user = { ...user, ...userInfo.user };
} }
RocketChat.registerPushToken(user.id);
return reduxStore.dispatch(loginSuccess(user)); return reduxStore.dispatch(loginSuccess(user));
} catch (e) { } catch (e) {
log('rocketchat.loginSuccess', e); log('rocketchat.loginSuccess', e);
@ -154,9 +153,9 @@ const RocketChat = {
} }
this.ddp = new Ddp(url, login); this.ddp = new Ddp(url, login);
if (login) { // if (login) {
protectedFunction(() => RocketChat.getRooms()); // protectedFunction(() => RocketChat.getRooms());
} // }
this.ddp.on('login', protectedFunction(() => reduxStore.dispatch(loginRequest()))); 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] })); 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) => { this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => {
if (ddpMessage.msg === 'added') { if (ddpMessage.msg === 'added') {
this.starredMessages = this.starredMessages || []; this.starredMessages = this.starredMessages || [];

View File

@ -47,16 +47,6 @@ const styles = StyleSheet.create({
color: '#444', color: '#444',
marginRight: 8 marginRight: 8
}, },
lastMessage: {
flex: 1,
flexShrink: 1,
marginRight: 8,
maxHeight: 20,
overflow: 'hidden',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'flex-start'
},
alert: { alert: {
fontWeight: 'bold' fontWeight: 'bold'
}, },
@ -268,6 +258,11 @@ export default class RoomItem extends React.Component {
<Text key={node.key}> <Text key={node.key}>
#{node.content} #{node.content}
</Text> </Text>
),
link: (node, children) => (
<Text key={node.key}>
{children}
</Text>
) )
}} }}
/> />

View File

@ -6,7 +6,8 @@ const initialState = {
errorMessage: '', errorMessage: '',
failure: false, failure: false,
server: '', server: '',
adding: false adding: false,
loading: true
}; };
@ -38,11 +39,18 @@ export default function server(state = initialState, action) {
...state, ...state,
adding: true adding: true
}; };
case SERVER.SELECT: case SERVER.SELECT_REQUEST:
return { return {
...state, ...state,
server: action.server, server: action.server,
adding: false loading: true
};
case SERVER.SELECT_SUCCESS:
return {
...state,
server: action.server,
adding: false,
loading: false
}; };
default: default:
return state; return state;

View File

@ -3,7 +3,7 @@ import { takeLatest, take, select, put } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { appStart } from '../actions'; import { appStart } from '../actions';
import { selectServer, addServer } from '../actions/server'; import { selectServerRequest, addServer } from '../actions/server';
import database from '../lib/realm'; import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { NavigationActions } from '../Navigation'; import { NavigationActions } from '../Navigation';
@ -68,7 +68,7 @@ const handleOpen = function* handleOpen({ params }) {
if (!token) { if (!token) {
yield put(appStart('outside')); yield put(appStart('outside'));
} else { } else {
yield put(selectServer(deepLinkServer)); yield put(selectServerRequest(deepLinkServer));
yield take(types.METEOR.REQUEST); yield take(types.METEOR.REQUEST);
yield navigate({ params, sameServer: false }); 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 { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../actions'; import * as actions from '../actions';
import { selectServer } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import { restoreToken, setUser } from '../actions/login'; import { restoreToken, setUser } from '../actions/login';
import { APP } from '../actions/actionsTypes'; import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -19,10 +19,7 @@ const restore = function* restore() {
const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer');
if (currentServer) { if (currentServer) {
yield put(selectServer(currentServer)); yield put(selectServerRequest(currentServer));
if (token) {
yield put(actions.appStart('inside'));
}
const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
if (login) { if (login) {

View File

@ -17,13 +17,14 @@ import {
setUsernameRequest, setUsernameRequest,
setUsernameSuccess, setUsernameSuccess,
forgotPasswordSuccess, forgotPasswordSuccess,
forgotPasswordFailure forgotPasswordFailure,
setUser
} from '../actions/login'; } from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
const getUser = state => state.login; const getUser = state => state.login.user;
const getServer = state => state.server.server; const getServer = state => state.server.server;
const getIsConnected = state => state.meteor.connected; const getIsConnected = state => state.meteor.connected;
@ -36,15 +37,10 @@ const forgotPasswordCall = args => RocketChat.forgotPassword(args);
const handleLoginSuccess = function* handleLoginSuccess() { const handleLoginSuccess = function* handleLoginSuccess() {
try { 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, user.token);
yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); yield put(setUser(user));
// const token = yield AsyncStorage.getItem('pushId'); if (!user.username || user.isRegistering) {
// if (token) {
// yield RocketChat.registerPushToken(user.user.id, token);
// }
yield RocketChat.registerPushToken(user.user.id);
if (!user.user.username || user.isRegistering) {
yield put(registerIncomplete()); yield put(registerIncomplete());
} else { } else {
yield delay(300); yield delay(300);
@ -127,17 +123,22 @@ const watchLoginOpen = function* watchLoginOpen() {
} }
const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration');
yield take(types.LOGIN.CLOSE); yield take(types.LOGIN.CLOSE);
if (sub) {
yield sub.unsubscribe().catch(err => console.warn(err)); yield sub.unsubscribe().catch(err => console.warn(err));
}
} catch (e) { } catch (e) {
log('watchLoginOpen', e); log('watchLoginOpen', e);
} }
}; };
// eslint-disable-next-line require-yield const handleSetUser = function* handleSetUser() {
const handleSetUser = function* handleSetUser(params) {
const [server, user] = yield all([select(getServer), select(getUser)]); const [server, user] = yield all([select(getServer), select(getUser)]);
if (params.language) { if (user) {
I18n.locale = params.language; // 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)); 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 { SERVER } from '../actions/actionsTypes';
import * as actions from '../actions'; import * as actions from '../actions';
import { connectRequest } from '../actions/connect'; 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 { setRoles } from '../actions/roles';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
@ -20,11 +20,14 @@ const validate = function* validate(server) {
const handleSelectServer = function* handleSelectServer({ server }) { const handleSelectServer = function* handleSelectServer({ server }) {
try { try {
yield database.setActiveDB(server); yield database.setActiveDB(server);
// yield RocketChat.disconnect();
yield call([AsyncStorage, 'setItem'], 'currentServer', server); 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'); const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
const emojis = database.objects('customEmojis'); const emojis = database.objects('customEmojis');
@ -36,6 +39,7 @@ const handleSelectServer = function* handleSelectServer({ server }) {
}, {}))); }, {})));
yield put(connectRequest()); yield put(connectRequest());
yield put(selectServerSuccess(server));
} catch (e) { } catch (e) {
log('handleSelectServer', e); log('handleSelectServer', e);
} }
@ -59,7 +63,7 @@ const addServer = function* addServer({ server }) {
database.databases.serversDB.write(() => { database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, current: false }, true); database.databases.serversDB.create('servers', { id: server, current: false }, true);
}); });
yield put(selectServer(server)); yield put(selectServerRequest(server));
} catch (e) { } catch (e) {
log('addServer', e); log('addServer', e);
} }
@ -67,7 +71,7 @@ const addServer = function* addServer({ server }) {
const root = function* root() { const root = function* root() {
yield takeLatest(SERVER.REQUEST, validateServer); yield takeLatest(SERVER.REQUEST, validateServer);
yield takeLatest(SERVER.SELECT, handleSelectServer); yield takeLatest(SERVER.SELECT_REQUEST, handleSelectServer);
yield takeLatest(SERVER.ADD, addServer); yield takeLatest(SERVER.ADD, addServer);
}; };
export default root; export default root;

View File

@ -5,6 +5,10 @@ import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
const appHasComeBackToForeground = function* appHasComeBackToForeground() { const appHasComeBackToForeground = function* appHasComeBackToForeground() {
const appRoot = yield select(state => state.app.root);
if (appRoot === 'outside') {
return;
}
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
if (!auth) { if (!auth) {
return; return;
@ -17,6 +21,10 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() {
}; };
const appHasComeBackToBackground = function* appHasComeBackToBackground() { const appHasComeBackToBackground = function* appHasComeBackToBackground() {
const appRoot = yield select(state => state.app.root);
if (appRoot === 'outside') {
return;
}
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
if (!auth) { if (!auth) {
return; return;

View File

@ -38,6 +38,12 @@ export default class CreateChannelView extends LoggedView {
}; };
} }
componentDidMount() {
setTimeout(() => {
this.channelNameRef.focus();
}, 600);
}
submit = () => { submit = () => {
if (!this.state.channelName.trim() || this.props.createChannel.isFetching) { if (!this.state.channelName.trim() || this.props.createChannel.isFetching) {
return; return;
@ -138,12 +144,12 @@ export default class CreateChannelView extends LoggedView {
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView testID='create-channel-view'> <SafeAreaView testID='create-channel-view'>
<RCTextInput <RCTextInput
inputRef={ref => this.channelNameRef = ref}
label={I18n.t('Channel_Name')} label={I18n.t('Channel_Name')}
value={this.state.channelName} value={this.state.channelName}
onChangeText={channelName => this.setState({ channelName })} onChangeText={channelName => this.setState({ channelName })}
placeholder={I18n.t('Type_the_channel_name_here')} placeholder={I18n.t('Type_the_channel_name_here')}
returnKeyType='done' returnKeyType='done'
autoFocus
testID='create-channel-name' testID='create-channel-name'
/> />
{this.renderChannelNameError()} {this.renderChannelNameError()}

View File

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

View File

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import LoggedView from './View'; import LoggedView from './View';
import { selectServer } from '../actions/server'; import { selectServerRequest } from '../actions/server';
import database from '../lib/realm'; import database from '../lib/realm';
import Fade from '../animations/fade'; import Fade from '../animations/fade';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
@ -14,27 +13,9 @@ import I18n from '../i18n';
import { iconsMap } from '../Icons'; import { iconsMap } from '../Icons';
const styles = StyleSheet.create({ 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: { container: {
flex: 1 flex: 1,
backgroundColor: '#fff'
}, },
separator: { separator: {
height: 1, height: 1,
@ -67,19 +48,20 @@ const styles = StyleSheet.create({
login: state.login, login: state.login,
connected: state.meteor.connected connected: state.meteor.connected
}), dispatch => ({ }), dispatch => ({
selectServer: server => dispatch(selectServer(server)) selectServerRequest: server => dispatch(selectServerRequest(server))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class ListServerView extends LoggedView { export default class ListServerView extends LoggedView {
static propTypes = { static propTypes = {
navigator: PropTypes.object, navigator: PropTypes.object,
login: PropTypes.object.isRequired, login: PropTypes.object.isRequired,
selectServer: PropTypes.func.isRequired, selectServerRequest: PropTypes.func.isRequired,
server: PropTypes.string server: PropTypes.string
} }
constructor(props) { constructor(props) {
super('ListServerView', props); super('ListServerView', props);
this.focused = true;
this.state = { this.state = {
sections: [] sections: []
}; };
@ -102,8 +84,19 @@ export default class ListServerView extends LoggedView {
this.jumpToSelectedServer(); 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() { componentWillUnmount() {
this.data.removeAllListeners(); this.data.removeAllListeners();
if (this.timeout) {
clearTimeout(this.timeout);
}
} }
onNavigatorEvent(event) { onNavigatorEvent(event) {
@ -114,6 +107,8 @@ export default class ListServerView extends LoggedView {
title: I18n.t('New_Server') 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) => { openLogin = (server) => {
if (this.focused) {
this.props.navigator.push({ this.props.navigator.push({
screen: 'LoginSignupView', screen: 'LoginSignupView',
title: server title: server
}); });
} }
}
selectAndNavigateTo = (server) => { selectAndNavigateTo = (server) => {
this.props.selectServer(server); this.props.selectServerRequest(server);
this.openLogin(server); this.openLogin(server);
} }
@ -193,7 +190,7 @@ export default class ListServerView extends LoggedView {
render() { render() {
return ( return (
<View style={styles.view} testID='list-server-view'> <SafeAreaView style={styles.container} testID='list-server-view'>
<SectionList <SectionList
style={styles.list} style={styles.list}
sections={this.state.sections} sections={this.state.sections}
@ -202,7 +199,7 @@ export default class ListServerView extends LoggedView {
keyExtractor={item => item.id} keyExtractor={item => item.id}
ItemSeparatorComponent={this.renderSeparator} ItemSeparatorComponent={this.renderSeparator}
/> />
</View> </SafeAreaView>
); );
} }
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome'; import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
@ -279,7 +279,7 @@ export default class LoginSignupView extends LoggedView {
style={[sharedStyles.container, sharedStyles.containerScrollView]} style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<View testID='welcome-view'> <SafeAreaView style={sharedStyles.container} testID='welcome-view'>
<View style={styles.container}> <View style={styles.container}>
<Image <Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
@ -307,7 +307,7 @@ export default class LoginSignupView extends LoggedView {
{this.renderServices()} {this.renderServices()}
</View> </View>
<Loading visible={this.props.isFetching} /> <Loading visible={this.props.isFetching} />
</View> </SafeAreaView>
</ScrollView> </ScrollView>
); );
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import { Answers } from 'react-native-fabric'; import { Answers } from 'react-native-fabric';
@ -106,7 +106,7 @@ export default class LoginView extends LoggedView {
key='login-view' key='login-view'
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<View testID='login-view'> <SafeAreaView style={styles.container} testID='login-view'>
<Text style={[styles.loginText, styles.loginTitle]}>Login</Text> <Text style={[styles.loginText, styles.loginTitle]}>Login</Text>
<TextInput <TextInput
label={I18n.t('Username')} 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} {this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null}
<Loading visible={this.props.isFetching} /> <Loading visible={this.props.isFetching} />
</View> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>
); );

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import LoggedView from '../View'; import LoggedView from '../View';
@ -102,10 +102,8 @@ export default class MentionedMessagesView extends LoggedView {
} }
return ( return (
[ <SafeAreaView style={styles.list} testID='mentioned-messages-view'>
<FlatList <FlatList
key='mentioned-messages-view-list'
testID='mentioned-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -114,7 +112,7 @@ export default class MentionedMessagesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null} ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server'; 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 props.validateServer(this.state.defaultServer); // Need to call because in case of submit with empty field
} }
componentDidMount() {
setTimeout(() => {
this.input.focus();
}, 600);
}
onChangeText = (text) => { onChangeText = (text) => {
this.setState({ text }); this.setState({ text });
this.props.validateServer(this.completeUrl(text)); this.props.validateServer(this.completeUrl(text));
@ -106,7 +112,7 @@ export default class NewServerView extends LoggedView {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <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> <Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_in_your_server')}</Text>
<TextInput <TextInput
inputRef={e => this.input = e} inputRef={e => this.input = e}
@ -129,7 +135,7 @@ export default class NewServerView extends LoggedView {
/> />
</View> </View>
<Loading visible={this.props.addingServer} /> <Loading visible={this.props.addingServer} />
</View> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>
); );

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
@ -69,8 +69,10 @@ export default class PinnedMessagesView extends LoggedView {
onLongPress = (message) => { onLongPress = (message) => {
this.setState({ message }); this.setState({ message });
if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show(); this.actionSheet.show();
} }
}
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
switch (actionIndex) { switch (actionIndex) {
@ -126,10 +128,8 @@ export default class PinnedMessagesView extends LoggedView {
} }
return ( return (
[ <SafeAreaView style={styles.list} testID='pinned-messages-view'>
<FlatList <FlatList
key='pinned-messages-view-list'
testID='pinned-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -137,16 +137,15 @@ export default class PinnedMessagesView extends LoggedView {
onEndReached={this.moreData} onEndReached={this.moreData}
ListHeaderComponent={loading ? <RCActivityIndicator /> : null} ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>, />
<ActionSheet <ActionSheet
key='pinned-messages-view-action-sheet'
ref={o => this.actionSheet = o} ref={o => this.actionSheet = o}
title={I18n.t('Actions')} title={I18n.t('Actions')}
options={options} options={options}
cancelButtonIndex={CANCEL_INDEX} cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress} onPress={this.handleActionPress}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { WebView } from 'react-native'; import { WebView, SafeAreaView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styles from './Styles';
@connect(state => ({ @connect(state => ({
privacyPolicy: state.settings.Layout_Privacy_Policy privacyPolicy: state.settings.Layout_Privacy_Policy
})) }))
@ -13,7 +15,9 @@ export default class PrivacyPolicyView extends React.PureComponent {
render() { render() {
return ( 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' testID='profile-view-list'
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView testID='profile-view'> <SafeAreaView style={sharedStyles.container} testID='profile-view'>
<View style={styles.avatarContainer} testID='profile-view-avatar'> <View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar <Avatar
text={username} text={username}

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 Icon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -394,7 +394,7 @@ export default class RoomActionsView extends LoggedView {
render() { render() {
return ( return (
<View testID='room-actions-view'> <SafeAreaView style={styles.container} testID='room-actions-view'>
<SectionList <SectionList
style={styles.container} style={styles.container}
stickySectionHeadersEnabled={false} stickySectionHeadersEnabled={false}
@ -404,7 +404,7 @@ export default class RoomActionsView extends LoggedView {
keyExtractor={item => item.name} keyExtractor={item => item.name}
testID='room-actions-list' testID='room-actions-list'
/> />
</View> </SafeAreaView>
); );
} }
} }

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import LoggedView from '../View'; import LoggedView from '../View';
@ -98,10 +98,8 @@ export default class RoomFilesView extends LoggedView {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;
return ( return (
[ <SafeAreaView style={styles.list} testID='room-files-view'>
<FlatList <FlatList
key='room-files-view-list'
testID='room-files-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -110,7 +108,7 @@ export default class RoomFilesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null} ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -34,8 +34,11 @@ const PERMISSIONS_ARRAY = [
PERMISSION_DELETE_P PERMISSION_DELETE_P
]; ];
@connect(null, dispatch => ({
eraseRoom: rid => dispatch(eraseRoom(rid))
}))
/** @extends React.Component */ /** @extends React.Component */
class RoomInfoEditView extends LoggedView { export default class RoomInfoEditView extends LoggedView {
static propTypes = { static propTypes = {
rid: PropTypes.string, rid: PropTypes.string,
eraseRoom: PropTypes.func eraseRoom: PropTypes.func
@ -263,7 +266,7 @@ class RoomInfoEditView extends LoggedView {
testID='room-info-edit-view-list' testID='room-info-edit-view-list'
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView testID='room-info-edit-view'> <SafeAreaView style={sharedStyles.container} testID='room-info-edit-view'>
<RCTextInput <RCTextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
label={I18n.t('Name')} 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 React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
@ -211,8 +211,9 @@ export default class RoomInfoView extends LoggedView {
return <View />; return <View />;
} }
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.scroll}>
<View style={styles.avatarContainer} testID='room-info-view'> <SafeAreaView style={styles.container} testID='room-info-view'>
<View style={styles.avatarContainer}>
{this.renderAvatar(room, roomUser)} {this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View> <View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
</View> </View>
@ -222,6 +223,7 @@ export default class RoomInfoView extends LoggedView {
{this.isDirect() ? this.renderRoles() : null} {this.isDirect() ? this.renderRoles() : null}
{this.isDirect() ? this.renderTimezone(roomUser._id) : null} {this.isDirect() ? this.renderTimezone(roomUser._id) : null}
{room.broadcast ? this.renderBroadcast() : null} {room.broadcast ? this.renderBroadcast() : null}
</SafeAreaView>
</ScrollView> </ScrollView>
); );
} }

View File

@ -2,6 +2,10 @@ import { StyleSheet } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
flex: 1,
backgroundColor: '#ffffff'
},
scroll: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
@ -122,7 +122,9 @@ export default class RoomMembersView extends LoggedView {
} }
this.setState({ userLongPressed: user }); this.setState({ userLongPressed: user });
Vibration.vibrate(50); Vibration.vibrate(50);
this.ActionSheet.show(); if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show();
}
} }
updateRoom = async() => { updateRoom = async() => {
@ -202,10 +204,8 @@ export default class RoomMembersView extends LoggedView {
render() { render() {
const { filtering, members, membersFiltered } = this.state; const { filtering, members, membersFiltered } = this.state;
return ( return (
[ <SafeAreaView style={styles.list} testID='room-members-view'>
<FlatList <FlatList
key='room-members-view-list'
testID='room-members-view'
data={filtering ? membersFiltered : members} data={filtering ? membersFiltered : members}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -213,16 +213,15 @@ export default class RoomMembersView extends LoggedView {
ItemSeparatorComponent={this.renderSeparator} ItemSeparatorComponent={this.renderSeparator}
ListHeaderComponent={this.renderSearchBar} ListHeaderComponent={this.renderSearchBar}
{...scrollPersistTaps} {...scrollPersistTaps}
/>, />
<ActionSheet <ActionSheet
key='room-members-actionsheet' ref={o => this.actionSheet = o}
ref={o => this.ActionSheet = o}
title={I18n.t('Actions')} title={I18n.t('Actions')}
options={this.actionSheetOptions} options={this.actionSheetOptions}
cancelButtonIndex={this.CANCEL_INDEX} cancelButtonIndex={this.CANCEL_INDEX}
onPress={this.handleActionPress} onPress={this.handleActionPress}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import equal from 'deep-equal'; import equal from 'deep-equal';
@ -295,7 +295,7 @@ export default class RoomView extends LoggedView {
render() { render() {
return ( return (
<View style={styles.container} testID='room-view'> <SafeAreaView style={styles.container} testID='room-view'>
{this.renderList()} {this.renderList()}
{this.renderFooter()} {this.renderFooter()}
{this.state.room._id && this.props.showActions ? {this.state.room._id && this.props.showActions ?
@ -304,7 +304,7 @@ export default class RoomView extends LoggedView {
{this.props.showErrorActions ? <MessageErrorActions /> : null} {this.props.showErrorActions ? <MessageErrorActions /> : null}
<ReactionPicker onEmojiSelected={this.onReactionPress} /> <ReactionPicker onEmojiSelected={this.onReactionPress} />
<UploadProgress rid={this.rid} /> <UploadProgress rid={this.rid} />
</View> </SafeAreaView>
); );
} }
} }

View File

@ -7,7 +7,10 @@ import { setSearch } from '../../../actions/rooms';
import styles from './styles'; import styles from './styles';
import I18n from '../../../i18n'; 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 = { static propTypes = {
setSearch: PropTypes.func 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 React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import { iconsMap } from '../../Icons'; import { iconsMap } from '../../Icons';
@ -13,11 +13,14 @@ import LoggedView from '../View';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
const ROW_HEIGHT = 70.5;
@connect(state => ({ @connect(state => ({
userId: state.login.user && state.login.user.id, userId: state.login.user && state.login.user.id,
server: state.server.server, server: state.server.server,
Site_Url: state.settings.Site_Url, Site_Url: state.settings.Site_Url,
searchText: state.rooms.searchText searchText: state.rooms.searchText,
loadingServer: state.server.loading
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomsListView extends LoggedView { export default class RoomsListView extends LoggedView {
@ -26,7 +29,8 @@ export default class RoomsListView extends LoggedView {
userId: PropTypes.string, userId: PropTypes.string,
Site_Url: PropTypes.string, Site_Url: PropTypes.string,
server: PropTypes.string, server: PropTypes.string,
searchText: PropTypes.string searchText: PropTypes.string,
loadingServer: PropTypes.bool
} }
constructor(props) { constructor(props) {
@ -34,7 +38,8 @@ export default class RoomsListView extends LoggedView {
this.state = { this.state = {
search: [], search: [],
rooms: [] rooms: [],
loading: true
}; };
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
} }
@ -48,7 +53,9 @@ export default class RoomsListView extends LoggedView {
} }
componentWillReceiveProps(props) { 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(); this.getSubscriptions();
} else if (this.props.searchText !== props.searchText) { } else if (this.props.searchText !== props.searchText) {
this.search(props.searchText); this.search(props.searchText);
@ -60,6 +67,9 @@ export default class RoomsListView extends LoggedView {
if (this.data) { if (this.data) {
this.data.removeAllListeners(); this.data.removeAllListeners();
} }
if (this.timeout) {
clearTimeout(this.timeout);
}
} }
onNavigatorEvent(event) { 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 = database.objects('subscriptions').filtered('archived != true && open == true').sorted('roomUpdatedAt', true);
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} }
this.timeout = setTimeout(() => {
this.setState({ loading: false });
}, 200);
} }
initDefaultHeader = () => { initDefaultHeader = () => {
@ -285,7 +298,11 @@ export default class RoomsListView extends LoggedView {
/>); />);
} }
renderList = () => ( renderList = () => {
if (this.state.loading) {
return <ActivityIndicator style={styles.loading} />;
}
return (
<FlatList <FlatList
data={this.state.search.length > 0 ? this.state.search : this.state.rooms} data={this.state.search.length > 0 ? this.state.search : this.state.rooms}
extraData={this.state.search.length > 0 ? this.state.search : this.state.rooms} extraData={this.state.search.length > 0 ? this.state.search : this.state.rooms}
@ -294,15 +311,18 @@ export default class RoomsListView extends LoggedView {
renderItem={this.renderItem} renderItem={this.renderItem}
ListHeaderComponent={Platform.OS === 'ios' ? this.renderSearchBar : null} ListHeaderComponent={Platform.OS === 'ios' ? this.renderSearchBar : null}
contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}} contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}}
getItemLayout={(data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index })}
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
testID='rooms-list-view-list' testID='rooms-list-view-list'
/> />
) );
}
render = () => ( render = () => (
<View style={styles.container} testID='rooms-list-view'> <SafeAreaView style={styles.container} testID='rooms-list-view'>
{this.renderList()} {this.renderList()}
</View>) </SafeAreaView>
)
} }

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, FlatList } from 'react-native'; import { View, FlatList, SafeAreaView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import LoggedView from '../View'; import LoggedView from '../View';
@ -115,10 +115,7 @@ export default class SearchMessagesView extends LoggedView {
render() { render() {
const { searching, loadingMore } = this.state; const { searching, loadingMore } = this.state;
return ( return (
<View <SafeAreaView style={styles.container} testID='search-messages-view'>
style={styles.container}
testID='search-messages-view'
>
<View style={styles.searchContainer}> <View style={styles.searchContainer}>
<RCTextInput <RCTextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
@ -140,7 +137,7 @@ export default class SearchMessagesView extends LoggedView {
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
{...scrollPersistTaps} {...scrollPersistTaps}
/> />
</View> </SafeAreaView>
); );
} }
} }

View File

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

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 = () => { formIsChanged = () => {
const { language } = this.state; const { language } = this.state;
return !(this.props.userLanguage === language); return !(this.props.userLanguage === language);
@ -107,6 +116,10 @@ export default class SettingsView extends LoggedView {
this.setState({ saving: false }); this.setState({ saving: false });
setTimeout(() => { setTimeout(() => {
showToast(I18n.t('Preferences_saved')); showToast(I18n.t('Preferences_saved'));
if (params.language) {
this.props.navigator.setTitle({ title: I18n.t('Settings') });
}
}, 300); }, 300);
} catch (e) { } catch (e) {
this.setState({ saving: false }); this.setState({ saving: false });
@ -132,7 +145,7 @@ export default class SettingsView extends LoggedView {
testID='settings-view-list' testID='settings-view-list'
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView testID='settings-view'> <SafeAreaView style={sharedStyles.container} testID='settings-view'>
<RNPickerSelect <RNPickerSelect
items={languages} items={languages}
onValueChange={(value) => { onValueChange={(value) => {
@ -145,7 +158,7 @@ export default class SettingsView extends LoggedView {
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
label={I18n.t('Language')} label={I18n.t('Language')}
placeholder={I18n.t('Language')} placeholder={I18n.t('Language')}
value={languages.find(i => i.value === language).label} value={this.getLabel(language)}
testID='settings-view-language' testID='settings-view-language'
/> />
</RNPickerSelect> </RNPickerSelect>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import LoggedView from '../View'; import LoggedView from '../View';
@ -102,10 +102,8 @@ export default class SnippetedMessagesView extends LoggedView {
} }
return ( return (
[ <SafeAreaView style={styles.list} testID='snippeted-messages-view'>
<FlatList <FlatList
key='snippeted-messages-view-list'
testID='snippeted-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -114,7 +112,7 @@ export default class SnippetedMessagesView extends LoggedView {
ListHeaderComponent={loading ? <RCActivityIndicator /> : null} ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
@ -69,8 +69,10 @@ export default class StarredMessagesView extends LoggedView {
onLongPress = (message) => { onLongPress = (message) => {
this.setState({ message }); this.setState({ message });
if (this.actionSheet && this.actionSheet.show) {
this.actionSheet.show(); this.actionSheet.show();
} }
}
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
switch (actionIndex) { switch (actionIndex) {
@ -126,10 +128,8 @@ export default class StarredMessagesView extends LoggedView {
} }
return ( return (
[ <SafeAreaView style={styles.list} testID='starred-messages-view'>
<FlatList <FlatList
key='starred-messages-view-list'
testID='starred-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -137,16 +137,15 @@ export default class StarredMessagesView extends LoggedView {
onEndReached={this.moreData} onEndReached={this.moreData}
ListHeaderComponent={loading ? <RCActivityIndicator /> : null} ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null} ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
/>, />
<ActionSheet <ActionSheet
key='starred-messages-view-action-sheet'
ref={o => this.actionSheet = o} ref={o => this.actionSheet = o}
title={I18n.t('Actions')} title={I18n.t('Actions')}
options={options} options={options}
cancelButtonIndex={CANCEL_INDEX} cancelButtonIndex={CANCEL_INDEX}
onPress={this.handleActionPress} onPress={this.handleActionPress}
/> />
] </SafeAreaView>
); );
} }
} }

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { WebView } from 'react-native'; import { WebView, SafeAreaView } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styles from './Styles';
@connect(state => ({ @connect(state => ({
termsService: state.settings.Layout_Terms_of_Service termsService: state.settings.Layout_Terms_of_Service
})) }))
@ -13,7 +15,9 @@ export default class TermsServiceView extends React.PureComponent {
render() { render() {
return ( 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/ReactotronConfig';
import './app/push'; import './app/push';
import App from './app/index'; import App from './app/index';

View File

@ -25,7 +25,7 @@
{ {
NSURL *jsCodeLocation; NSURL *jsCodeLocation;
#ifdef DEBUG #ifdef DEBUG
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
#else #else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif #endif

View File

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