From da173275cef2ed2473c7b3eba13f6759000f8fe4 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 12 Jun 2018 22:33:00 -0300 Subject: [PATCH] [NEW] User Profile (#323) * Drawer layout * Drawer changes * Profile * Profile avatar * Set language * Tests * Custom fields * Readme updated * fix invalid user muted value * Fix for "Cannot add a child that doesn't have a YogaNode to a parent without a measure function! (Trying to add a 'RCTVirtualText' to a 'RCTView')" --- README.md | 6 + __tests__/__snapshots__/RoomItem.js.snap | 6 +- .../__snapshots__/Storyshots.test.js.snap | 30 +- app/actions/login.js | 6 +- app/containers/Avatar.js | 81 +++- app/containers/MessageBox/index.js | 11 +- app/containers/Sidebar.js | 49 +- app/containers/TextInput.js | 8 +- app/containers/message/Reply.js | 7 +- app/containers/message/index.js | 3 +- app/containers/routes/AuthRoutes.js | 29 +- app/i18n/locales/en.js | 92 +++- app/lib/ddp.js | 6 +- app/lib/realm.js | 6 +- app/lib/rocketchat.js | 60 ++- app/sagas/init.js | 4 +- app/sagas/login.js | 11 + app/views/ForgotPasswordView.js | 40 +- app/views/LoginSignupView.js | 21 +- app/views/LoginView.js | 2 +- app/views/MentionedMessagesView/index.js | 4 +- app/views/PinnedMessagesView/index.js | 4 +- app/views/ProfileView/index.js | 426 +++++++++++++++++- app/views/ProfileView/styles.js | 24 + app/views/RegisterView.js | 3 +- app/views/RoomActionsView/index.js | 2 +- app/views/RoomActionsView/styles.js | 7 - app/views/RoomFilesView/index.js | 4 +- app/views/RoomInfoEditView/index.js | 230 +++++----- app/views/RoomInfoView/index.js | 35 +- app/views/RoomInfoView/styles.js | 7 - app/views/RoomMembersView/index.js | 16 +- app/views/RoomMembersView/styles.js | 16 +- app/views/RoomView/Header/index.js | 7 +- app/views/RoomView/Header/styles.js | 7 - app/views/RoomView/ListView.js | 4 +- app/views/RoomView/index.js | 8 +- app/views/RoomsListView/Header/index.js | 9 +- app/views/RoomsListView/Header/styles.js | 7 - app/views/RoomsListView/index.js | 2 +- app/views/SearchMessagesView/index.js | 4 +- app/views/SettingsView/index.js | 124 ++++- app/views/SnippetedMessagesView/index.js | 4 +- app/views/StarredMessagesView/index.js | 4 +- app/views/Styles.js | 7 + e2e/11-broadcast.spec.js | 11 +- e2e/12-profile.spec.js | 115 +++++ package-lock.json | 38 ++ package.json | 3 + 49 files changed, 1279 insertions(+), 331 deletions(-) create mode 100644 app/views/ProfileView/styles.js create mode 100644 e2e/12-profile.spec.js 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' + /> + + +