[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)
|
**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
|
# 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.
|
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={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/name",
|
"uri": "/avatar/name?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -835,7 +835,7 @@ exports[`render unread 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/name",
|
"uri": "/avatar/name?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -1085,7 +1085,7 @@ exports[`renders correctly 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/name",
|
"uri": "/avatar/name?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
|
|
@ -62,7 +62,7 @@ exports[`Storyshots Avatar avatar 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/test",
|
"uri": "/avatar/test?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -136,7 +136,7 @@ exports[`Storyshots Avatar avatar 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/aa",
|
"uri": "/avatar/aa?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -210,7 +210,7 @@ exports[`Storyshots Avatar avatar 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/bb",
|
"uri": "/avatar/bb?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -284,7 +284,7 @@ exports[`Storyshots Avatar avatar 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/test",
|
"uri": "/avatar/test?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -393,7 +393,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/rocket.cat",
|
"uri": "/avatar/rocket.cat?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -615,7 +615,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/rocket.cat",
|
"uri": "/avatar/rocket.cat?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -841,7 +841,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/rocket.cat",
|
"uri": "/avatar/rocket.cat?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -1086,7 +1086,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"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={
|
style={
|
||||||
|
@ -1335,7 +1335,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"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={
|
style={
|
||||||
|
@ -1580,7 +1580,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"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={
|
style={
|
||||||
|
@ -1825,7 +1825,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"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={
|
style={
|
||||||
|
@ -2070,7 +2070,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"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={
|
style={
|
||||||
|
@ -2315,7 +2315,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/W",
|
"uri": "/avatar/W?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -2537,7 +2537,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/WW",
|
"uri": "/avatar/WW?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
@ -2759,7 +2759,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
|
||||||
source={
|
source={
|
||||||
Object {
|
Object {
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"uri": "/avatar/",
|
"uri": "/avatar/?random=0",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
|
|
|
@ -120,8 +120,10 @@ export function forgotPasswordFailure(err) {
|
||||||
|
|
||||||
export function setUser(action) {
|
export function setUser(action) {
|
||||||
return {
|
return {
|
||||||
type: types.USER.SET,
|
// do not change this params order
|
||||||
...action
|
// 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 { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
|
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
|
||||||
|
import database from '../lib/realm';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
|
@ -26,17 +27,78 @@ export default class Avatar extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
style: ViewPropTypes.style,
|
style: ViewPropTypes.style,
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string,
|
||||||
avatar: PropTypes.string,
|
avatar: PropTypes.string,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
borderRadius: PropTypes.number,
|
borderRadius: PropTypes.number,
|
||||||
type: PropTypes.string,
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd'
|
text, size, baseUrl, borderRadius, style, avatar, type, forceInitials
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { initials, color } = avatarInitialsAndColor(`${ text }`);
|
const { initials, color } = avatarInitialsAndColor(`${ text }`);
|
||||||
|
|
||||||
|
@ -60,9 +122,9 @@ export default class Avatar extends React.PureComponent {
|
||||||
|
|
||||||
let image;
|
let image;
|
||||||
|
|
||||||
if (type === 'd') {
|
if (type === 'd' && !forceInitials) {
|
||||||
const uri = avatar || `${ baseUrl }/avatar/${ text }`;
|
const uri = avatar || `${ baseUrl }/avatar/${ text }?random=${ this.avatarVersion }`;
|
||||||
image = uri && (
|
image = uri ? (
|
||||||
<FastImage
|
<FastImage
|
||||||
style={[styles.avatar, avatarStyle]}
|
style={[styles.avatar, avatarStyle]}
|
||||||
source={{
|
source={{
|
||||||
|
@ -70,18 +132,19 @@ export default class Avatar extends React.PureComponent {
|
||||||
priority: FastImage.priority.high
|
priority: FastImage.priority.high
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.iconContainer, iconContainerStyle, style]}>
|
<View style={[styles.iconContainer, iconContainerStyle, style]}>
|
||||||
{this.state.showInitials &&
|
{this.state.showInitials ?
|
||||||
<Text
|
<Text
|
||||||
style={[styles.avatarInitials, avatarInitialsStyle]}
|
style={[styles.avatarInitials, avatarInitialsStyle]}
|
||||||
allowFontScaling={false}
|
allowFontScaling={false}
|
||||||
>
|
>
|
||||||
{initials}
|
{initials}
|
||||||
</Text>
|
</Text>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{image}
|
{image}
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|
|
@ -172,7 +172,7 @@ export default class MessageBox extends React.PureComponent {
|
||||||
maxWidth: 1960,
|
maxWidth: 1960,
|
||||||
quality: 0.8
|
quality: 0.8
|
||||||
};
|
};
|
||||||
ImagePicker.showImagePicker(options, (response) => {
|
ImagePicker.showImagePicker(options, async(response) => {
|
||||||
if (response.didCancel) {
|
if (response.didCancel) {
|
||||||
console.warn('User cancelled image picker');
|
console.warn('User cancelled image picker');
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
|
@ -185,7 +185,11 @@ export default class MessageBox extends React.PureComponent {
|
||||||
// description: '',
|
// description: '',
|
||||||
store: 'Uploads'
|
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 }}
|
style={{ margin: 8 }}
|
||||||
text={item.username || item.name}
|
text={item.username || item.name}
|
||||||
size={30}
|
size={30}
|
||||||
|
type={item.username ? 'd' : 'c'}
|
||||||
/>,
|
/>,
|
||||||
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
||||||
]
|
]
|
||||||
|
@ -477,7 +482,7 @@ export default class MessageBox extends React.PureComponent {
|
||||||
style={styles.mentionList}
|
style={styles.mentionList}
|
||||||
data={mentions}
|
data={mentions}
|
||||||
renderItem={({ item }) => this.renderMentionItem(item)}
|
renderItem={({ item }) => this.renderMentionItem(item)}
|
||||||
keyExtractor={item => item._id || item}
|
keyExtractor={item => item._id || item.username || item}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -95,6 +95,34 @@ export default class Sidebar extends Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
servers: [],
|
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: [{
|
status: [{
|
||||||
id: 'online',
|
id: 'online',
|
||||||
name: I18n.t('Online')
|
name: I18n.t('Online')
|
||||||
|
@ -107,23 +135,9 @@ export default class Sidebar extends Component {
|
||||||
}, {
|
}, {
|
||||||
id: 'offline',
|
id: 'offline',
|
||||||
name: I18n.t('Invisible')
|
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 = () => ({
|
getState = () => ({
|
||||||
|
@ -153,6 +167,8 @@ export default class Sidebar extends Component {
|
||||||
const { navigate } = this.props.navigation;
|
const { navigate } = this.props.navigation;
|
||||||
if (!this.isRouteFocused(route)) {
|
if (!this.isRouteFocused(route)) {
|
||||||
navigate(route);
|
navigate(route);
|
||||||
|
} else {
|
||||||
|
this.closeDrawer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,6 +227,7 @@ export default class Sidebar extends Component {
|
||||||
this.toggleServers();
|
this.toggleServers();
|
||||||
if (this.props.server !== item.id) {
|
if (this.props.server !== item.id) {
|
||||||
this.props.selectServer(item.id);
|
this.props.selectServer(item.id);
|
||||||
|
this.props.navigation.navigate('RoomsList');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
testID: `sidebar-${ item.id }`
|
testID: `sidebar-${ item.id }`
|
||||||
|
@ -324,8 +341,8 @@ export default class Sidebar extends Component {
|
||||||
|
|
||||||
{this.renderSeparator('separator-header')}
|
{this.renderSeparator('separator-header')}
|
||||||
|
|
||||||
{!this.state.showServers && this.renderNavigation()}
|
{!this.state.showServers ? this.renderNavigation() : null}
|
||||||
{this.state.showServers && this.renderServers()}
|
{this.state.showServers ? this.renderServers() : null}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default class RCTextInput extends React.PureComponent {
|
||||||
const { showPassword } = this.state;
|
const { showPassword } = this.state;
|
||||||
return (
|
return (
|
||||||
<View style={[styles.inputContainer, containerStyle]}>
|
<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}>
|
<View style={styles.wrap}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[
|
style={[
|
||||||
|
@ -126,10 +126,10 @@ export default class RCTextInput extends React.PureComponent {
|
||||||
contentDescription={placeholder}
|
contentDescription={placeholder}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
{iconLeft && this.iconLeft(iconLeft)}
|
{iconLeft ? this.iconLeft(iconLeft) : null}
|
||||||
{secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')}
|
{secureTextEntry ? this.iconPassword(showPassword ? 'eye-off' : 'eye') : null}
|
||||||
</View>
|
</View>
|
||||||
{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
|
{error.error ? <Text style={sharedStyles.error}>{error.reason}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,6 @@ const Reply = ({ attachment, timeFormat }) => {
|
||||||
<Avatar
|
<Avatar
|
||||||
text={attachment.author_name}
|
text={attachment.author_name}
|
||||||
size={16}
|
size={16}
|
||||||
avatar={attachment.author_icon}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -136,7 +135,11 @@ const Reply = ({ attachment, timeFormat }) => {
|
||||||
{renderTitle()}
|
{renderTitle()}
|
||||||
{renderText()}
|
{renderText()}
|
||||||
{renderFields()}
|
{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>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
@ -358,13 +358,14 @@ export default class Message extends React.Component {
|
||||||
{this.renderBroadcastReply()}
|
{this.renderBroadcastReply()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{this.state.reactionsModal &&
|
{this.state.reactionsModal ?
|
||||||
<ReactionsModal
|
<ReactionsModal
|
||||||
isVisible={this.state.reactionsModal}
|
isVisible={this.state.reactionsModal}
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
reactions={item.reactions}
|
reactions={item.reactions}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</Touch>
|
</Touch>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Platform } from 'react-native';
|
import { Platform, TouchableOpacity } from 'react-native';
|
||||||
import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
|
import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
|
||||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import RoomInfoEditView from '../../views/RoomInfoEditView';
|
||||||
import ProfileView from '../../views/ProfileView';
|
import ProfileView from '../../views/ProfileView';
|
||||||
import SettingsView from '../../views/SettingsView';
|
import SettingsView from '../../views/SettingsView';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
import sharedStyles from '../../views/Styles';
|
||||||
|
|
||||||
const headerTintColor = '#292E35';
|
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(
|
const Routes = createDrawerNavigator(
|
||||||
{
|
{
|
||||||
Chats: {
|
Chats: {
|
||||||
screen: AuthRoutes,
|
screen: AuthRoutes,
|
||||||
navigationOptions: {
|
navigationOptions: {
|
||||||
drawerLabel: 'Chats',
|
drawerLabel: I18n.t('Chats'),
|
||||||
drawerIcon: () => <Icon name='chat-bubble' size={20} />
|
drawerIcon: () => <Icon name='chat-bubble' size={20} />
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -146,9 +159,9 @@ const Routes = createDrawerNavigator(
|
||||||
ProfileView: {
|
ProfileView: {
|
||||||
screen: ProfileView,
|
screen: ProfileView,
|
||||||
navigationOptions: ({ navigation }) => ({
|
navigationOptions: ({ navigation }) => ({
|
||||||
title: 'Profile',
|
title: I18n.t('Profile'),
|
||||||
headerTintColor: '#292E35',
|
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: {
|
SettingsView: {
|
||||||
screen: SettingsView,
|
screen: SettingsView,
|
||||||
navigationOptions: ({ navigation }) => ({
|
navigationOptions: ({ navigation }) => ({
|
||||||
title: 'Settings',
|
title: I18n.t('Settings'),
|
||||||
headerTintColor: '#292E35',
|
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,
|
contentComponent: Sidebar,
|
||||||
navigationOptions: {
|
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked',
|
||||||
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
|
|
||||||
},
|
|
||||||
initialRouteName: 'Chats',
|
initialRouteName: 'Chats',
|
||||||
backBehavior: 'initialRoute'
|
backBehavior: 'initialRoute'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,80 @@
|
||||||
export default {
|
export default {
|
||||||
'1_online_member': '1 online member',
|
'1_online_member': '1 online member',
|
||||||
'1_person_reacted': '1 person reacted',
|
'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',
|
Actions: 'Actions',
|
||||||
Add_Reaction: 'Add Reaction',
|
Add_Reaction: 'Add Reaction',
|
||||||
Add_Server: 'Add Server',
|
Add_Server: 'Add Server',
|
||||||
|
@ -21,6 +95,8 @@ export default {
|
||||||
Are_you_sure_question_mark: 'Are you sure?',
|
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}}?',
|
Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
|
||||||
Authenticating: 'Authenticating',
|
Authenticating: 'Authenticating',
|
||||||
|
Avatar_changed_successfully: 'Avatar changed successfully!',
|
||||||
|
Avatar_Url: 'Avatar URL',
|
||||||
Away: 'Away',
|
Away: 'Away',
|
||||||
Block_user: 'Block user',
|
Block_user: 'Block user',
|
||||||
Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
|
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_editing: 'Cancel editing',
|
||||||
Cancel_recording: 'Cancel recording',
|
Cancel_recording: 'Cancel recording',
|
||||||
Cancel: 'Cancel',
|
Cancel: 'Cancel',
|
||||||
|
changing_avatar: 'changing avatar',
|
||||||
Channel_Name: 'Channel Name',
|
Channel_Name: 'Channel Name',
|
||||||
Chats: 'Chats',
|
Chats: 'Chats',
|
||||||
Close_emoji_selector: 'Close emoji selector',
|
Close_emoji_selector: 'Close emoji selector',
|
||||||
|
@ -60,6 +137,7 @@ export default {
|
||||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||||
Files: 'Files',
|
Files: 'Files',
|
||||||
Finish_recording: 'Finish recording',
|
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_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_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',
|
Forgot_password: 'Forgot password',
|
||||||
|
@ -71,6 +149,7 @@ export default {
|
||||||
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
|
is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
|
||||||
is_typing: 'is typing',
|
is_typing: 'is typing',
|
||||||
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
|
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
|
||||||
|
Language: 'Language',
|
||||||
last_message: 'last message',
|
last_message: 'last message',
|
||||||
Leave_channel: 'Leave channel',
|
Leave_channel: 'Leave channel',
|
||||||
leave: 'leave',
|
leave: 'leave',
|
||||||
|
@ -95,6 +174,7 @@ export default {
|
||||||
Name: 'Name',
|
Name: 'Name',
|
||||||
New_in_RocketChat_question_mark: 'New in Rocket.Chat?',
|
New_in_RocketChat_question_mark: 'New in Rocket.Chat?',
|
||||||
New_Message: 'New Message',
|
New_Message: 'New Message',
|
||||||
|
New_Password: 'New Password',
|
||||||
New_Server: 'New Server',
|
New_Server: 'New Server',
|
||||||
No_files: 'No files',
|
No_files: 'No files',
|
||||||
No_mentioned_messages: 'No mentioned messages',
|
No_mentioned_messages: 'No mentioned messages',
|
||||||
|
@ -121,9 +201,12 @@ export default {
|
||||||
Pinned_Messages: 'Pinned Messages',
|
Pinned_Messages: 'Pinned Messages',
|
||||||
pinned: 'pinned',
|
pinned: 'pinned',
|
||||||
Pinned: 'Pinned',
|
Pinned: 'Pinned',
|
||||||
|
Please_enter_your_password: 'Please enter your password',
|
||||||
|
Preferences_saved: 'Preferences saved!',
|
||||||
Privacy_Policy: ' Privacy Policy',
|
Privacy_Policy: ' Privacy Policy',
|
||||||
Private_Channel: 'Private Channel',
|
Private_Channel: 'Private Channel',
|
||||||
Private: 'Private',
|
Private: 'Private',
|
||||||
|
Profile_saved_successfully: 'Profile saved successfully!',
|
||||||
Profile: 'Profile',
|
Profile: 'Profile',
|
||||||
Public_Channel: 'Public Channel',
|
Public_Channel: 'Public Channel',
|
||||||
Public: 'Public',
|
Public: 'Public',
|
||||||
|
@ -151,8 +234,14 @@ export default {
|
||||||
Room_Members: 'Room Members',
|
Room_Members: 'Room Members',
|
||||||
Room_name_changed: 'Room name changed to: {{name}} by {{userBy}}',
|
Room_name_changed: 'Room name changed to: {{name}} by {{userBy}}',
|
||||||
SAVE: 'SAVE',
|
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_Messages: 'Search Messages',
|
||||||
Search: 'Search',
|
Search: 'Search',
|
||||||
|
Select_Avatar: 'Select Avatar',
|
||||||
Select_Users: 'Select Users',
|
Select_Users: 'Select Users',
|
||||||
Send_audio_message: 'Send audio message',
|
Send_audio_message: 'Send audio message',
|
||||||
Send_message: 'Send message',
|
Send_message: 'Send message',
|
||||||
|
@ -177,10 +266,11 @@ export default {
|
||||||
tap_to_change_status: 'tap to change status',
|
tap_to_change_status: 'tap to change status',
|
||||||
Tap_to_view_servers_list: 'Tap to view servers list',
|
Tap_to_view_servers_list: 'Tap to view servers list',
|
||||||
Terms_of_Service: ' Terms of Service ',
|
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_blocked: 'This room is blocked',
|
||||||
This_room_is_read_only: 'This room is read only',
|
This_room_is_read_only: 'This room is read only',
|
||||||
Timezone: 'Timezone',
|
Timezone: 'Timezone',
|
||||||
|
Toggle_Drawer: 'Toggle_Drawer',
|
||||||
topic: 'topic',
|
topic: 'topic',
|
||||||
Topic: 'Topic',
|
Topic: 'Topic',
|
||||||
Type_the_channel_name_here: 'Type the channel name here',
|
Type_the_channel_name_here: 'Type the channel name here',
|
||||||
|
|
|
@ -144,9 +144,11 @@ export default class Socket extends EventEmitter {
|
||||||
try {
|
try {
|
||||||
this.emit('login', params);
|
this.emit('login', params);
|
||||||
const result = await this.call('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._logged = true;
|
||||||
this.emit('logged', result);
|
// this.emit('logged', result);
|
||||||
|
this.emit('logged', this._login);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = { ...err };
|
const error = { ...err };
|
||||||
|
|
|
@ -106,11 +106,11 @@ const subscriptionSchema = {
|
||||||
|
|
||||||
const usersSchema = {
|
const usersSchema = {
|
||||||
name: 'users',
|
name: 'users',
|
||||||
primaryKey: '_id',
|
primaryKey: 'username',
|
||||||
properties: {
|
properties: {
|
||||||
_id: 'string',
|
|
||||||
username: '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 || {};
|
this.activeUsers = this.activeUsers || {};
|
||||||
const { user } = reduxStore.getState().login;
|
const { user } = reduxStore.getState().login;
|
||||||
|
|
||||||
if (user && user.id === ddpMessage.id) {
|
if (ddpMessage.fields && user && user.id === ddpMessage.id) {
|
||||||
if (!ddpMessage.fields) {
|
|
||||||
reduxStore.dispatch(setUser({ status: 'offline' }));
|
|
||||||
}
|
|
||||||
reduxStore.dispatch(setUser(ddpMessage.fields));
|
reduxStore.dispatch(setUser(ddpMessage.fields));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,9 +104,14 @@ const RocketChat = {
|
||||||
reduxStore.dispatch(setActiveUser(this.activeUsers));
|
reduxStore.dispatch(setActiveUser(this.activeUsers));
|
||||||
this._setUserTimer = null;
|
this._setUserTimer = null;
|
||||||
return this.activeUsers = {};
|
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) {
|
async loginSuccess(user) {
|
||||||
try {
|
try {
|
||||||
|
@ -122,15 +124,11 @@ const RocketChat = {
|
||||||
// call /me only one time
|
// call /me only one time
|
||||||
if (!user.username) {
|
if (!user.username) {
|
||||||
const me = await this.me({ token: user.token, userId: user.id });
|
const me = await this.me({ token: user.token, userId: user.id });
|
||||||
// eslint-disable-next-line
|
user = { ...user, ...me };
|
||||||
user.username = me.username;
|
|
||||||
}
|
}
|
||||||
if (user.username) {
|
if (user.username) {
|
||||||
const userInfo = await this.userInfo({ token: user.token, userId: user.id });
|
const userInfo = await this.userInfo({ token: user.token, userId: user.id });
|
||||||
user.username = userInfo.user.username;
|
user = { ...user, ...userInfo.user };
|
||||||
if (userInfo.user.roles) {
|
|
||||||
user.roles = userInfo.user.roles;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return reduxStore.dispatch(loginSuccess(user));
|
return reduxStore.dispatch(loginSuccess(user));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -163,7 +161,10 @@ const RocketChat = {
|
||||||
this.getRooms().catch(e => log('logged getRooms', e));
|
this.getRooms().catch(e => log('logged getRooms', e));
|
||||||
this.loginSuccess(user);
|
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(() => {
|
this.ddp.on('disconnected', protectedFunction(() => {
|
||||||
reduxStore.dispatch(disconnect());
|
reduxStore.dispatch(disconnect());
|
||||||
|
@ -184,6 +185,24 @@ const RocketChat = {
|
||||||
return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
|
return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.ddp.on('stream-notify-logged', (ddpMessage) => {
|
||||||
|
// this entire logic needs a better solution
|
||||||
|
// we're using it only because our image cache lib doesn't support clear cache
|
||||||
|
if (ddpMessage.fields && ddpMessage.fields.eventName === 'updateAvatar') {
|
||||||
|
const { args } = ddpMessage.fields;
|
||||||
|
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) => {
|
// this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => {
|
||||||
// console.warn('rc.stream-notify-user')
|
// console.warn('rc.stream-notify-user')
|
||||||
// const [type, data] = ddpMessage.fields.args;
|
// const [type, data] = ddpMessage.fields.args;
|
||||||
|
@ -804,6 +823,12 @@ const RocketChat = {
|
||||||
saveRoomSettings(rid, params) {
|
saveRoomSettings(rid, params) {
|
||||||
return call('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) {
|
saveNotificationSettings(rid, param, value) {
|
||||||
return call('saveNotificationSettings', rid, param, value);
|
return call('saveNotificationSettings', rid, param, value);
|
||||||
},
|
},
|
||||||
|
@ -836,6 +861,15 @@ const RocketChat = {
|
||||||
.some(item => mergedRoles.indexOf(item) !== -1);
|
.some(item => mergedRoles.indexOf(item) !== -1);
|
||||||
return result;
|
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));
|
yield put(setServer(currentServer));
|
||||||
|
|
||||||
const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
|
const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
|
||||||
if (login && login.user) {
|
if (login) {
|
||||||
yield put(setUser(login.user));
|
yield put(setUser(JSON.parse(login)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import * as NavigationService from '../containers/routes/NavigationService';
|
import * as NavigationService from '../containers/routes/NavigationService';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
|
import I18n from '../i18n';
|
||||||
|
|
||||||
const getUser = state => state.login;
|
const getUser = state => state.login;
|
||||||
const getServer = state => state.server.server;
|
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() {
|
const root = function* root() {
|
||||||
// yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
|
// yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
|
||||||
// yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
|
// yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
|
||||||
|
@ -184,5 +194,6 @@ const root = function* root() {
|
||||||
yield takeLatest(types.LOGOUT, handleLogout);
|
yield takeLatest(types.LOGOUT, handleLogout);
|
||||||
yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
|
yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
|
||||||
yield takeLatest(types.LOGIN.OPEN, watchLoginOpen);
|
yield takeLatest(types.LOGIN.OPEN, watchLoginOpen);
|
||||||
|
yield takeLatest(types.USER.SET, handleSetUser);
|
||||||
};
|
};
|
||||||
export default root;
|
export default root;
|
||||||
|
|
|
@ -78,7 +78,6 @@ export default class ForgotPasswordView extends LoggedView {
|
||||||
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
|
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
|
||||||
<SafeAreaView testID='forgot-password-view'>
|
<SafeAreaView testID='forgot-password-view'>
|
||||||
<View style={styles.loginView}>
|
<View style={styles.loginView}>
|
||||||
<View style={styles.formContainer}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
|
inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
|
||||||
label={I18n.t('Email')}
|
label={I18n.t('Email')}
|
||||||
|
@ -99,8 +98,7 @@ export default class ForgotPasswordView extends LoggedView {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
|
{this.props.login.failure ? <Text style={styles.error}>{this.props.login.error.reason}</Text> : null}
|
||||||
</View>
|
|
||||||
<Loading visible={this.props.login.isFetching} />
|
<Loading visible={this.props.login.isFetching} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
@ -209,61 +209,68 @@ export default class LoginSignupView extends LoggedView {
|
||||||
{I18n.t('Or_continue_using_social_accounts')}
|
{I18n.t('Or_continue_using_social_accounts')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={sharedStyles.loginOAuthButtons} key='services'>
|
<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
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
|
||||||
onPress={this.onPressFacebook}
|
onPress={this.onPressFacebook}
|
||||||
>
|
>
|
||||||
<Icon name='facebook' size={20} color='#ffffff' />
|
<Icon name='facebook' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Github && this.props.services.github &&
|
{this.props.Accounts_OAuth_Github && this.props.services.github ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
|
||||||
onPress={this.onPressGithub}
|
onPress={this.onPressGithub}
|
||||||
>
|
>
|
||||||
<Icon name='github' size={20} color='#ffffff' />
|
<Icon name='github' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab &&
|
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
|
||||||
onPress={this.onPressGitlab}
|
onPress={this.onPressGitlab}
|
||||||
>
|
>
|
||||||
<Icon name='gitlab' size={20} color='#ffffff' />
|
<Icon name='gitlab' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Google && this.props.services.google &&
|
{this.props.Accounts_OAuth_Google && this.props.services.google ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
|
||||||
onPress={this.onPressGoogle}
|
onPress={this.onPressGoogle}
|
||||||
>
|
>
|
||||||
<Icon name='google' size={20} color='#ffffff' />
|
<Icon name='google' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin &&
|
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
|
||||||
onPress={this.onPressLinkedin}
|
onPress={this.onPressLinkedin}
|
||||||
>
|
>
|
||||||
<Icon name='linkedin' size={20} color='#ffffff' />
|
<Icon name='linkedin' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] &&
|
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
|
||||||
onPress={this.onPressMeteor}
|
onPress={this.onPressMeteor}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
|
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter &&
|
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
|
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
|
||||||
onPress={this.onPressTwitter}
|
onPress={this.onPressTwitter}
|
||||||
>
|
>
|
||||||
<Icon name='twitter' size={20} color='#ffffff' />
|
<Icon name='twitter' size={20} color='#ffffff' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default class LoginView extends LoggedView {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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} />
|
<Loading visible={this.props.isFetching} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -109,8 +109,8 @@ export default class MentionedMessagesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
/>
|
/>
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -133,8 +133,8 @@ export default class PinnedMessagesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
/>,
|
/>,
|
||||||
<ActionSheet
|
<ActionSheet
|
||||||
key='pinned-messages-view-action-sheet'
|
key='pinned-messages-view-action-sheet'
|
||||||
|
|
|
@ -1,18 +1,436 @@
|
||||||
import React from 'react';
|
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 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 {
|
export default class ProfileView extends LoggedView {
|
||||||
|
static propTypes = {
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
user: PropTypes.object,
|
||||||
|
Accounts_CustomFields: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super('ProfileView', 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() {
|
render() {
|
||||||
|
const {
|
||||||
|
name, username, email, newPassword, avatarUrl, customFields
|
||||||
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<KeyboardView
|
||||||
<Text>ProfileView</Text>
|
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>
|
</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>
|
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
|
||||||
{this._renderRegister()}
|
{this._renderRegister()}
|
||||||
{this._renderUsername()}
|
{this._renderUsername()}
|
||||||
{this.props.login.failure &&
|
{this.props.login.failure ?
|
||||||
<Text style={styles.error} testID='register-view-error'>
|
<Text style={styles.error} testID='register-view-error'>
|
||||||
{this.props.login.error.reason}
|
{this.props.login.error.reason}
|
||||||
</Text>
|
</Text>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
<Loading visible={this.props.login.isFetching} />
|
<Loading visible={this.props.login.isFetching} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
@ -381,7 +381,7 @@ export default class RoomActionsView extends LoggedView {
|
||||||
] : [
|
] : [
|
||||||
<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
|
<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
|
||||||
<Text key='name' style={styles.sectionItemName}>{ item.name }</Text>,
|
<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' />
|
<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#ccc' />
|
||||||
];
|
];
|
||||||
return this.renderTouchableItem(subview, item);
|
return this.renderTouchableItem(subview, item);
|
||||||
|
|
|
@ -4,13 +4,6 @@ export default StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: '#F6F7F9'
|
backgroundColor: '#F6F7F9'
|
||||||
},
|
},
|
||||||
headerButton: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
sectionItem: {
|
sectionItem: {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
|
|
|
@ -107,8 +107,8 @@ export default class RoomFilesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
/>
|
/>
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -190,7 +190,7 @@ export default class RoomInfoEditView extends LoggedView {
|
||||||
await this.setState({ saving: false });
|
await this.setState({ saving: false });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (error) {
|
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 {
|
} else {
|
||||||
showToast(I18n.t('Settings_succesfully_changed'));
|
showToast(I18n.t('Settings_succesfully_changed'));
|
||||||
}
|
}
|
||||||
|
@ -266,7 +266,6 @@ export default class RoomInfoEditView extends LoggedView {
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
>
|
>
|
||||||
<SafeAreaView testID='room-info-edit-view'>
|
<SafeAreaView testID='room-info-edit-view'>
|
||||||
<View style={sharedStyles.formContainer}>
|
|
||||||
<RCTextInput
|
<RCTextInput
|
||||||
inputRef={(e) => { this.name = e; }}
|
inputRef={(e) => { this.name = e; }}
|
||||||
label={I18n.t('Name')}
|
label={I18n.t('Name')}
|
||||||
|
@ -328,7 +327,7 @@ export default class RoomInfoEditView extends LoggedView {
|
||||||
disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
|
disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
|
||||||
testID='room-info-edit-view-ro'
|
testID='room-info-edit-view-ro'
|
||||||
/>
|
/>
|
||||||
{ro && !room.broadcast &&
|
{ro && !room.broadcast ?
|
||||||
<SwitchContainer
|
<SwitchContainer
|
||||||
value={reactWhenReadOnly}
|
value={reactWhenReadOnly}
|
||||||
leftLabelPrimary={I18n.t('No_Reactions')}
|
leftLabelPrimary={I18n.t('No_Reactions')}
|
||||||
|
@ -339,12 +338,14 @@ export default class RoomInfoEditView extends LoggedView {
|
||||||
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
|
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
|
||||||
testID='room-info-edit-view-react-when-ro'
|
testID='room-info-edit-view-react-when-ro'
|
||||||
/>
|
/>
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
{room.broadcast &&
|
{room.broadcast ?
|
||||||
[
|
[
|
||||||
<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
|
<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
]
|
]
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
|
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>
|
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
|
||||||
<Loading visible={this.state.saving} />
|
<Loading visible={this.state.saving} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class RoomInfoView extends LoggedView {
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
testID='room-info-view-edit-button'
|
testID='room-info-view-edit-button'
|
||||||
>
|
>
|
||||||
<View style={styles.headerButton}>
|
<View style={sharedStyles.headerButton}>
|
||||||
<MaterialIcon name='edit' size={20} />
|
<MaterialIcon name='edit' size={20} />
|
||||||
</View>
|
</View>
|
||||||
</Touch>
|
</Touch>
|
||||||
|
@ -146,7 +146,7 @@ export default class RoomInfoView extends LoggedView {
|
||||||
);
|
);
|
||||||
|
|
||||||
renderRoles = () => (
|
renderRoles = () => (
|
||||||
this.state.roles.length > 0 &&
|
this.state.roles.length > 0 ?
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
|
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
|
||||||
<View style={styles.rolesContainer}>
|
<View style={styles.rolesContainer}>
|
||||||
|
@ -157,6 +157,7 @@ export default class RoomInfoView extends LoggedView {
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
: null
|
||||||
)
|
)
|
||||||
|
|
||||||
renderTimezone = (userId) => {
|
renderTimezone = (userId) => {
|
||||||
|
@ -210,12 +211,12 @@ export default class RoomInfoView extends LoggedView {
|
||||||
{this.renderAvatar(room, roomUser)}
|
{this.renderAvatar(room, roomUser)}
|
||||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
|
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
|
||||||
</View>
|
</View>
|
||||||
{!this.isDirect() && this.renderItem('description', room)}
|
{!this.isDirect() ? this.renderItem('description', room) : null}
|
||||||
{!this.isDirect() && this.renderItem('topic', room)}
|
{!this.isDirect() ? this.renderItem('topic', room) : null}
|
||||||
{!this.isDirect() && this.renderItem('announcement', room)}
|
{!this.isDirect() ? this.renderItem('announcement', room) : null}
|
||||||
{this.isDirect() && this.renderRoles()}
|
{this.isDirect() ? this.renderRoles() : null}
|
||||||
{this.isDirect() && this.renderTimezone(roomUser._id)}
|
{this.isDirect() ? this.renderTimezone(roomUser._id) : null}
|
||||||
{room.broadcast && this.renderBroadcast()}
|
{room.broadcast ? this.renderBroadcast() : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,6 @@ export default StyleSheet.create({
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
padding: 10
|
padding: 10
|
||||||
},
|
},
|
||||||
headerButton: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
item: {
|
item: {
|
||||||
padding: 10,
|
padding: 10,
|
||||||
// borderColor: '#EBEDF1',
|
// borderColor: '#EBEDF1',
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { connect } from 'react-redux';
|
||||||
import ActionSheet from 'react-native-actionsheet';
|
import ActionSheet from 'react-native-actionsheet';
|
||||||
|
|
||||||
import LoggedView from '../View';
|
import LoggedView from '../View';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
import RoomItem from '../../presentation/RoomItem';
|
import RoomItem from '../../presentation/RoomItem';
|
||||||
import Touch from '../../utils/touch';
|
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import { goRoom } from '../../containers/routes/NavigationService';
|
import { goRoom } from '../../containers/routes/NavigationService';
|
||||||
|
@ -33,19 +33,15 @@ export default class MentionedMessagesView extends LoggedView {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
headerRight: (
|
headerRight: (
|
||||||
<Touch
|
<TouchableOpacity
|
||||||
onPress={params.onPressToogleStatus}
|
onPress={params.onPressToogleStatus}
|
||||||
underlayColor='#ffffff'
|
|
||||||
activeOpacity={0.5}
|
|
||||||
accessibilityLabel={label}
|
accessibilityLabel={label}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
style={styles.headerButtonTouchable}
|
style={[sharedStyles.headerButton, styles.headerButton]}
|
||||||
testID='room-members-view-toggle-status'
|
testID='room-members-view-toggle-status'
|
||||||
>
|
>
|
||||||
<View style={styles.headerButton}>
|
<Text>{label}</Text>
|
||||||
<Text style={styles.headerButtonText}>{label}</Text>
|
</TouchableOpacity>
|
||||||
</View>
|
|
||||||
</Touch>
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,18 +31,6 @@ export default StyleSheet.create({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#444'
|
color: '#444'
|
||||||
},
|
},
|
||||||
headerButtonTouchable: {
|
|
||||||
borderRadius: 4
|
|
||||||
},
|
|
||||||
headerButton: {
|
|
||||||
padding: 6,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
headerButtonText: {
|
|
||||||
color: '#292E35'
|
|
||||||
},
|
|
||||||
searchBoxView: {
|
searchBoxView: {
|
||||||
backgroundColor: '#eee'
|
backgroundColor: '#eee'
|
||||||
},
|
},
|
||||||
|
@ -53,5 +41,9 @@ export default StyleSheet.create({
|
||||||
padding: 5,
|
padding: 5,
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
marginRight: 9,
|
||||||
|
alignItems: 'flex-end'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { closeRoom } from '../../../actions/room';
|
||||||
import log from '../../../utils/log';
|
import log from '../../../utils/log';
|
||||||
import RoomTypeIcon from '../../../containers/RoomTypeIcon';
|
import RoomTypeIcon from '../../../containers/RoomTypeIcon';
|
||||||
import I18n from '../../../i18n';
|
import I18n from '../../../i18n';
|
||||||
|
import sharedStyles from '../../Styles';
|
||||||
|
|
||||||
const title = (offline, connecting, authenticating, logged) => {
|
const title = (offline, connecting, authenticating, logged) => {
|
||||||
if (offline) {
|
if (offline) {
|
||||||
|
@ -159,7 +160,7 @@ export default class RoomHeaderView extends React.PureComponent {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -169,7 +170,7 @@ export default class RoomHeaderView extends React.PureComponent {
|
||||||
renderRight = () => (
|
renderRight = () => (
|
||||||
<View style={styles.right}>
|
<View style={styles.right}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.headerButton}
|
style={sharedStyles.headerButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
try {
|
try {
|
||||||
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
|
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
|
||||||
|
@ -189,7 +190,7 @@ export default class RoomHeaderView extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.headerButton}
|
style={sharedStyles.headerButton}
|
||||||
onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
|
onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
|
||||||
accessibilityLabel={I18n.t('Room_actions')}
|
accessibilityLabel={I18n.t('Room_actions')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
|
|
|
@ -40,13 +40,6 @@ export default StyleSheet.create({
|
||||||
right: {
|
right: {
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
},
|
},
|
||||||
headerButton: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: 44,
|
|
||||||
width: 40,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
avatar: {
|
avatar: {
|
||||||
marginRight: 5
|
marginRight: 5
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,8 +113,8 @@ export class ListView extends OldList2 {
|
||||||
|
|
||||||
// const { renderSectionHeader } = this.props;
|
// const { renderSectionHeader } = this.props;
|
||||||
|
|
||||||
const header = this.props.renderHeader && this.props.renderHeader();
|
const header = this.props.renderHeader ? this.props.renderHeader() : null;
|
||||||
const footer = this.props.renderFooter && this.props.renderFooter();
|
const footer = this.props.renderFooter ? this.props.renderFooter() : null;
|
||||||
// let totalIndex = header ? 1 : 0;
|
// let totalIndex = header ? 1 : 0;
|
||||||
|
|
||||||
const { data } = this.props;
|
const { data } = this.props;
|
||||||
|
|
|
@ -95,8 +95,12 @@ export default class RoomView extends LoggedView {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(async() => {
|
requestAnimationFrame(async() => {
|
||||||
|
try {
|
||||||
const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
|
const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
|
||||||
this.setState({ end: result < 20 });
|
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 { STATUS_COLORS } from '../../../constants/colors';
|
||||||
import { setSearch } from '../../../actions/rooms';
|
import { setSearch } from '../../../actions/rooms';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
import sharedStyles from '../../Styles';
|
||||||
import log from '../../../utils/log';
|
import log from '../../../utils/log';
|
||||||
import I18n from '../../../i18n';
|
import I18n from '../../../i18n';
|
||||||
|
|
||||||
|
@ -130,7 +131,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
||||||
testID='rooms-list-view-sidebar'
|
testID='rooms-list-view-sidebar'
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.headerButton}
|
style={sharedStyles.headerButton}
|
||||||
onPress={() => this.props.navigation.openDrawer()}
|
onPress={() => this.props.navigation.openDrawer()}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
|
@ -174,7 +175,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<View style={styles.rows}>
|
<View style={styles.rows}>
|
||||||
<Text accessible={false} style={styles.title} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{this.props.user.username}</Text>
|
<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>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
@ -189,7 +190,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
||||||
<View style={styles.right}>
|
<View style={styles.right}>
|
||||||
{Platform.OS === 'android' ?
|
{Platform.OS === 'android' ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.headerButton}
|
style={sharedStyles.headerButton}
|
||||||
onPress={() => this.onPressSearchButton()}
|
onPress={() => this.onPressSearchButton()}
|
||||||
accessibilityLabel={I18n.t('Search')}
|
accessibilityLabel={I18n.t('Search')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
|
@ -203,7 +204,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
|
||||||
</TouchableOpacity> : null}
|
</TouchableOpacity> : null}
|
||||||
{Platform.OS === 'ios' ?
|
{Platform.OS === 'ios' ?
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.headerButton}
|
style={sharedStyles.headerButton}
|
||||||
onPress={() => this.createChannel()}
|
onPress={() => this.createChannel()}
|
||||||
accessibilityLabel={I18n.t('Create_Channel')}
|
accessibilityLabel={I18n.t('Create_Channel')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
|
|
|
@ -55,13 +55,6 @@ export default StyleSheet.create({
|
||||||
borderBottomColor: 'rgba(0, 0, 0, .3)',
|
borderBottomColor: 'rgba(0, 0, 0, .3)',
|
||||||
paddingHorizontal: 20
|
paddingHorizontal: 20
|
||||||
},
|
},
|
||||||
headerButton: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: 44,
|
|
||||||
width: 44,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
user_status: {
|
user_status: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: -2,
|
bottom: -2,
|
||||||
|
|
|
@ -223,6 +223,6 @@ export default class RoomsListView extends LoggedView {
|
||||||
render = () => (
|
render = () => (
|
||||||
<View style={styles.container} testID='rooms-list-view'>
|
<View style={styles.container} testID='rooms-list-view'>
|
||||||
{this.renderList()}
|
{this.renderList()}
|
||||||
{Platform.OS === 'android' && this.renderCreateButtons()}
|
{Platform.OS === 'android' ? this.renderCreateButtons() : null}
|
||||||
</View>)
|
</View>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,8 +129,8 @@ export default class SearchMessagesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={searching && <RCActivityIndicator />}
|
ListHeaderComponent={searching ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,18 +1,134 @@
|
||||||
import React from 'react';
|
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 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 {
|
export default class SettingsView extends LoggedView {
|
||||||
|
static propTypes = {
|
||||||
|
user: PropTypes.object,
|
||||||
|
setUser: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super('SettingsView', 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() {
|
render() {
|
||||||
|
const { language, languages, placeholder } = this.state;
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<KeyboardView
|
||||||
<Text>SettingsView</Text>
|
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>
|
</View>
|
||||||
|
<Loading visible={this.state.saving} />
|
||||||
|
</SafeAreaView>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,8 +109,8 @@ export default class SnippetedMessagesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
/>
|
/>
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -133,8 +133,8 @@ export default class StarredMessagesView extends LoggedView {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
keyExtractor={item => item._id}
|
keyExtractor={item => item._id}
|
||||||
onEndReached={this.moreData}
|
onEndReached={this.moreData}
|
||||||
ListHeaderComponent={loading && <RCActivityIndicator />}
|
ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
ListFooterComponent={loadingMore && <RCActivityIndicator />}
|
ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
|
||||||
/>,
|
/>,
|
||||||
<ActionSheet
|
<ActionSheet
|
||||||
key='starred-messages-view-action-sheet'
|
key='starred-messages-view-action-sheet'
|
||||||
|
|
|
@ -195,5 +195,12 @@ export default StyleSheet.create({
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
marginVertical: 25
|
marginVertical: 25
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
height: 44,
|
||||||
|
width: 44,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ const {
|
||||||
device, expect, element, by, waitFor
|
device, expect, element, by, waitFor
|
||||||
} = require('detox');
|
} = require('detox');
|
||||||
const { takeScreenshot } = require('./helpers/screenshot');
|
const { takeScreenshot } = require('./helpers/screenshot');
|
||||||
const { logout, navigateToLogin } = require('./helpers/app');
|
const { logout, navigateToLogin, login } = require('./helpers/app');
|
||||||
const data = require('./data');
|
const data = require('./data');
|
||||||
|
|
||||||
describe('Broadcast room', () => {
|
describe('Broadcast room', () => {
|
||||||
|
@ -99,4 +99,13 @@ describe('Broadcast room', () => {
|
||||||
afterEach(async() => {
|
afterEach(async() => {
|
||||||
takeScreenshot();
|
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",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
|
||||||
"integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ=="
|
"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": {
|
"js-tokens": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
|
||||||
"integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
|
"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": {
|
"lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
@ -14980,6 +14990,26 @@
|
||||||
"prop-types": "15.6.1"
|
"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": {
|
"react-native-dismiss-keyboard": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz",
|
"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"
|
"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": {
|
"react-native-push-notification": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.2.tgz",
|
"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",
|
"deep-equal": "^1.0.1",
|
||||||
"ejson": "^2.1.2",
|
"ejson": "^2.1.2",
|
||||||
"js-base64": "^2.4.5",
|
"js-base64": "^2.4.5",
|
||||||
|
"js-sha256": "^0.9.0",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"markdown-it-flowdock": "^0.3.7",
|
"markdown-it-flowdock": "^0.3.7",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
"react-native-action-button": "^2.8.3",
|
"react-native-action-button": "^2.8.3",
|
||||||
"react-native-actionsheet": "^2.4.2",
|
"react-native-actionsheet": "^2.4.2",
|
||||||
"react-native-audio": "^4.1.3",
|
"react-native-audio": "^4.1.3",
|
||||||
|
"react-native-dialog": "^4.0.0",
|
||||||
"react-native-fabric": "^0.5.1",
|
"react-native-fabric": "^0.5.1",
|
||||||
"react-native-fast-image": "^4.0.14",
|
"react-native-fast-image": "^4.0.14",
|
||||||
"react-native-fetch-blob": "^0.10.8",
|
"react-native-fetch-blob": "^0.10.8",
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
"react-native-meteor": "^1.3.0",
|
"react-native-meteor": "^1.3.0",
|
||||||
"react-native-modal": "^6.1.0",
|
"react-native-modal": "^6.1.0",
|
||||||
"react-native-optimized-flatlist": "^1.0.4",
|
"react-native-optimized-flatlist": "^1.0.4",
|
||||||
|
"react-native-picker-select": "^3.1.1",
|
||||||
"react-native-push-notification": "^3.0.1",
|
"react-native-push-notification": "^3.0.1",
|
||||||
"react-native-responsive-ui": "^1.1.1",
|
"react-native-responsive-ui": "^1.1.1",
|
||||||
"react-native-safari-view": "^2.1.0",
|
"react-native-safari-view": "^2.1.0",
|
||||||
|
|
Loading…
Reference in New Issue