[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')"
This commit is contained in:
parent
802eff267c
commit
da173275ce
|
@ -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.
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
<FastImage
|
||||
style={[styles.avatar, avatarStyle]}
|
||||
source={{
|
||||
|
@ -70,18 +132,19 @@ export default class Avatar extends React.PureComponent {
|
|||
priority: FastImage.priority.high
|
||||
}}
|
||||
/>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.iconContainer, iconContainerStyle, style]}>
|
||||
{this.state.showInitials &&
|
||||
{this.state.showInitials ?
|
||||
<Text
|
||||
style={[styles.avatarInitials, avatarInitialsStyle]}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{initials}
|
||||
</Text>
|
||||
: null
|
||||
}
|
||||
{image}
|
||||
{this.props.children}
|
||||
|
|
|
@ -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'}
|
||||
/>,
|
||||
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
||||
]
|
||||
|
@ -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'
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -95,6 +95,34 @@ export default class Sidebar extends Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
servers: [],
|
||||
showServers: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
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() {
|
||||
database.databases.serversDB.removeListener('change', this.updateState);
|
||||
}
|
||||
|
||||
onPressItem = (item) => {
|
||||
this.props.selectServer(item.id);
|
||||
this.closeDrawer();
|
||||
}
|
||||
|
||||
setStatus = () => {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
status: [{
|
||||
id: 'online',
|
||||
name: I18n.t('Online')
|
||||
|
@ -107,23 +135,9 @@ export default class Sidebar extends Component {
|
|||
}, {
|
||||
id: 'offline',
|
||||
name: I18n.t('Invisible')
|
||||
}],
|
||||
showServers: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
database.databases.serversDB.addListener('change', this.updateState);
|
||||
this.setState(this.getState());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
database.databases.serversDB.removeListener('change', this.updateState);
|
||||
}
|
||||
|
||||
onPressItem = (item) => {
|
||||
this.props.selectServer(item.id);
|
||||
this.closeDrawer();
|
||||
}]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getState = () => ({
|
||||
|
@ -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}
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
@ -105,7 +105,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
const { showPassword } = this.state;
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
{label && <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> }
|
||||
{label ? <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> : null }
|
||||
<View style={styles.wrap}>
|
||||
<TextInput
|
||||
style={[
|
||||
|
@ -126,10 +126,10 @@ export default class RCTextInput extends React.PureComponent {
|
|||
contentDescription={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
{iconLeft && this.iconLeft(iconLeft)}
|
||||
{secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')}
|
||||
{iconLeft ? this.iconLeft(iconLeft) : null}
|
||||
{secureTextEntry ? this.iconPassword(showPassword ? 'eye-off' : 'eye') : null}
|
||||
</View>
|
||||
{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
|
||||
{error.error ? <Text style={sharedStyles.error}>{error.reason}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,6 @@ const Reply = ({ attachment, timeFormat }) => {
|
|||
<Avatar
|
||||
text={attachment.author_name}
|
||||
size={16}
|
||||
avatar={attachment.author_icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -136,7 +135,11 @@ const Reply = ({ attachment, timeFormat }) => {
|
|||
{renderTitle()}
|
||||
{renderText()}
|
||||
{renderFields()}
|
||||
{attachment.attachments && attachment.attachments.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)}
|
||||
{attachment.attachments ?
|
||||
attachment.attachments
|
||||
.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
@ -358,13 +358,14 @@ export default class Message extends React.Component {
|
|||
{this.renderBroadcastReply()}
|
||||
</View>
|
||||
</View>
|
||||
{this.state.reactionsModal &&
|
||||
{this.state.reactionsModal ?
|
||||
<ReactionsModal
|
||||
isVisible={this.state.reactionsModal}
|
||||
onClose={this.onClose}
|
||||
reactions={item.reactions}
|
||||
user={this.props.user}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</Touch>
|
||||
|
|
|
@ -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 }) => (
|
||||
<TouchableOpacity
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={navigation.toggleDrawer}
|
||||
accessibilityLabel={I18n.t('Toggle_Drawer')}
|
||||
accessibilityTraits='button'
|
||||
testID={testID}
|
||||
>
|
||||
<Icon name='menu' size={30} color='#292E35' />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const Routes = createDrawerNavigator(
|
||||
{
|
||||
Chats: {
|
||||
screen: AuthRoutes,
|
||||
navigationOptions: {
|
||||
drawerLabel: 'Chats',
|
||||
drawerLabel: I18n.t('Chats'),
|
||||
drawerIcon: () => <Icon name='chat-bubble' size={20} />
|
||||
}
|
||||
},
|
||||
|
@ -146,9 +159,9 @@ const Routes = createDrawerNavigator(
|
|||
ProfileView: {
|
||||
screen: ProfileView,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Profile',
|
||||
title: I18n.t('Profile'),
|
||||
headerTintColor: '#292E35',
|
||||
headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
|
||||
headerLeft: <MenuButton navigation={navigation} testID='profile-view-sidebar' />
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -158,9 +171,9 @@ const Routes = createDrawerNavigator(
|
|||
SettingsView: {
|
||||
screen: SettingsView,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Settings',
|
||||
title: I18n.t('Settings'),
|
||||
headerTintColor: '#292E35',
|
||||
headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
|
||||
headerLeft: <MenuButton navigation={navigation} testID='settings-view-sidebar' />
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -78,7 +78,6 @@ export default class ForgotPasswordView extends LoggedView {
|
|||
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
|
||||
<SafeAreaView testID='forgot-password-view'>
|
||||
<View style={styles.loginView}>
|
||||
<View style={styles.formContainer}>
|
||||
<TextInput
|
||||
inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
|
||||
label={I18n.t('Email')}
|
||||
|
@ -99,8 +98,7 @@ export default class ForgotPasswordView extends LoggedView {
|
|||
/>
|
||||
</View>
|
||||
|
||||
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
|
||||
</View>
|
||||
{this.props.login.failure ? <Text style={styles.error}>{this.props.login.error.reason}</Text> : null}
|
||||
<Loading visible={this.props.login.isFetching} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
|
|
@ -209,61 +209,68 @@ export default class LoginSignupView extends LoggedView {
|
|||
{I18n.t('Or_continue_using_social_accounts')}
|
||||
</Text>
|
||||
<View style={sharedStyles.loginOAuthButtons} key='services'>
|
||||
{this.props.Accounts_OAuth_Facebook && this.props.services.facebook &&
|
||||
{this.props.Accounts_OAuth_Facebook && this.props.services.facebook ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
|
||||
onPress={this.onPressFacebook}
|
||||
>
|
||||
<Icon name='facebook' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Github && this.props.services.github &&
|
||||
{this.props.Accounts_OAuth_Github && this.props.services.github ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
|
||||
onPress={this.onPressGithub}
|
||||
>
|
||||
<Icon name='github' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab &&
|
||||
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
|
||||
onPress={this.onPressGitlab}
|
||||
>
|
||||
<Icon name='gitlab' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Google && this.props.services.google &&
|
||||
{this.props.Accounts_OAuth_Google && this.props.services.google ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
|
||||
onPress={this.onPressGoogle}
|
||||
>
|
||||
<Icon name='google' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin &&
|
||||
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
|
||||
onPress={this.onPressLinkedin}
|
||||
>
|
||||
<Icon name='linkedin' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] &&
|
||||
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
|
||||
onPress={this.onPressMeteor}
|
||||
>
|
||||
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter &&
|
||||
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter ?
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
|
||||
onPress={this.onPressTwitter}
|
||||
>
|
||||
<Icon name='twitter' size={20} color='#ffffff' />
|
||||
</TouchableOpacity>
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -135,7 +135,7 @@ export default class LoginView extends LoggedView {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
{this.props.failure && <Text style={styles.error}>{this.props.reason}</Text>}
|
||||
{this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null}
|
||||
<Loading visible={this.props.isFetching} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
|
|
|
@ -109,8 +109,8 @@ export default class MentionedMessagesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
/>
|
||||
]
|
||||
);
|
||||
|
|
|
@ -133,8 +133,8 @@ export default class PinnedMessagesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
/>,
|
||||
<ActionSheet
|
||||
key='pinned-messages-view-action-sheet'
|
||||
|
|
|
@ -1,18 +1,436 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, ScrollView, SafeAreaView, Keyboard } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Dialog from 'react-native-dialog';
|
||||
import SHA256 from 'js-sha256';
|
||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import RNPickerSelect from 'react-native-picker-select';
|
||||
|
||||
import LoggedView from '../View';
|
||||
import KeyboardView from '../../presentation/KeyboardView';
|
||||
import sharedStyles from '../Styles';
|
||||
import styles from './styles';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import { showErrorAlert, showToast } from '../../utils/info';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import RCTextInput from '../../containers/TextInput';
|
||||
import Loading from '../../containers/Loading';
|
||||
import log from '../../utils/log';
|
||||
import I18n from '../../i18n';
|
||||
import Button from '../../containers/Button';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import Touch from '../../utils/touch';
|
||||
|
||||
@connect(state => ({
|
||||
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
|
||||
}) => (
|
||||
<Touch
|
||||
key={key}
|
||||
testID={key}
|
||||
onPress={onPress}
|
||||
underlayColor='rgba(255, 255, 255, 0.5)'
|
||||
activeOpacity={0.3}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View
|
||||
style={[styles.avatarButton, { opacity: disabled ? 0.5 : 1 }]}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</Touch>
|
||||
)
|
||||
|
||||
renderAvatarButtons = () => (
|
||||
<View style={styles.avatarButtons}>
|
||||
{this.renderAvatarButton({
|
||||
child: <Avatar text={this.props.user.username} size={50} forceInitials />,
|
||||
onPress: () => this.resetAvatar(),
|
||||
key: 'profile-view-reset-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <Icon name='file-upload' size={30} />,
|
||||
onPress: () => this.pickImage(),
|
||||
key: 'profile-view-upload-avatar'
|
||||
})}
|
||||
{this.renderAvatarButton({
|
||||
child: <Icon name='link' size={30} />,
|
||||
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: <Avatar avatar={url} size={50} />,
|
||||
onPress: () => this.setAvatar({
|
||||
url, data: blob, service, contentType
|
||||
})
|
||||
});
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
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 (
|
||||
<RNPickerSelect
|
||||
key={key}
|
||||
items={options}
|
||||
onValueChange={(value) => {
|
||||
const newValue = {};
|
||||
newValue[key] = value;
|
||||
this.setState({ customFields: { ...this.state.customFields, ...newValue } });
|
||||
}}
|
||||
value={customFields[key]}
|
||||
>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { this[key] = e; }}
|
||||
label={key}
|
||||
placeholder={key}
|
||||
value={customFields[key]}
|
||||
testID='settings-view-language'
|
||||
/>
|
||||
</RNPickerSelect>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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 (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text>ProfileView</Text>
|
||||
<KeyboardView
|
||||
contentContainerStyle={sharedStyles.container}
|
||||
keyboardVerticalOffset={128}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={sharedStyles.containerScrollView}
|
||||
testID='profile-view-list'
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
<SafeAreaView testID='profile-view'>
|
||||
<View style={styles.avatarContainer} testID='profile-view-avatar'>
|
||||
<Avatar
|
||||
text={username}
|
||||
avatar={this.state.avatar && this.state.avatar.url}
|
||||
size={100}
|
||||
/>
|
||||
</View>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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'
|
||||
/>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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'
|
||||
/>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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'
|
||||
/>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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()}
|
||||
<RCTextInput
|
||||
inputRef={(e) => { 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()}
|
||||
<View style={sharedStyles.alignItemsFlexStart}>
|
||||
<Button
|
||||
title={I18n.t('Save_Changes')}
|
||||
type='primary'
|
||||
onPress={this.submit}
|
||||
disabled={!this.formIsChanged()}
|
||||
testID='profile-view-submit'
|
||||
/>
|
||||
</View>
|
||||
<Loading visible={this.state.saving} />
|
||||
<Dialog.Container visible={this.state.showPasswordAlert}>
|
||||
<Dialog.Title>
|
||||
{I18n.t('Please_enter_your_password')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{I18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
|
||||
</Dialog.Description>
|
||||
<Dialog.Input
|
||||
onChangeText={value => this.setState({ typedPassword: value })}
|
||||
secureTextEntry
|
||||
testID='profile-view-typed-password'
|
||||
/>
|
||||
<Dialog.Button label={I18n.t('Cancel')} onPress={this.closePasswordAlert} />
|
||||
<Dialog.Button label={I18n.t('Save')} onPress={this.submit} />
|
||||
</Dialog.Container>
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
</KeyboardView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -207,10 +207,11 @@ export default class RegisterView extends LoggedView {
|
|||
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
|
||||
{this._renderRegister()}
|
||||
{this._renderUsername()}
|
||||
{this.props.login.failure &&
|
||||
{this.props.login.failure ?
|
||||
<Text style={styles.error} testID='register-view-error'>
|
||||
{this.props.login.error.reason}
|
||||
</Text>
|
||||
: null
|
||||
}
|
||||
<Loading visible={this.props.login.isFetching} />
|
||||
</SafeAreaView>
|
||||
|
|
|
@ -381,7 +381,7 @@ export default class RoomActionsView extends LoggedView {
|
|||
] : [
|
||||
<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
|
||||
<Text key='name' style={styles.sectionItemName}>{ item.name }</Text>,
|
||||
item.description && <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text>,
|
||||
item.description ? <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text> : null,
|
||||
<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#ccc' />
|
||||
];
|
||||
return this.renderTouchableItem(subview, item);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -107,8 +107,8 @@ export default class RoomFilesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
/>
|
||||
]
|
||||
);
|
||||
|
|
|
@ -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,7 +266,6 @@ export default class RoomInfoEditView extends LoggedView {
|
|||
{...scrollPersistTaps}
|
||||
>
|
||||
<SafeAreaView testID='room-info-edit-view'>
|
||||
<View style={sharedStyles.formContainer}>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { this.name = e; }}
|
||||
label={I18n.t('Name')}
|
||||
|
@ -328,7 +327,7 @@ export default class RoomInfoEditView extends LoggedView {
|
|||
disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
|
||||
testID='room-info-edit-view-ro'
|
||||
/>
|
||||
{ro && !room.broadcast &&
|
||||
{ro && !room.broadcast ?
|
||||
<SwitchContainer
|
||||
value={reactWhenReadOnly}
|
||||
leftLabelPrimary={I18n.t('No_Reactions')}
|
||||
|
@ -339,12 +338,14 @@ export default class RoomInfoEditView extends LoggedView {
|
|||
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
|
||||
testID='room-info-edit-view-react-when-ro'
|
||||
/>
|
||||
: null
|
||||
}
|
||||
{room.broadcast &&
|
||||
{room.broadcast ?
|
||||
[
|
||||
<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
|
||||
<View style={styles.divider} />
|
||||
]
|
||||
: null
|
||||
}
|
||||
<TouchableOpacity
|
||||
style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
|
||||
|
@ -392,7 +393,6 @@ export default class RoomInfoEditView extends LoggedView {
|
|||
>
|
||||
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Loading visible={this.state.saving} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class RoomInfoView extends LoggedView {
|
|||
accessibilityTraits='button'
|
||||
testID='room-info-view-edit-button'
|
||||
>
|
||||
<View style={styles.headerButton}>
|
||||
<View style={sharedStyles.headerButton}>
|
||||
<MaterialIcon name='edit' size={20} />
|
||||
</View>
|
||||
</Touch>
|
||||
|
@ -146,7 +146,7 @@ export default class RoomInfoView extends LoggedView {
|
|||
);
|
||||
|
||||
renderRoles = () => (
|
||||
this.state.roles.length > 0 &&
|
||||
this.state.roles.length > 0 ?
|
||||
<View style={styles.item}>
|
||||
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
|
||||
<View style={styles.rolesContainer}>
|
||||
|
@ -157,6 +157,7 @@ export default class RoomInfoView extends LoggedView {
|
|||
))}
|
||||
</View>
|
||||
</View>
|
||||
: null
|
||||
)
|
||||
|
||||
renderTimezone = (userId) => {
|
||||
|
@ -210,12 +211,12 @@ export default class RoomInfoView extends LoggedView {
|
|||
{this.renderAvatar(room, roomUser)}
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
|
||||
</View>
|
||||
{!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}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: (
|
||||
<Touch
|
||||
<TouchableOpacity
|
||||
onPress={params.onPressToogleStatus}
|
||||
underlayColor='#ffffff'
|
||||
activeOpacity={0.5}
|
||||
accessibilityLabel={label}
|
||||
accessibilityTraits='button'
|
||||
style={styles.headerButtonTouchable}
|
||||
style={[sharedStyles.headerButton, styles.headerButton]}
|
||||
testID='room-members-view-toggle-status'
|
||||
>
|
||||
<View style={styles.headerButton}>
|
||||
<Text style={styles.headerButtonText}>{label}</Text>
|
||||
</View>
|
||||
</Touch>
|
||||
<Text>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
{ t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>}
|
||||
{ t ? <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text> : null}
|
||||
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
@ -169,7 +170,7 @@ export default class RoomHeaderView extends React.PureComponent {
|
|||
renderRight = () => (
|
||||
<View style={styles.right}>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={() => {
|
||||
try {
|
||||
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
|
||||
|
@ -189,7 +190,7 @@ export default class RoomHeaderView extends React.PureComponent {
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
|
||||
accessibilityLabel={I18n.t('Room_actions')}
|
||||
accessibilityTraits='button'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -95,8 +95,12 @@ export default class RoomView extends LoggedView {
|
|||
}
|
||||
|
||||
requestAnimationFrame(async() => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={() => this.props.navigation.openDrawer()}
|
||||
>
|
||||
<FastImage
|
||||
|
@ -174,7 +175,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
|||
</Avatar>
|
||||
<View style={styles.rows}>
|
||||
<Text accessible={false} style={styles.title} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{this.props.user.username}</Text>
|
||||
{ t && <Text accessible={false} style={styles.status_text} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{t}</Text>}
|
||||
{ t ? <Text accessible={false} style={styles.status_text} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{t}</Text> : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
@ -189,7 +190,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
|||
<View style={styles.right}>
|
||||
{Platform.OS === 'android' ?
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={() => this.onPressSearchButton()}
|
||||
accessibilityLabel={I18n.t('Search')}
|
||||
accessibilityTraits='button'
|
||||
|
@ -203,7 +204,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
|||
</TouchableOpacity> : null}
|
||||
{Platform.OS === 'ios' ?
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
style={sharedStyles.headerButton}
|
||||
onPress={() => this.createChannel()}
|
||||
accessibilityLabel={I18n.t('Create_Channel')}
|
||||
accessibilityTraits='button'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -223,6 +223,6 @@ export default class RoomsListView extends LoggedView {
|
|||
render = () => (
|
||||
<View style={styles.container} testID='rooms-list-view'>
|
||||
{this.renderList()}
|
||||
{Platform.OS === 'android' && this.renderCreateButtons()}
|
||||
{Platform.OS === 'android' ? this.renderCreateButtons() : null}
|
||||
</View>)
|
||||
}
|
||||
|
|
|
@ -129,8 +129,8 @@ export default class SearchMessagesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={searching && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={searching ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
{...scrollPersistTaps}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -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 (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text>SettingsView</Text>
|
||||
<KeyboardView
|
||||
contentContainerStyle={sharedStyles.container}
|
||||
keyboardVerticalOffset={128}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={sharedStyles.containerScrollView}
|
||||
testID='settings-view-list'
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
<SafeAreaView testID='settings-view'>
|
||||
<RNPickerSelect
|
||||
items={languages}
|
||||
onValueChange={(value) => {
|
||||
this.setState({ language: value });
|
||||
}}
|
||||
value={language}
|
||||
placeholder={placeholder}
|
||||
>
|
||||
<RCTextInput
|
||||
inputRef={(e) => { this.name = e; }}
|
||||
label={I18n.t('Language')}
|
||||
placeholder={I18n.t('Language')}
|
||||
value={language}
|
||||
testID='settings-view-language'
|
||||
/>
|
||||
</RNPickerSelect>
|
||||
<View style={sharedStyles.alignItemsFlexStart}>
|
||||
<Button
|
||||
title={I18n.t('Save_Changes')}
|
||||
type='primary'
|
||||
onPress={this.submit}
|
||||
disabled={!this.formIsChanged()}
|
||||
testID='settings-view-button'
|
||||
/>
|
||||
</View>
|
||||
<Loading visible={this.state.saving} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
</KeyboardView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,8 +109,8 @@ export default class SnippetedMessagesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
/>
|
||||
]
|
||||
);
|
||||
|
|
|
@ -133,8 +133,8 @@ export default class StarredMessagesView extends LoggedView {
|
|||
style={styles.list}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.moreData}
|
||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
||||
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||
/>,
|
||||
<ActionSheet
|
||||
key='starred-messages-view-action-sheet'
|
||||
|
|
|
@ -195,5 +195,12 @@ export default StyleSheet.create({
|
|||
width: 50,
|
||||
height: 50,
|
||||
marginVertical: 25
|
||||
},
|
||||
headerButton: {
|
||||
backgroundColor: 'transparent',
|
||||
height: 44,
|
||||
width: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ const {
|
|||
device, expect, element, by, waitFor
|
||||
} = require('detox');
|
||||
const { takeScreenshot } = require('./helpers/screenshot');
|
||||
const { logout, navigateToLogin } = require('./helpers/app');
|
||||
const { logout, navigateToLogin, login } = require('./helpers/app');
|
||||
const data = require('./data');
|
||||
|
||||
describe('Broadcast room', () => {
|
||||
|
@ -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();
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue