diff --git a/README.md b/README.md
index 62d11f3b9..1ee828a97 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,12 @@
**Supported Server Versions:** 0.58.0+ (We are working to support earlier versions)
+# Download
+[![Rocket.Chat.ReactNative on Google Play](https://play.google.com/intl/en_us/badges/images/badge_new.png)](https://play.google.com/store/apps/details?id=chat.rocket.reactnative)
+
+Note: If you want to try iOS version, send us an email to testflight@rocket.chat and we'll add you to TestFlight users.
+
+
# Installing dependencies
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap
index 8b1bf18ed..988f57af1 100644
--- a/__tests__/__snapshots__/RoomItem.js.snap
+++ b/__tests__/__snapshots__/RoomItem.js.snap
@@ -585,7 +585,7 @@ exports[`render unread +999 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/name",
+ "uri": "/avatar/name?random=0",
}
}
style={
@@ -835,7 +835,7 @@ exports[`render unread 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/name",
+ "uri": "/avatar/name?random=0",
}
}
style={
@@ -1085,7 +1085,7 @@ exports[`renders correctly 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/name",
+ "uri": "/avatar/name?random=0",
}
}
style={
diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 7dc94d89e..86fba1e32 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -62,7 +62,7 @@ exports[`Storyshots Avatar avatar 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/test",
+ "uri": "/avatar/test?random=0",
}
}
style={
@@ -136,7 +136,7 @@ exports[`Storyshots Avatar avatar 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/aa",
+ "uri": "/avatar/aa?random=0",
}
}
style={
@@ -210,7 +210,7 @@ exports[`Storyshots Avatar avatar 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/bb",
+ "uri": "/avatar/bb?random=0",
}
}
style={
@@ -284,7 +284,7 @@ exports[`Storyshots Avatar avatar 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/test",
+ "uri": "/avatar/test?random=0",
}
}
style={
@@ -393,7 +393,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/rocket.cat",
+ "uri": "/avatar/rocket.cat?random=0",
}
}
style={
@@ -615,7 +615,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/rocket.cat",
+ "uri": "/avatar/rocket.cat?random=0",
}
}
style={
@@ -841,7 +841,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/rocket.cat",
+ "uri": "/avatar/rocket.cat?random=0",
}
}
style={
@@ -1086,7 +1086,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+ "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
}
}
style={
@@ -1335,7 +1335,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+ "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
}
}
style={
@@ -1580,7 +1580,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+ "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
}
}
style={
@@ -1825,7 +1825,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+ "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
}
}
style={
@@ -2070,7 +2070,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+ "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
}
}
style={
@@ -2315,7 +2315,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/W",
+ "uri": "/avatar/W?random=0",
}
}
style={
@@ -2537,7 +2537,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/WW",
+ "uri": "/avatar/WW?random=0",
}
}
style={
@@ -2759,7 +2759,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
source={
Object {
"priority": "high",
- "uri": "/avatar/",
+ "uri": "/avatar/?random=0",
}
}
style={
diff --git a/app/actions/login.js b/app/actions/login.js
index 875c46706..e9e332b36 100644
--- a/app/actions/login.js
+++ b/app/actions/login.js
@@ -120,8 +120,10 @@ export function forgotPasswordFailure(err) {
export function setUser(action) {
return {
- type: types.USER.SET,
- ...action
+ // do not change this params order
+ // since we use spread operator, sometimes `type` is overriden
+ ...action,
+ type: types.USER.SET
};
}
diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js
index 3e49a100e..42b52c794 100644
--- a/app/containers/Avatar.js
+++ b/app/containers/Avatar.js
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
import FastImage from 'react-native-fast-image';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
+import database from '../lib/realm';
const styles = StyleSheet.create({
iconContainer: {
@@ -26,17 +27,78 @@ export default class Avatar extends React.PureComponent {
static propTypes = {
style: ViewPropTypes.style,
baseUrl: PropTypes.string,
- text: PropTypes.string.isRequired,
+ text: PropTypes.string,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
- children: PropTypes.object
+ children: PropTypes.object,
+ forceInitials: PropTypes.bool
};
- state = { showInitials: true };
+ static defaultProps = {
+ text: '',
+ size: 25,
+ type: 'd',
+ borderRadius: 2,
+ forceInitials: false
+ };
+ state = { showInitials: true, user: {} };
+
+ componentDidMount() {
+ const { text, type } = this.props;
+ if (type === 'd') {
+ this.users = this.userQuery(text);
+ this.users.addListener(this.update);
+ this.update();
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.text !== this.props.text && nextProps.type === 'd') {
+ if (this.users) {
+ this.users.removeAllListeners();
+ }
+ this.users = this.userQuery(nextProps.text);
+ this.users.addListener(this.update);
+ this.update();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.users) {
+ this.users.removeAllListeners();
+ }
+ }
+
+ get avatarVersion() {
+ return (this.state.user && this.state.user.avatarVersion) || 0;
+ }
+
+ /** FIXME: Workaround
+ * While we don't have containers/components structure, this is breaking tests.
+ * In that case, avatar would be a component, it would receive an `avatarVersion` param
+ * and we would have a avatar container in charge of making queries.
+ * Also, it would make possible to write unit tests like these.
+ */
+ userQuery = (username) => {
+ if (database && database.databases && database.databases.activeDB) {
+ return database.objects('users').filtered('username = $0', username);
+ }
+ return {
+ addListener: () => {},
+ removeAllListeners: () => {}
+ };
+ }
+
+ update = () => {
+ if (this.users.length) {
+ this.setState({ user: this.users[0] });
+ }
+ }
+
render() {
const {
- text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd'
+ text, size, baseUrl, borderRadius, style, avatar, type, forceInitials
} = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`);
@@ -60,9 +122,9 @@ export default class Avatar extends React.PureComponent {
let image;
- if (type === 'd') {
- const uri = avatar || `${ baseUrl }/avatar/${ text }`;
- image = uri && (
+ if (type === 'd' && !forceInitials) {
+ const uri = avatar || `${ baseUrl }/avatar/${ text }?random=${ this.avatarVersion }`;
+ image = uri ? (
- );
+ ) : null;
}
return (
- {this.state.showInitials &&
+ {this.state.showInitials ?
{initials}
+ : null
}
{image}
{this.props.children}
diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js
index fea1c26b6..25925ad5f 100644
--- a/app/containers/MessageBox/index.js
+++ b/app/containers/MessageBox/index.js
@@ -172,7 +172,7 @@ export default class MessageBox extends React.PureComponent {
maxWidth: 1960,
quality: 0.8
};
- ImagePicker.showImagePicker(options, (response) => {
+ ImagePicker.showImagePicker(options, async(response) => {
if (response.didCancel) {
console.warn('User cancelled image picker');
} else if (response.error) {
@@ -185,7 +185,11 @@ export default class MessageBox extends React.PureComponent {
// description: '',
store: 'Uploads'
};
- RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
+ try {
+ await RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
+ } catch (e) {
+ log('addFile', e);
+ }
}
});
}
@@ -459,6 +463,7 @@ export default class MessageBox extends React.PureComponent {
style={{ margin: 8 }}
text={item.username || item.name}
size={30}
+ type={item.username ? 'd' : 'c'}
/>,
{ item.username || item.name }
]
@@ -477,7 +482,7 @@ export default class MessageBox extends React.PureComponent {
style={styles.mentionList}
data={mentions}
renderItem={({ item }) => this.renderMentionItem(item)}
- keyExtractor={item => item._id || item}
+ keyExtractor={item => item._id || item.username || item}
keyboardShouldPersistTaps='always'
/>
diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js
index e2acce4c7..ea6cc2f9d 100644
--- a/app/containers/Sidebar.js
+++ b/app/containers/Sidebar.js
@@ -95,26 +95,20 @@ export default class Sidebar extends Component {
super(props);
this.state = {
servers: [],
- status: [{
- id: 'online',
- name: I18n.t('Online')
- }, {
- id: 'busy',
- name: I18n.t('Busy')
- }, {
- id: 'away',
- name: I18n.t('Away')
- }, {
- id: 'offline',
- name: I18n.t('Invisible')
- }],
showServers: false
};
}
componentDidMount() {
- database.databases.serversDB.addListener('change', this.updateState);
this.setState(this.getState());
+ this.setStatus();
+ database.databases.serversDB.addListener('change', this.updateState);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.user && this.props.user && this.props.user.language !== nextProps.user.language) {
+ this.setStatus();
+ }
}
componentWillUnmount() {
@@ -126,6 +120,26 @@ export default class Sidebar extends Component {
this.closeDrawer();
}
+ setStatus = () => {
+ setTimeout(() => {
+ this.setState({
+ status: [{
+ id: 'online',
+ name: I18n.t('Online')
+ }, {
+ id: 'busy',
+ name: I18n.t('Busy')
+ }, {
+ id: 'away',
+ name: I18n.t('Away')
+ }, {
+ id: 'offline',
+ name: I18n.t('Invisible')
+ }]
+ });
+ });
+ }
+
getState = () => ({
servers: database.databases.serversDB.objects('servers')
})
@@ -153,6 +167,8 @@ export default class Sidebar extends Component {
const { navigate } = this.props.navigation;
if (!this.isRouteFocused(route)) {
navigate(route);
+ } else {
+ this.closeDrawer();
}
}
@@ -211,6 +227,7 @@ export default class Sidebar extends Component {
this.toggleServers();
if (this.props.server !== item.id) {
this.props.selectServer(item.id);
+ this.props.navigation.navigate('RoomsList');
}
},
testID: `sidebar-${ item.id }`
@@ -324,8 +341,8 @@ export default class Sidebar extends Component {
{this.renderSeparator('separator-header')}
- {!this.state.showServers && this.renderNavigation()}
- {this.state.showServers && this.renderServers()}
+ {!this.state.showServers ? this.renderNavigation() : null}
+ {this.state.showServers ? this.renderServers() : null}
);
diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js
index 885b93022..7e85e553c 100644
--- a/app/containers/TextInput.js
+++ b/app/containers/TextInput.js
@@ -105,7 +105,7 @@ export default class RCTextInput extends React.PureComponent {
const { showPassword } = this.state;
return (
- {label && {label} }
+ {label ? {label} : null }
- {iconLeft && this.iconLeft(iconLeft)}
- {secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')}
+ {iconLeft ? this.iconLeft(iconLeft) : null}
+ {secureTextEntry ? this.iconPassword(showPassword ? 'eye-off' : 'eye') : null}
- {error.error && {error.reason}}
+ {error.error ? {error.reason} : null}
);
}
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
index 592482bb6..1b42c73c3 100644
--- a/app/containers/message/Reply.js
+++ b/app/containers/message/Reply.js
@@ -78,7 +78,6 @@ const Reply = ({ attachment, timeFormat }) => {
);
};
@@ -136,7 +135,11 @@ const Reply = ({ attachment, timeFormat }) => {
{renderTitle()}
{renderText()}
{renderFields()}
- {attachment.attachments && attachment.attachments.map(attach => )}
+ {attachment.attachments ?
+ attachment.attachments
+ .map(attach => )
+ : null
+ }
);
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 54c31aa49..46152b204 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -358,13 +358,14 @@ export default class Message extends React.Component {
{this.renderBroadcastReply()}
- {this.state.reactionsModal &&
+ {this.state.reactionsModal ?
+ : null
}
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index 3ded765b3..5361dd2e3 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { Platform } from 'react-native';
+import { Platform, TouchableOpacity } from 'react-native';
import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/MaterialIcons';
@@ -22,6 +22,7 @@ import RoomInfoEditView from '../../views/RoomInfoEditView';
import ProfileView from '../../views/ProfileView';
import SettingsView from '../../views/SettingsView';
import I18n from '../../i18n';
+import sharedStyles from '../../views/Styles';
const headerTintColor = '#292E35';
@@ -132,12 +133,24 @@ const AuthRoutes = createStackNavigator(
}
);
+const MenuButton = ({ navigation, testID }) => (
+
+
+
+);
+
const Routes = createDrawerNavigator(
{
Chats: {
screen: AuthRoutes,
navigationOptions: {
- drawerLabel: 'Chats',
+ drawerLabel: I18n.t('Chats'),
drawerIcon: () =>
}
},
@@ -146,9 +159,9 @@ const Routes = createDrawerNavigator(
ProfileView: {
screen: ProfileView,
navigationOptions: ({ navigation }) => ({
- title: 'Profile',
+ title: I18n.t('Profile'),
headerTintColor: '#292E35',
- headerLeft: navigation.toggleDrawer()} /> // TODO: refactor
+ headerLeft:
})
}
})
@@ -158,9 +171,9 @@ const Routes = createDrawerNavigator(
SettingsView: {
screen: SettingsView,
navigationOptions: ({ navigation }) => ({
- title: 'Settings',
+ title: I18n.t('Settings'),
headerTintColor: '#292E35',
- headerLeft: navigation.toggleDrawer()} /> // TODO: refactor
+ headerLeft:
})
}
})
@@ -168,9 +181,7 @@ const Routes = createDrawerNavigator(
},
{
contentComponent: Sidebar,
- navigationOptions: {
- drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
- },
+ drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked',
initialRouteName: 'Chats',
backBehavior: 'initialRoute'
}
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index c66849ca2..a54dcdceb 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -1,6 +1,80 @@
export default {
'1_online_member': '1 online member',
'1_person_reacted': '1 person reacted',
+ 'error-action-not-allowed': '{{action}} is not allowed',
+ 'error-application-not-found': 'Application not found',
+ 'error-archived-duplicate-name': 'There\'s an archived channel with name {{room_name}}',
+ 'error-avatar-invalid-url': 'Invalid avatar URL: {{url}}',
+ 'error-avatar-url-handling': 'Error while handling avatar setting from a URL ({{url}}) for {{username}}',
+ 'error-cant-invite-for-direct-room': 'Can\'t invite user to direct rooms',
+ 'error-could-not-change-email': 'Could not change email',
+ 'error-could-not-change-name': 'Could not change name',
+ 'error-could-not-change-username': 'Could not change username',
+ 'error-delete-protected-role': 'Cannot delete a protected role',
+ 'error-department-not-found': 'Department not found',
+ 'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages',
+ 'error-duplicate-channel-name': 'A channel with name {{channel_name}} exists',
+ 'error-email-domain-blacklisted': 'The email domain is blacklisted',
+ 'error-email-send-failed': 'Error trying to send email: {{message}}',
+ 'error-field-unavailable': '{{field}} is already in use :(',
+ 'error-file-too-large': 'File is too large',
+ 'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
+ 'error-input-is-not-a-valid-field': '{{input}} is not a valid {{field}}',
+ 'error-invalid-actionlink': 'Invalid action link',
+ 'error-invalid-arguments': 'Invalid arguments',
+ 'error-invalid-asset': 'Invalid asset',
+ 'error-invalid-channel': 'Invalid channel.',
+ 'error-invalid-channel-start-with-chars': 'Invalid channel. Start with @ or #',
+ 'error-invalid-custom-field': 'Invalid custom field',
+ 'error-invalid-custom-field-name': 'Invalid custom field name. Use only letters, numbers, hyphens and underscores.',
+ 'error-invalid-date': 'Invalid date provided.',
+ 'error-invalid-description': 'Invalid description',
+ 'error-invalid-domain': 'Invalid domain',
+ 'error-invalid-email': 'Invalid email {{emai}}',
+ 'error-invalid-email-address': 'Invalid email address',
+ 'error-invalid-file-height': 'Invalid file height',
+ 'error-invalid-file-type': 'Invalid file type',
+ 'error-invalid-file-width': 'Invalid file width',
+ 'error-invalid-from-address': 'You informed an invalid FROM address.',
+ 'error-invalid-integration': 'Invalid integration',
+ 'error-invalid-message': 'Invalid message',
+ 'error-invalid-method': 'Invalid method',
+ 'error-invalid-name': 'Invalid name',
+ 'error-invalid-password': 'Invalid password',
+ 'error-invalid-redirectUri': 'Invalid redirectUri',
+ 'error-invalid-role': 'Invalid role',
+ 'error-invalid-room': 'Invalid room',
+ 'error-invalid-room-name': '{{room_name}} is not a valid room name',
+ 'error-invalid-room-type': '{{type}} is not a valid room type.',
+ 'error-invalid-settings': 'Invalid settings provided',
+ 'error-invalid-subscription': 'Invalid subscription',
+ 'error-invalid-token': 'Invalid token',
+ 'error-invalid-triggerWords': 'Invalid triggerWords',
+ 'error-invalid-urls': 'Invalid URLs',
+ 'error-invalid-user': 'Invalid user',
+ 'error-invalid-username': 'Invalid username',
+ 'error-invalid-webhook-response': 'The webhook URL responded with a status other than 200',
+ 'error-message-deleting-blocked': 'Message deleting is blocked',
+ 'error-message-editing-blocked': 'Message editing is blocked',
+ 'error-message-size-exceeded': 'Message size exceeds Message_MaxAllowedSize',
+ 'error-missing-unsubscribe-link': 'You must provide the [unsubscribe] link.',
+ 'error-no-tokens-for-this-user': 'There are no tokens for this user',
+ 'error-not-allowed': 'Not allowed',
+ 'error-not-authorized': 'Not authorized',
+ 'error-push-disabled': 'Push is disabled',
+ 'error-remove-last-owner': 'This is the last owner. Please set a new owner before removing this one.',
+ 'error-role-in-use': 'Cannot delete role because it\'s in use',
+ 'error-role-name-required': 'Role name is required',
+ 'error-the-field-is-required': 'The field {{field}} is required.',
+ 'error-too-many-requests': 'Error, too many requests. Please slow down. You must wait {{seconds}} seconds before trying again.',
+ 'error-user-is-not-activated': 'User is not activated',
+ 'error-user-has-no-roles': 'User has no roles',
+ 'error-user-limit-exceeded': 'The number of users you are trying to invite to #channel_name exceeds the limit set by the administrator',
+ 'error-user-not-in-room': 'User is not in this room',
+ 'error-user-registration-custom-field': 'error-user-registration-custom-field',
+ 'error-user-registration-disabled': 'User registration is disabled',
+ 'error-user-registration-secret': 'User registration is only allowed via Secret URL',
+ 'error-you-are-last-owner': 'You are the last owner. Please set new owner before leaving the room.',
Actions: 'Actions',
Add_Reaction: 'Add Reaction',
Add_Server: 'Add Server',
@@ -21,6 +95,8 @@ export default {
Are_you_sure_question_mark: 'Are you sure?',
Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
Authenticating: 'Authenticating',
+ Avatar_changed_successfully: 'Avatar changed successfully!',
+ Avatar_Url: 'Avatar URL',
Away: 'Away',
Block_user: 'Block user',
Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
@@ -30,6 +106,7 @@ export default {
Cancel_editing: 'Cancel editing',
Cancel_recording: 'Cancel recording',
Cancel: 'Cancel',
+ changing_avatar: 'changing avatar',
Channel_Name: 'Channel Name',
Chats: 'Chats',
Close_emoji_selector: 'Close emoji selector',
@@ -60,6 +137,7 @@ export default {
Everyone_can_access_this_channel: 'Everyone can access this channel',
Files: 'Files',
Finish_recording: 'Finish recording',
+ For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
Forgot_my_password: 'Forgot my password',
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot password',
@@ -71,6 +149,7 @@ export default {
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
is_typing: 'is typing',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
+ Language: 'Language',
last_message: 'last message',
Leave_channel: 'Leave channel',
leave: 'leave',
@@ -95,6 +174,7 @@ export default {
Name: 'Name',
New_in_RocketChat_question_mark: 'New in Rocket.Chat?',
New_Message: 'New Message',
+ New_Password: 'New Password',
New_Server: 'New Server',
No_files: 'No files',
No_mentioned_messages: 'No mentioned messages',
@@ -121,9 +201,12 @@ export default {
Pinned_Messages: 'Pinned Messages',
pinned: 'pinned',
Pinned: 'Pinned',
+ Please_enter_your_password: 'Please enter your password',
+ Preferences_saved: 'Preferences saved!',
Privacy_Policy: ' Privacy Policy',
Private_Channel: 'Private Channel',
Private: 'Private',
+ Profile_saved_successfully: 'Profile saved successfully!',
Profile: 'Profile',
Public_Channel: 'Public Channel',
Public: 'Public',
@@ -151,8 +234,14 @@ export default {
Room_Members: 'Room Members',
Room_name_changed: 'Room name changed to: {{name}} by {{userBy}}',
SAVE: 'SAVE',
+ Save_Changes: 'Save Changes',
+ Save: 'Save',
+ saving_preferences: 'saving preferences',
+ saving_profile: 'saving profile',
+ saving_settings: 'saving settings',
Search_Messages: 'Search Messages',
Search: 'Search',
+ Select_Avatar: 'Select Avatar',
Select_Users: 'Select Users',
Send_audio_message: 'Send audio message',
Send_message: 'Send message',
@@ -177,10 +266,11 @@ export default {
tap_to_change_status: 'tap to change status',
Tap_to_view_servers_list: 'Tap to view servers list',
Terms_of_Service: ' Terms of Service ',
- There_was_an_error_while_saving_settings: 'There was an error while saving settings!',
+ There_was_an_error_while_action: 'There was an error while {{action}}!',
This_room_is_blocked: 'This room is blocked',
This_room_is_read_only: 'This room is read only',
Timezone: 'Timezone',
+ Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic',
Topic: 'Topic',
Type_the_channel_name_here: 'Type the channel name here',
diff --git a/app/lib/ddp.js b/app/lib/ddp.js
index 33e856085..bf4dba72c 100644
--- a/app/lib/ddp.js
+++ b/app/lib/ddp.js
@@ -144,9 +144,11 @@ export default class Socket extends EventEmitter {
try {
this.emit('login', params);
const result = await this.call('login', params);
- this._login = { resume: result.token, ...result };
+ // this._login = { resume: result.token, ...result };
+ this._login = { resume: result.token, ...result, ...params };
this._logged = true;
- this.emit('logged', result);
+ // this.emit('logged', result);
+ this.emit('logged', this._login);
return result;
} catch (err) {
const error = { ...err };
diff --git a/app/lib/realm.js b/app/lib/realm.js
index 1d8aedde8..ee7b84640 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -106,11 +106,11 @@ const subscriptionSchema = {
const usersSchema = {
name: 'users',
- primaryKey: '_id',
+ primaryKey: 'username',
properties: {
- _id: 'string',
username: 'string',
- name: { type: 'string', optional: true }
+ name: { type: 'string', optional: true },
+ avatarVersion: { type: 'int', optional: true }
}
};
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index b01774a00..420768755 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -91,10 +91,7 @@ const RocketChat = {
this.activeUsers = this.activeUsers || {};
const { user } = reduxStore.getState().login;
- if (user && user.id === ddpMessage.id) {
- if (!ddpMessage.fields) {
- reduxStore.dispatch(setUser({ status: 'offline' }));
- }
+ if (ddpMessage.fields && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser(ddpMessage.fields));
}
@@ -107,9 +104,14 @@ const RocketChat = {
reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null;
return this.activeUsers = {};
- }, 1000);
+ }, 3000);
- this.activeUsers[ddpMessage.id] = ddpMessage.fields;
+ const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
+ if (!ddpMessage.fields) {
+ this.activeUsers[ddpMessage.id] = {};
+ } else {
+ this.activeUsers[ddpMessage.id] = { ...this.activeUsers[ddpMessage.id], ...activeUser, ...ddpMessage.fields };
+ }
},
async loginSuccess(user) {
try {
@@ -122,15 +124,11 @@ const RocketChat = {
// call /me only one time
if (!user.username) {
const me = await this.me({ token: user.token, userId: user.id });
- // eslint-disable-next-line
- user.username = me.username;
+ user = { ...user, ...me };
}
if (user.username) {
const userInfo = await this.userInfo({ token: user.token, userId: user.id });
- user.username = userInfo.user.username;
- if (userInfo.user.roles) {
- user.roles = userInfo.user.roles;
- }
+ user = { ...user, ...userInfo.user };
}
return reduxStore.dispatch(loginSuccess(user));
} catch (e) {
@@ -163,7 +161,10 @@ const RocketChat = {
this.getRooms().catch(e => log('logged getRooms', e));
this.loginSuccess(user);
}));
- this.ddp.once('logged', protectedFunction(({ id }) => { this.subscribeRooms(id); }));
+ this.ddp.once('logged', protectedFunction(({ id }) => {
+ this.subscribeRooms(id);
+ this.ddp.subscribe('stream-notify-logged', 'updateAvatar', false);
+ }));
this.ddp.on('disconnected', protectedFunction(() => {
reduxStore.dispatch(disconnect());
@@ -184,6 +185,24 @@ 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;
+ database.write(() => {
+ args.forEach((arg) => {
+ const user = database.objects('users').filtered('username = $0', arg.username);
+ if (!user.length) {
+ database.create('users', { username: arg.username, avatarVersion: 0 });
+ } else {
+ user[0].avatarVersion += 1;
+ }
+ });
+ });
+ }
+ });
+
// this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => {
// console.warn('rc.stream-notify-user')
// const [type, data] = ddpMessage.fields.args;
@@ -804,6 +823,12 @@ const RocketChat = {
saveRoomSettings(rid, params) {
return call('saveRoomSettings', rid, params);
},
+ saveUserProfile(params, customFields) {
+ return call('saveUserProfile', params, customFields);
+ },
+ saveUserPreferences(params) {
+ return call('saveUserPreferences', params);
+ },
saveNotificationSettings(rid, param, value) {
return call('saveNotificationSettings', rid, param, value);
},
@@ -836,6 +861,15 @@ const RocketChat = {
.some(item => mergedRoles.indexOf(item) !== -1);
return result;
}, {});
+ },
+ getAvatarSuggestion() {
+ return call('getAvatarSuggestion');
+ },
+ resetAvatar() {
+ return call('resetAvatar');
+ },
+ setAvatarFromService({ data, contentType = '', service = null }) {
+ return call('setAvatarFromService', data, contentType, service);
}
};
diff --git a/app/sagas/init.js b/app/sagas/init.js
index b7227e3fd..d911923af 100644
--- a/app/sagas/init.js
+++ b/app/sagas/init.js
@@ -20,8 +20,8 @@ const restore = function* restore() {
yield put(setServer(currentServer));
const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
- if (login && login.user) {
- yield put(setUser(login.user));
+ if (login) {
+ yield put(setUser(JSON.parse(login)));
}
}
diff --git a/app/sagas/login.js b/app/sagas/login.js
index 92602eb41..495b4ed5b 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -20,6 +20,7 @@ import {
import RocketChat from '../lib/rocketchat';
import * as NavigationService from '../containers/routes/NavigationService';
import log from '../utils/log';
+import I18n from '../i18n';
const getUser = state => state.login;
const getServer = state => state.server.server;
@@ -170,6 +171,15 @@ const watchLoginOpen = function* watchLoginOpen() {
}
};
+// eslint-disable-next-line require-yield
+const handleSetUser = function* handleSetUser(params) {
+ const [server, user] = yield all([select(getServer), select(getUser)]);
+ if (params.language) {
+ I18n.locale = params.language;
+ }
+ yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user));
+};
+
const root = function* root() {
// yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
// yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
@@ -184,5 +194,6 @@ const root = function* root() {
yield takeLatest(types.LOGOUT, handleLogout);
yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
yield takeLatest(types.LOGIN.OPEN, watchLoginOpen);
+ yield takeLatest(types.USER.SET, handleSetUser);
};
export default root;
diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js
index 09d317095..77d4666d4 100644
--- a/app/views/ForgotPasswordView.js
+++ b/app/views/ForgotPasswordView.js
@@ -78,29 +78,27 @@ export default class ForgotPasswordView extends LoggedView {
-
- this.validate(email)}
- onSubmitEditing={() => this.resetPassword()}
- testID='forgot-password-view-email'
+ this.validate(email)}
+ onSubmitEditing={() => this.resetPassword()}
+ testID='forgot-password-view-email'
+ />
+
+
+
-
-
-
-
-
- {this.props.login.failure && {this.props.login.error.reason}}
+
+ {this.props.login.failure ? {this.props.login.error.reason} : null}
diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js
index 2cd391dcc..c9a55546d 100644
--- a/app/views/LoginSignupView.js
+++ b/app/views/LoginSignupView.js
@@ -209,61 +209,68 @@ export default class LoginSignupView extends LoggedView {
{I18n.t('Or_continue_using_social_accounts')}
- {this.props.Accounts_OAuth_Facebook && this.props.services.facebook &&
+ {this.props.Accounts_OAuth_Facebook && this.props.services.facebook ?
+ : null
}
- {this.props.Accounts_OAuth_Github && this.props.services.github &&
+ {this.props.Accounts_OAuth_Github && this.props.services.github ?
+ : null
}
- {this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab &&
+ {this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ?
+ : null
}
- {this.props.Accounts_OAuth_Google && this.props.services.google &&
+ {this.props.Accounts_OAuth_Google && this.props.services.google ?
+ : null
}
- {this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin &&
+ {this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ?
+ : null
}
- {this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] &&
+ {this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ?
+ : null
}
- {this.props.Accounts_OAuth_Twitter && this.props.services.twitter &&
+ {this.props.Accounts_OAuth_Twitter && this.props.services.twitter ?
+ : null
}
diff --git a/app/views/LoginView.js b/app/views/LoginView.js
index 25b29853d..86d11b69b 100644
--- a/app/views/LoginView.js
+++ b/app/views/LoginView.js
@@ -135,7 +135,7 @@ export default class LoginView extends LoggedView {
- {this.props.failure && {this.props.reason}}
+ {this.props.failure ? {this.props.reason} : null}
diff --git a/app/views/MentionedMessagesView/index.js b/app/views/MentionedMessagesView/index.js
index 226153cb1..7a8b5c479 100644
--- a/app/views/MentionedMessagesView/index.js
+++ b/app/views/MentionedMessagesView/index.js
@@ -109,8 +109,8 @@ export default class MentionedMessagesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={loading && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={loading ? : null}
+ ListFooterComponent={loadingMore ? : null}
/>
]
);
diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js
index 107be23b0..47befc457 100644
--- a/app/views/PinnedMessagesView/index.js
+++ b/app/views/PinnedMessagesView/index.js
@@ -133,8 +133,8 @@ export default class PinnedMessagesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={loading && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={loading ? : null}
+ ListFooterComponent={loadingMore ? : null}
/>,
({
+ user: state.login.user,
+ Accounts_CustomFields: state.settings.Accounts_CustomFields
+}))
export default class ProfileView extends LoggedView {
+ static propTypes = {
+ navigation: PropTypes.object,
+ user: PropTypes.object,
+ Accounts_CustomFields: PropTypes.string
+ };
+
constructor(props) {
super('ProfileView', props);
+ this.state = {
+ showPasswordAlert: false,
+ saving: false,
+ name: null,
+ username: null,
+ email: null,
+ newPassword: null,
+ typedPassword: null,
+ avatarUrl: null,
+ avatar: {},
+ avatarSuggestions: {},
+ customFields: {}
+ };
+ }
+
+ async componentDidMount() {
+ this.init();
+
+ try {
+ const result = await RocketChat.getAvatarSuggestion();
+ this.setState({ avatarSuggestions: result });
+ } catch (e) {
+ log('getAvatarSuggestion', e);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.user !== nextProps.user) {
+ this.init(nextProps.user);
+ }
+ }
+
+ init = (user) => {
+ const {
+ name, username, emails, customFields
+ } = user || this.props.user;
+ this.setState({
+ name,
+ username,
+ email: emails ? emails[0].address : null,
+ newPassword: null,
+ typedPassword: null,
+ avatarUrl: null,
+ avatar: {},
+ customFields: customFields || {}
+ });
+ }
+
+ formIsChanged = () => {
+ const {
+ name, username, email, newPassword, avatar, customFields
+ } = this.state;
+ const { user } = this.props;
+ let customFieldsChanged = false;
+
+ const customFieldsKeys = Object.keys(customFields);
+ if (customFieldsKeys.length) {
+ customFieldsKeys.forEach((key) => {
+ if (user.customFields[key] !== customFields[key]) {
+ customFieldsChanged = true;
+ }
+ });
+ }
+
+ return !(user.name === name &&
+ user.username === username &&
+ !newPassword &&
+ (user.emails && user.emails[0].address === email) &&
+ !avatar.data &&
+ !customFieldsChanged
+ );
+ }
+
+ closePasswordAlert = () => {
+ this.setState({ showPasswordAlert: false });
+ }
+
+ handleError = (e, func, action) => {
+ if (e && e.error && e.error !== 500) {
+ if (e.details && e.details.timeToReset) {
+ return showErrorAlert(I18n.t('error-too-many-requests', {
+ seconds: parseInt(e.details.timeToReset / 1000, 10)
+ }));
+ }
+ return showErrorAlert(I18n.t(e.error, e.details));
+ }
+ showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
+ log(func, e);
+ }
+
+ submit = async() => {
+ Keyboard.dismiss();
+
+ if (!this.formIsChanged()) {
+ return;
+ }
+
+ this.setState({ saving: true, showPasswordAlert: false });
+
+ const {
+ name, username, email, newPassword, typedPassword, avatar, customFields
+ } = this.state;
+ const { user } = this.props;
+ const params = {};
+
+ // Name
+ if (user.name !== name) {
+ params.realname = name;
+ }
+
+ // Username
+ if (user.username !== username) {
+ params.username = username;
+ }
+
+ // Email
+ if (user.emails && user.emails[0].address !== email) {
+ params.email = email;
+ }
+
+ // newPassword
+ if (newPassword) {
+ params.newPassword = newPassword;
+ }
+
+ // typedPassword
+ if (typedPassword) {
+ params.typedPassword = SHA256(typedPassword);
+ }
+
+ const requirePassword = !!params.email || newPassword;
+ if (requirePassword && !params.typedPassword) {
+ return this.setState({ showPasswordAlert: true, saving: false });
+ }
+
+ try {
+ if (avatar.url) {
+ try {
+ await RocketChat.setAvatarFromService(avatar);
+ } catch (e) {
+ this.setState({ saving: false, typedPassword: null });
+ return setTimeout(() => this.handleError(e, 'setAvatarFromService', 'changing_avatar'), 300);
+ }
+ }
+
+ await RocketChat.saveUserProfile(params, customFields);
+ this.setState({ saving: false });
+ setTimeout(() => {
+ showToast(I18n.t('Profile_saved_successfully'));
+ this.init();
+ }, 300);
+ } catch (e) {
+ this.setState({ saving: false, typedPassword: null });
+ setTimeout(() => {
+ this.handleError(e, 'saveUserProfile', 'saving_profile');
+ }, 300);
+ }
+ }
+
+ setAvatar = (avatar) => {
+ this.setState({ avatar });
+ }
+
+ resetAvatar = async() => {
+ try {
+ await RocketChat.resetAvatar();
+ showToast(I18n.t('Avatar_changed_successfully'));
+ this.init();
+ } catch (e) {
+ this.handleError(e, 'resetAvatar', 'changing_avatar');
+ }
+ }
+
+ pickImage = () => {
+ const options = {
+ title: I18n.t('Select_Avatar')
+ };
+ ImagePicker.showImagePicker(options, async(response) => {
+ if (response.didCancel) {
+ console.warn('User cancelled image picker');
+ } else if (response.error) {
+ log('ImagePicker Error', response.error);
+ } else {
+ this.setAvatar({ url: response.uri, data: `data:image/jpeg;base64,${ response.data }`, service: 'upload' });
+ }
+ });
+ }
+
+ renderAvatarButton = ({
+ key, child, onPress, disabled = false
+ }) => (
+
+
+ {child}
+
+
+ )
+
+ renderAvatarButtons = () => (
+
+ {this.renderAvatarButton({
+ child: ,
+ onPress: () => this.resetAvatar(),
+ key: 'profile-view-reset-avatar'
+ })}
+ {this.renderAvatarButton({
+ child: ,
+ onPress: () => this.pickImage(),
+ key: 'profile-view-upload-avatar'
+ })}
+ {this.renderAvatarButton({
+ child: ,
+ onPress: () => this.setAvatar({ url: this.state.avatarUrl, data: this.state.avatarUrl, service: 'url' }),
+ disabled: !this.state.avatarUrl,
+ key: 'profile-view-avatar-url-button'
+ })}
+ {Object.keys(this.state.avatarSuggestions).map((service) => {
+ const { url, blob, contentType } = this.state.avatarSuggestions[service];
+ return this.renderAvatarButton({
+ key: `profile-view-avatar-${ service }`,
+ child: ,
+ onPress: () => this.setAvatar({
+ url, data: blob, service, contentType
+ })
+ });
+ })}
+
+ );
+
+ renderCustomFields = () => {
+ const { customFields } = this.state;
+ if (!this.props.Accounts_CustomFields) {
+ return null;
+ }
+ const parsedCustomFields = JSON.parse(this.props.Accounts_CustomFields);
+ return Object.keys(parsedCustomFields).map((key, index, array) => {
+ if (parsedCustomFields[key].type === 'select') {
+ const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option }));
+ return (
+ {
+ const newValue = {};
+ newValue[key] = value;
+ this.setState({ customFields: { ...this.state.customFields, ...newValue } });
+ }}
+ value={customFields[key]}
+ >
+ { this[key] = e; }}
+ label={key}
+ placeholder={key}
+ value={customFields[key]}
+ testID='settings-view-language'
+ />
+
+ );
+ }
+
+ return (
+ { this[key] = e; }}
+ key={key}
+ label={key}
+ placeholder={key}
+ value={customFields[key]}
+ onChangeText={(value) => {
+ const newValue = {};
+ newValue[key] = value;
+ this.setState({ customFields: { ...this.state.customFields, ...newValue } });
+ }}
+ onSubmitEditing={() => {
+ if (array.length - 1 > index) {
+ return this[array[index + 1]].focus();
+ }
+ this.avatarUrl.focus();
+ }}
+ />
+ );
+ });
}
render() {
+ const {
+ name, username, email, newPassword, avatarUrl, customFields
+ } = this.state;
return (
-
- ProfileView
-
+
+
+
+
+
+
+ { this.name = e; }}
+ label={I18n.t('Name')}
+ placeholder={I18n.t('Name')}
+ value={name}
+ onChangeText={value => this.setState({ name: value })}
+ onSubmitEditing={() => { this.username.focus(); }}
+ testID='profile-view-name'
+ />
+ { this.username = e; }}
+ label={I18n.t('Username')}
+ placeholder={I18n.t('Username')}
+ value={username}
+ onChangeText={value => this.setState({ username: value })}
+ onSubmitEditing={() => { this.email.focus(); }}
+ testID='profile-view-username'
+ />
+ { this.email = e; }}
+ label={I18n.t('Email')}
+ placeholder={I18n.t('Email')}
+ value={email}
+ onChangeText={value => this.setState({ email: value })}
+ onSubmitEditing={() => { this.newPassword.focus(); }}
+ testID='profile-view-email'
+ />
+ { this.newPassword = e; }}
+ label={I18n.t('New_Password')}
+ placeholder={I18n.t('New_Password')}
+ value={newPassword}
+ onChangeText={value => this.setState({ newPassword: value })}
+ onSubmitEditing={() => {
+ if (Object.keys(customFields).length) {
+ return this[Object.keys(customFields)[0]].focus();
+ }
+ this.avatarUrl.focus();
+ }}
+ secureTextEntry
+ testID='profile-view-new-password'
+ />
+ {this.renderCustomFields()}
+ { this.avatarUrl = e; }}
+ label={I18n.t('Avatar_Url')}
+ placeholder={I18n.t('Avatar_Url')}
+ value={avatarUrl}
+ onChangeText={value => this.setState({ avatarUrl: value })}
+ onSubmitEditing={this.submit}
+ testID='profile-view-avatar-url'
+ />
+ {this.renderAvatarButtons()}
+
+
+
+
+
+
+ {I18n.t('Please_enter_your_password')}
+
+
+ {I18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
+
+ this.setState({ typedPassword: value })}
+ secureTextEntry
+ testID='profile-view-typed-password'
+ />
+
+
+
+
+
+
);
}
}
diff --git a/app/views/ProfileView/styles.js b/app/views/ProfileView/styles.js
new file mode 100644
index 000000000..3ac375c40
--- /dev/null
+++ b/app/views/ProfileView/styles.js
@@ -0,0 +1,24 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ avatarContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 10
+ },
+ avatarButtons: {
+ flexWrap: 'wrap',
+ flexDirection: 'row',
+ justifyContent: 'flex-start'
+ },
+ avatarButton: {
+ backgroundColor: '#e1e5e8',
+ width: 50,
+ height: 50,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 15,
+ marginBottom: 15,
+ borderRadius: 2
+ }
+});
diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js
index caeb72613..db3193b4a 100644
--- a/app/views/RegisterView.js
+++ b/app/views/RegisterView.js
@@ -207,10 +207,11 @@ export default class RegisterView extends LoggedView {
{I18n.t('Sign_Up')}
{this._renderRegister()}
{this._renderUsername()}
- {this.props.login.failure &&
+ {this.props.login.failure ?
{this.props.login.error.reason}
+ : null
}
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 52aef0c93..c921ea310 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -381,7 +381,7 @@ export default class RoomActionsView extends LoggedView {
] : [
,
{ item.name },
- item.description && { item.description },
+ item.description ? { item.description } : null,
];
return this.renderTouchableItem(subview, item);
diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js
index 175c04bd3..1d4717f89 100644
--- a/app/views/RoomActionsView/styles.js
+++ b/app/views/RoomActionsView/styles.js
@@ -4,13 +4,6 @@ export default StyleSheet.create({
container: {
backgroundColor: '#F6F7F9'
},
- headerButton: {
- backgroundColor: 'transparent',
- height: 44,
- width: 44,
- alignItems: 'center',
- justifyContent: 'center'
- },
sectionItem: {
backgroundColor: '#ffffff',
paddingVertical: 16,
diff --git a/app/views/RoomFilesView/index.js b/app/views/RoomFilesView/index.js
index 8a4941cbd..8028275e5 100644
--- a/app/views/RoomFilesView/index.js
+++ b/app/views/RoomFilesView/index.js
@@ -107,8 +107,8 @@ export default class RoomFilesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={loading && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={loading ? : null}
+ ListFooterComponent={loadingMore ? : null}
/>
]
);
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index 95effd731..58b74f350 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -190,7 +190,7 @@ export default class RoomInfoEditView extends LoggedView {
await this.setState({ saving: false });
setTimeout(() => {
if (error) {
- showErrorAlert(I18n.t('There_was_an_error_while_saving_settings'));
+ showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_settings') }));
} else {
showToast(I18n.t('Settings_succesfully_changed'));
}
@@ -266,133 +266,133 @@ export default class RoomInfoEditView extends LoggedView {
{...scrollPersistTaps}
>
-
- { this.name = e; }}
- label={I18n.t('Name')}
- value={name}
- onChangeText={value => this.setState({ name: value })}
- onSubmitEditing={() => { this.description.focus(); }}
- error={nameError}
- testID='room-info-edit-view-name'
- />
- { this.description = e; }}
- label={I18n.t('Description')}
- value={description}
- onChangeText={value => this.setState({ description: value })}
- onSubmitEditing={() => { this.topic.focus(); }}
- testID='room-info-edit-view-description'
- />
- { this.topic = e; }}
- label={I18n.t('Topic')}
- value={topic}
- onChangeText={value => this.setState({ topic: value })}
- onSubmitEditing={() => { this.announcement.focus(); }}
- testID='room-info-edit-view-topic'
- />
- { this.announcement = e; }}
- label={I18n.t('Announcement')}
- value={announcement}
- onChangeText={value => this.setState({ announcement: value })}
- onSubmitEditing={() => { this.joinCode.focus(); }}
- testID='room-info-edit-view-announcement'
- />
- { this.joinCode = e; }}
- label={I18n.t('Password')}
- value={joinCode}
- onChangeText={value => this.setState({ joinCode: value })}
- onSubmitEditing={this.submit}
- secureTextEntry
- testID='room-info-edit-view-password'
- />
+ { this.name = e; }}
+ label={I18n.t('Name')}
+ value={name}
+ onChangeText={value => this.setState({ name: value })}
+ onSubmitEditing={() => { this.description.focus(); }}
+ error={nameError}
+ testID='room-info-edit-view-name'
+ />
+ { this.description = e; }}
+ label={I18n.t('Description')}
+ value={description}
+ onChangeText={value => this.setState({ description: value })}
+ onSubmitEditing={() => { this.topic.focus(); }}
+ testID='room-info-edit-view-description'
+ />
+ { this.topic = e; }}
+ label={I18n.t('Topic')}
+ value={topic}
+ onChangeText={value => this.setState({ topic: value })}
+ onSubmitEditing={() => { this.announcement.focus(); }}
+ testID='room-info-edit-view-topic'
+ />
+ { this.announcement = e; }}
+ label={I18n.t('Announcement')}
+ value={announcement}
+ onChangeText={value => this.setState({ announcement: value })}
+ onSubmitEditing={() => { this.joinCode.focus(); }}
+ testID='room-info-edit-view-announcement'
+ />
+ { this.joinCode = e; }}
+ label={I18n.t('Password')}
+ value={joinCode}
+ onChangeText={value => this.setState({ joinCode: value })}
+ onSubmitEditing={this.submit}
+ secureTextEntry
+ testID='room-info-edit-view-password'
+ />
+ this.setState({ t: value })}
+ testID='room-info-edit-view-t'
+ />
+ this.setState({ ro: value })}
+ disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
+ testID='room-info-edit-view-ro'
+ />
+ {ro && !room.broadcast ?
this.setState({ t: value })}
- testID='room-info-edit-view-t'
+ value={reactWhenReadOnly}
+ leftLabelPrimary={I18n.t('No_Reactions')}
+ leftLabelSecondary={I18n.t('Reactions_are_disabled')}
+ rightLabelPrimary={I18n.t('Allow_Reactions')}
+ rightLabelSecondary={I18n.t('Reactions_are_enabled')}
+ onValueChange={value => this.setState({ reactWhenReadOnly: value })}
+ disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
+ testID='room-info-edit-view-react-when-ro'
/>
- this.setState({ ro: value })}
- disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
- testID='room-info-edit-view-ro'
- />
- {ro && !room.broadcast &&
- this.setState({ reactWhenReadOnly: value })}
- disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
- testID='room-info-edit-view-react-when-ro'
- />
- }
- {room.broadcast &&
- [
- {I18n.t('Broadcast_Channel')},
-
- ]
- }
+ : null
+ }
+ {room.broadcast ?
+ [
+ {I18n.t('Broadcast_Channel')},
+
+ ]
+ : null
+ }
+
+ {I18n.t('SAVE')}
+
+
- {I18n.t('SAVE')}
+ {I18n.t('RESET')}
-
-
- {I18n.t('RESET')}
-
-
-
- { room.archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
-
-
-
-
- {I18n.t('DELETE')}
+
+ { room.archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
+
+
+
+ {I18n.t('DELETE')}
+
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 68209a655..0047354d9 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -61,7 +61,7 @@ export default class RoomInfoView extends LoggedView {
accessibilityTraits='button'
testID='room-info-view-edit-button'
>
-
+
@@ -146,17 +146,18 @@ export default class RoomInfoView extends LoggedView {
);
renderRoles = () => (
- this.state.roles.length > 0 &&
-
- {I18n.t('Roles')}
-
- {this.state.roles.map(role => (
-
- { this.props.roles[role] }
-
- ))}
+ this.state.roles.length > 0 ?
+
+ {I18n.t('Roles')}
+
+ {this.state.roles.map(role => (
+
+ { this.props.roles[role] }
+
+ ))}
+
-
+ : null
)
renderTimezone = (userId) => {
@@ -210,12 +211,12 @@ export default class RoomInfoView extends LoggedView {
{this.renderAvatar(room, roomUser)}
{ getRoomTitle(room) }
- {!this.isDirect() && this.renderItem('description', room)}
- {!this.isDirect() && this.renderItem('topic', room)}
- {!this.isDirect() && this.renderItem('announcement', room)}
- {this.isDirect() && this.renderRoles()}
- {this.isDirect() && this.renderTimezone(roomUser._id)}
- {room.broadcast && this.renderBroadcast()}
+ {!this.isDirect() ? this.renderItem('description', room) : null}
+ {!this.isDirect() ? this.renderItem('topic', room) : null}
+ {!this.isDirect() ? this.renderItem('announcement', room) : null}
+ {this.isDirect() ? this.renderRoles() : null}
+ {this.isDirect() ? this.renderTimezone(roomUser._id) : null}
+ {room.broadcast ? this.renderBroadcast() : null}
);
}
diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js
index fe9c0af71..70c44dc17 100644
--- a/app/views/RoomInfoView/styles.js
+++ b/app/views/RoomInfoView/styles.js
@@ -7,13 +7,6 @@ export default StyleSheet.create({
backgroundColor: '#ffffff',
padding: 10
},
- headerButton: {
- backgroundColor: 'transparent',
- height: 44,
- width: 44,
- alignItems: 'center',
- justifyContent: 'center'
- },
item: {
padding: 10,
// borderColor: '#EBEDF1',
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 68fc2d0cf..dcadebf56 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { FlatList, Text, View, TextInput, Vibration } from 'react-native';
+import { FlatList, Text, View, TextInput, Vibration, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import LoggedView from '../View';
import styles from './styles';
+import sharedStyles from '../Styles';
import RoomItem from '../../presentation/RoomItem';
-import Touch from '../../utils/touch';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import RocketChat from '../../lib/rocketchat';
import { goRoom } from '../../containers/routes/NavigationService';
@@ -33,19 +33,15 @@ export default class MentionedMessagesView extends LoggedView {
}
return {
headerRight: (
-
-
- {label}
-
-
+ {label}
+
)
};
};
diff --git a/app/views/RoomMembersView/styles.js b/app/views/RoomMembersView/styles.js
index 12faa2c29..07e5c1e5a 100644
--- a/app/views/RoomMembersView/styles.js
+++ b/app/views/RoomMembersView/styles.js
@@ -31,18 +31,6 @@ export default StyleSheet.create({
fontSize: 16,
color: '#444'
},
- headerButtonTouchable: {
- borderRadius: 4
- },
- headerButton: {
- padding: 6,
- backgroundColor: 'transparent',
- alignItems: 'center',
- justifyContent: 'center'
- },
- headerButtonText: {
- color: '#292E35'
- },
searchBoxView: {
backgroundColor: '#eee'
},
@@ -53,5 +41,9 @@ export default StyleSheet.create({
padding: 5,
paddingLeft: 10,
color: '#aaa'
+ },
+ headerButton: {
+ marginRight: 9,
+ alignItems: 'flex-end'
}
});
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 275150aad..2cf249c17 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -14,6 +14,7 @@ import { closeRoom } from '../../../actions/room';
import log from '../../../utils/log';
import RoomTypeIcon from '../../../containers/RoomTypeIcon';
import I18n from '../../../i18n';
+import sharedStyles from '../../Styles';
const title = (offline, connecting, authenticating, logged) => {
if (offline) {
@@ -159,7 +160,7 @@ export default class RoomHeaderView extends React.PureComponent {
- { t && {t}}
+ { t ? {t} : null}
@@ -169,7 +170,7 @@ export default class RoomHeaderView extends React.PureComponent {
renderRight = () => (
{
try {
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
@@ -189,7 +190,7 @@ export default class RoomHeaderView extends React.PureComponent {
/>
this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
accessibilityLabel={I18n.t('Room_actions')}
accessibilityTraits='button'
diff --git a/app/views/RoomView/Header/styles.js b/app/views/RoomView/Header/styles.js
index 56cbd186c..1be2af4f5 100644
--- a/app/views/RoomView/Header/styles.js
+++ b/app/views/RoomView/Header/styles.js
@@ -40,13 +40,6 @@ export default StyleSheet.create({
right: {
flexDirection: 'row'
},
- headerButton: {
- backgroundColor: 'transparent',
- height: 44,
- width: 40,
- alignItems: 'center',
- justifyContent: 'center'
- },
avatar: {
marginRight: 5
}
diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js
index a9036a6ef..95443cfca 100644
--- a/app/views/RoomView/ListView.js
+++ b/app/views/RoomView/ListView.js
@@ -113,8 +113,8 @@ export class ListView extends OldList2 {
// const { renderSectionHeader } = this.props;
- const header = this.props.renderHeader && this.props.renderHeader();
- const footer = this.props.renderFooter && this.props.renderFooter();
+ const header = this.props.renderHeader ? this.props.renderHeader() : null;
+ const footer = this.props.renderFooter ? this.props.renderFooter() : null;
// let totalIndex = header ? 1 : 0;
const { data } = this.props;
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 28cfadb00..db86edd13 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -95,8 +95,12 @@ export default class RoomView extends LoggedView {
}
requestAnimationFrame(async() => {
- const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
- this.setState({ end: result < 20 });
+ try {
+ const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
+ this.setState({ end: result < 20 });
+ } catch (e) {
+ log('RoomView.onEndReached', e);
+ }
});
}
diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js
index a225b7d70..f9272ac2b 100644
--- a/app/views/RoomsListView/Header/index.js
+++ b/app/views/RoomsListView/Header/index.js
@@ -13,6 +13,7 @@ import RocketChat from '../../../lib/rocketchat';
import { STATUS_COLORS } from '../../../constants/colors';
import { setSearch } from '../../../actions/rooms';
import styles from './styles';
+import sharedStyles from '../../Styles';
import log from '../../../utils/log';
import I18n from '../../../i18n';
@@ -130,7 +131,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
testID='rooms-list-view-sidebar'
>
this.props.navigation.openDrawer()}
>
{this.props.user.username}
- { t && {t}}
+ { t ? {t} : null}
);
@@ -189,7 +190,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
{Platform.OS === 'android' ?
this.onPressSearchButton()}
accessibilityLabel={I18n.t('Search')}
accessibilityTraits='button'
@@ -203,7 +204,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
: null}
{Platform.OS === 'ios' ?
this.createChannel()}
accessibilityLabel={I18n.t('Create_Channel')}
accessibilityTraits='button'
diff --git a/app/views/RoomsListView/Header/styles.js b/app/views/RoomsListView/Header/styles.js
index 1a4e2f587..282bbe58d 100644
--- a/app/views/RoomsListView/Header/styles.js
+++ b/app/views/RoomsListView/Header/styles.js
@@ -55,13 +55,6 @@ export default StyleSheet.create({
borderBottomColor: 'rgba(0, 0, 0, .3)',
paddingHorizontal: 20
},
- headerButton: {
- backgroundColor: 'transparent',
- height: 44,
- width: 44,
- alignItems: 'center',
- justifyContent: 'center'
- },
user_status: {
position: 'absolute',
bottom: -2,
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index 929e0cfb9..444ebee3b 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -223,6 +223,6 @@ export default class RoomsListView extends LoggedView {
render = () => (
{this.renderList()}
- {Platform.OS === 'android' && this.renderCreateButtons()}
+ {Platform.OS === 'android' ? this.renderCreateButtons() : null}
)
}
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index 4aa45da26..db6bc7162 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -129,8 +129,8 @@ export default class SearchMessagesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={searching && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={searching ? : null}
+ ListFooterComponent={loadingMore ? : null}
{...scrollPersistTaps}
/>
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
index c25511c71..8c1bac4dd 100644
--- a/app/views/SettingsView/index.js
+++ b/app/views/SettingsView/index.js
@@ -1,18 +1,134 @@
import React from 'react';
-import { Text, View } from 'react-native';
+import PropTypes from 'prop-types';
+import { View, ScrollView, SafeAreaView } from 'react-native';
+import RNPickerSelect from 'react-native-picker-select';
+import { connect } from 'react-redux';
import LoggedView from '../View';
+import RocketChat from '../../lib/rocketchat';
+import KeyboardView from '../../presentation/KeyboardView';
+import sharedStyles from '../Styles';
+import RCTextInput from '../../containers/TextInput';
+import scrollPersistTaps from '../../utils/scrollPersistTaps';
+import I18n from '../../i18n';
+import Button from '../../containers/Button';
+import Loading from '../../containers/Loading';
+import { showErrorAlert, showToast } from '../../utils/info';
+import log from '../../utils/log';
+import { setUser } from '../../actions/login';
+@connect(state => ({
+ user: state.login.user
+}), dispatch => ({
+ setUser: params => dispatch(setUser(params))
+}))
export default class SettingsView extends LoggedView {
+ static propTypes = {
+ user: PropTypes.object,
+ setUser: PropTypes.func
+ };
+
constructor(props) {
super('SettingsView', props);
+ this.state = {
+ placeholder: {},
+ language: props.user ? props.user.language : 'en',
+ languages: [{
+ label: 'English',
+ value: 'en'
+ }],
+ saving: false
+ };
+ }
+
+ formIsChanged = () => {
+ const { language } = this.state;
+ const { user } = this.props;
+ return !(user.language === language);
+ }
+
+ submit = async() => {
+ this.setState({ saving: true });
+
+ const {
+ language
+ } = this.state;
+ const { user } = this.props;
+
+ if (!this.formIsChanged()) {
+ return;
+ }
+
+ const params = {};
+
+ // language
+ if (user.language !== language) {
+ params.language = language;
+ }
+
+ try {
+ await RocketChat.saveUserPreferences(params);
+ this.props.setUser({ language: params.language });
+ this.props.navigation.setParams({ title: I18n.t('Settings') });
+
+ this.setState({ saving: false });
+ setTimeout(() => {
+ showToast(I18n.t('Preferences_saved'));
+ }, 300);
+ } catch (e) {
+ this.setState({ saving: false });
+ setTimeout(() => {
+ if (e && e.error) {
+ return showErrorAlert(I18n.t(e.error, e.details));
+ }
+ showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
+ log('saveUserPreferences', e);
+ }, 300);
+ }
}
render() {
+ const { language, languages, placeholder } = this.state;
return (
-
- SettingsView
-
+
+
+
+ {
+ this.setState({ language: value });
+ }}
+ value={language}
+ placeholder={placeholder}
+ >
+ { this.name = e; }}
+ label={I18n.t('Language')}
+ placeholder={I18n.t('Language')}
+ value={language}
+ testID='settings-view-language'
+ />
+
+
+
+
+
+
+
+
);
}
}
diff --git a/app/views/SnippetedMessagesView/index.js b/app/views/SnippetedMessagesView/index.js
index f1b240023..7b0f426dd 100644
--- a/app/views/SnippetedMessagesView/index.js
+++ b/app/views/SnippetedMessagesView/index.js
@@ -109,8 +109,8 @@ export default class SnippetedMessagesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={loading && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={loading ? : null}
+ ListFooterComponent={loadingMore ? : null}
/>
]
);
diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js
index 6feed3c3b..ce468bc89 100644
--- a/app/views/StarredMessagesView/index.js
+++ b/app/views/StarredMessagesView/index.js
@@ -133,8 +133,8 @@ export default class StarredMessagesView extends LoggedView {
style={styles.list}
keyExtractor={item => item._id}
onEndReached={this.moreData}
- ListHeaderComponent={loading && }
- ListFooterComponent={loadingMore && }
+ ListHeaderComponent={loading ? : null}
+ ListFooterComponent={loadingMore ? : null}
/>,
{
@@ -99,4 +99,13 @@ describe('Broadcast room', () => {
afterEach(async() => {
takeScreenshot();
});
+
+ after(async() => {
+ // log back as main test user and left screen on RoomsListView
+ await element(by.id('header-back')).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await logout();
+ await navigateToLogin();
+ await login();
+ })
});
diff --git a/e2e/12-profile.spec.js b/e2e/12-profile.spec.js
new file mode 100644
index 000000000..ecf2ff107
--- /dev/null
+++ b/e2e/12-profile.spec.js
@@ -0,0 +1,115 @@
+const {
+ device, expect, element, by, waitFor
+} = require('detox');
+const { takeScreenshot } = require('./helpers/screenshot');
+const { logout, navigateToLogin, login } = require('./helpers/app');
+const data = require('./data');
+
+const scrollDown = 200;
+
+describe('Profile screen', () => {
+ before(async() => {
+ await element(by.id('rooms-list-view-sidebar')).tap();
+ await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('sidebar-profile'))).toBeVisible().withTimeout(2000);
+ await expect(element(by.id('sidebar-profile'))).toBeVisible();
+ await element(by.id('sidebar-profile')).tap();
+ await waitFor(element(by.id('profile-view'))).toBeVisible().withTimeout(2000);
+
+ });
+
+ describe('Render', async() => {
+ it('should have profile view', async() => {
+ await expect(element(by.id('profile-view'))).toBeVisible();
+ });
+
+ it('should have avatar', async() => {
+ await expect(element(by.id('profile-view-avatar')).atIndex(0)).toBeVisible();
+ });
+
+ it('should have name', async() => {
+ await expect(element(by.id('profile-view-name'))).toBeVisible();
+ });
+
+ it('should have username', async() => {
+ await expect(element(by.id('profile-view-username'))).toBeVisible();
+ });
+
+ it('should have email', async() => {
+ await expect(element(by.id('profile-view-email'))).toExist();
+ });
+
+ it('should have new password', async() => {
+ await expect(element(by.id('profile-view-new-password'))).toBeVisible();
+ });
+
+ it('should have avatar url', async() => {
+ await expect(element(by.id('profile-view-avatar-url'))).toBeVisible();
+ });
+
+ it('should have reset avatar button', async() => {
+ await waitFor(element(by.id('profile-view-reset-avatar'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+ await expect(element(by.id('profile-view-reset-avatar'))).toBeVisible();
+ });
+
+ it('should have upload avatar button', async() => {
+ await waitFor(element(by.id('profile-view-upload-avatar'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+ await expect(element(by.id('profile-view-upload-avatar'))).toBeVisible();
+ });
+
+ it('should have avatar url button', async() => {
+ await waitFor(element(by.id('profile-view-avatar-url-button'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+ await expect(element(by.id('profile-view-avatar-url-button'))).toBeVisible();
+ });
+
+ it('should have submit button', async() => {
+ await waitFor(element(by.id('profile-view-submit'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+ await expect(element(by.id('profile-view-submit'))).toBeVisible();
+ });
+
+ after(async() => {
+ takeScreenshot();
+ });
+ });
+
+ describe('Usage', async() => {
+ it('should change name and username', async() => {
+ await element(by.id('profile-view-list')).swipe('down');
+ await element(by.id('profile-view-name')).replaceText(`${ data.user }new`);
+ await element(by.id('profile-view-username')).replaceText(`${ data.user }new`);
+ await element(by.id('profile-view-list')).swipe('up');
+ await element(by.id('profile-view-submit')).tap();
+ await waitFor(element(by.text('Profile saved successfully!'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text('Profile saved successfully!'))).toBeVisible();
+ await waitFor(element(by.text('Profile saved successfully!'))).toBeNotVisible().withTimeout(10000);
+ await expect(element(by.text('Profile saved successfully!'))).toBeNotVisible();
+ });
+
+ it('should change email and password', async() => {
+ await element(by.id('profile-view-email')).replaceText(`test${ data.email }`);
+ await element(by.id('profile-view-new-password')).replaceText(`${ data.password }new`);
+ await element(by.id('profile-view-submit')).tap();
+ await waitFor(element(by.id('profile-view-typed-password'))).toBeVisible().withTimeout(10000);
+ await expect(element(by.id('profile-view-typed-password'))).toBeVisible();
+ await element(by.id('profile-view-typed-password')).replaceText(`${ data.password }`);
+ await element(by.text('Save')).tap();
+ await waitFor(element(by.text('Profile saved successfully!'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text('Profile saved successfully!'))).toBeVisible();
+ await waitFor(element(by.text('Profile saved successfully!'))).toBeNotVisible().withTimeout(10000);
+ await expect(element(by.text('Profile saved successfully!'))).toBeNotVisible();
+ });
+
+ it('should reset avatar', async() => {
+ await element(by.id('profile-view-list')).swipe('up');
+ await element(by.id('profile-view-reset-avatar')).tap();
+ await waitFor(element(by.text('Avatar changed successfully!'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text('Avatar changed successfully!'))).toBeVisible();
+ await waitFor(element(by.text('Avatar changed successfully!'))).toBeNotVisible().withTimeout(10000);
+ await expect(element(by.text('Avatar changed successfully!'))).toBeNotVisible();
+ });
+
+ after(async() => {
+ takeScreenshot();
+ });
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index e0d2ec7de..1458aa307 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10764,6 +10764,11 @@
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
"integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ=="
},
+ "js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -11116,6 +11121,11 @@
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
},
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+ },
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -14980,6 +14990,26 @@
"prop-types": "15.6.1"
}
},
+ "react-native-dialog": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-4.0.0.tgz",
+ "integrity": "sha512-BQ2nR2ISDohgSZ/9V34o66FbuuIlJvjOb8FXMoc69aP+fuZt9JA0AiPn2kjQpryfQtTMnFNjcPTDJjXLylSLsw==",
+ "requires": {
+ "prop-types": "15.6.1",
+ "react-native-modal": "5.4.0"
+ },
+ "dependencies": {
+ "react-native-modal": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-5.4.0.tgz",
+ "integrity": "sha512-Bvq4FQPMAFijqjqNX6TxLgKOwdbruM6GvFwF9rb+mowbaFZVoYbHTKLaAbdPlrblgaZKWyOuuxBUoDx41+Xktg==",
+ "requires": {
+ "prop-types": "15.6.1",
+ "react-native-animatable": "1.2.4"
+ }
+ }
+ }
+ },
"react-native-dismiss-keyboard": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz",
@@ -15127,6 +15157,14 @@
"prop-types": "15.6.1"
}
},
+ "react-native-picker-select": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-3.1.1.tgz",
+ "integrity": "sha512-zuASTVjdW9fkT1NXMGguLwL2bmiZH0AXATAAKPAS/Rqu5/4GRhwJ+HFwnSL+rGYaGTh4Q2vMlox4cmfSv0IIFQ==",
+ "requires": {
+ "lodash.isequal": "4.5.0"
+ }
+ },
"react-native-push-notification": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.2.tgz",
diff --git a/package.json b/package.json
index 1bf7ec01a..c2faafedd 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"deep-equal": "^1.0.1",
"ejson": "^2.1.2",
"js-base64": "^2.4.5",
+ "js-sha256": "^0.9.0",
"lodash": "^4.17.10",
"markdown-it-flowdock": "^0.3.7",
"moment": "^2.22.2",
@@ -44,6 +45,7 @@
"react-native-action-button": "^2.8.3",
"react-native-actionsheet": "^2.4.2",
"react-native-audio": "^4.1.3",
+ "react-native-dialog": "^4.0.0",
"react-native-fabric": "^0.5.1",
"react-native-fast-image": "^4.0.14",
"react-native-fetch-blob": "^0.10.8",
@@ -56,6 +58,7 @@
"react-native-meteor": "^1.3.0",
"react-native-modal": "^6.1.0",
"react-native-optimized-flatlist": "^1.0.4",
+ "react-native-picker-select": "^3.1.1",
"react-native-push-notification": "^3.0.1",
"react-native-responsive-ui": "^1.1.1",
"react-native-safari-view": "^2.1.0",