Merge branch 'develop' into beta

This commit is contained in:
diegolmello 2019-06-11 15:23:24 -03:00
commit 89ad22cd1d
74 changed files with 3003 additions and 440 deletions

View File

@ -253,9 +253,14 @@ workflows:
build-and-test:
jobs:
- lint-testunit
# - e2e-test:
# requires:
# - lint-testunit
- e2e-hold:
type: approval
requires:
- lint-testunit
- e2e-test:
requires:
- e2e-hold
- ios-build:
requires:

View File

@ -0,0 +1,3 @@
export default {
createSagaMonitor: () => {}
};

View File

@ -9216,6 +9216,612 @@ exports[`Storyshots Message list 1`] = `
</View>
</View>
</View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"marginBottom": 0,
"marginTop": 30,
},
]
}
>
Message with read receipt
</Text>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View>
<View
style={
Array [
Object {
"flexDirection": "column",
"paddingHorizontal": 14,
"paddingVertical": 4,
"width": "100%",
},
undefined,
undefined,
]
}
>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 36,
"width": 36,
},
Object {
"marginTop": 4,
},
]
}
>
<View
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"borderRadius": 4,
"height": 36,
"width": 36,
},
]
}
>
<FastImageView
resizeMode="cover"
source={
Object {
"priority": "high",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
}
}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={
Array [
Object {
"flex": 1,
"marginLeft": 46,
},
Object {
"marginLeft": 10,
},
]
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"lineHeight": 22,
}
}
>
diego.mello
</Text>
</View>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#9ca2a8",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "300",
"lineHeight": 22,
"paddingLeft": 10,
}
}
>
10:00 AM
</Text>
</View>
<View
style={Object {}}
>
<Text
numberOfLines={0}
style={
Object {
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
"justifyContent": "flex-start",
"marginBottom": 0,
"marginTop": 0,
}
}
>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
}
}
>
<Text>
Im fine!
</Text>
</Text>
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View>
<View
style={
Array [
Object {
"flexDirection": "column",
"paddingHorizontal": 14,
"paddingVertical": 4,
"width": "100%",
},
undefined,
undefined,
]
}
>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"flex": 1,
"marginLeft": 46,
},
false,
]
}
>
<View
style={Object {}}
>
<Text
numberOfLines={0}
style={
Object {
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
"justifyContent": "flex-start",
"marginBottom": 0,
"marginTop": 0,
}
}
>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
}
}
>
<Text>
Im fine!
</Text>
</Text>
</Text>
</View>
</View>
</View>
</View>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View>
<View
style={
Array [
Object {
"flexDirection": "column",
"paddingHorizontal": 14,
"paddingVertical": 4,
"width": "100%",
},
undefined,
undefined,
]
}
>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 36,
"width": 36,
},
Object {
"marginTop": 4,
},
]
}
>
<View
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"borderRadius": 4,
"height": 36,
"width": 36,
},
]
}
>
<FastImageView
resizeMode="cover"
source={
Object {
"priority": "high",
"uri": "https://open.rocket.chat/avatar/diego.mello?format=png&width=50&height=50&rc_token=79q6lH40W4ZRGLOshDiDiVlQaCc4f_lU9HNdHLAzuHz&rc_uid=y8bd77ptZswPj3EW8",
}
}
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
</View>
<View
style={
Array [
Object {
"flex": 1,
"marginLeft": 46,
},
Object {
"marginLeft": 10,
},
]
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
numberOfLines={1}
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"lineHeight": 22,
}
}
>
diego.mello
</Text>
</View>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#9ca2a8",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "300",
"lineHeight": 22,
"paddingLeft": 10,
}
}
>
10:00 AM
</Text>
</View>
<View
style={Object {}}
>
<Text
numberOfLines={0}
style={
Object {
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
"justifyContent": "flex-start",
"marginBottom": 0,
"marginTop": 0,
}
}
>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
}
}
>
<Text>
Im fine!
</Text>
</Text>
</Text>
</View>
</View>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": "#1d74f5",
"fontSize": 15,
},
Object {
"lineHeight": 20,
},
Object {
"fontFamily": "custom",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
</View>
</View>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View>
<View
style={
Array [
Object {
"flexDirection": "column",
"paddingHorizontal": 14,
"paddingVertical": 4,
"width": "100%",
},
undefined,
undefined,
]
}
>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"flex": 1,
"marginLeft": 46,
},
false,
]
}
>
<View
style={Object {}}
>
<Text
numberOfLines={0}
style={
Object {
"alignItems": "flex-start",
"flexDirection": "row",
"flexWrap": "wrap",
"justifyContent": "flex-start",
"marginBottom": 0,
"marginTop": 0,
}
}
>
<Text
style={
Object {
"backgroundColor": "transparent",
"color": "#2F343D",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "400",
}
}
>
<Text>
Im fine!
</Text>
</Text>
</Text>
</View>
</View>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": "#1d74f5",
"fontSize": 15,
},
Object {
"lineHeight": 20,
},
Object {
"fontFamily": "custom",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
</View>
</View>
</View>
</View>
<Text
style={
Array [

View File

@ -109,10 +109,15 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "1.14.0"
versionName "1.15.0"
vectorDrawables.useSupportLibrary = true
}
packagingOptions {
pickFirst '**/libjsc.so'
pickFirst '**/libc++_shared.so'
}
signingConfigs {
release {
if (project.hasProperty('KEYSTORE')) {
@ -166,6 +171,7 @@ android {
}
dependencies {
implementation "org.webkit:android-jsc:r241213"
implementation project(':react-native-firebase')
implementation project(':react-native-webview')
implementation project(':react-native-orientation-locker')

View File

@ -36,6 +36,10 @@ allprojects {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
}
}
}

View File

@ -66,4 +66,5 @@ export const LOGOUT = 'LOGOUT'; // logout is always success
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
export const TOGGLE_MARKDOWN = 'TOGGLE_MARKDOWN';

View File

@ -0,0 +1,17 @@
import { NOTIFICATION } from './actionsTypes';
export function notificationReceived(params) {
return {
type: NOTIFICATION.RECEIVED,
payload: {
message: params.text,
payload: params.payload
}
};
}
export function removeNotification() {
return {
type: NOTIFICATION.REMOVE
};
}

View File

@ -10,6 +10,7 @@ export const COLOR_TEXT = '#2F343D';
export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
export const COLOR_SEPARATOR = '#A7A7AA';
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
export const COLOR_BACKGROUND_NOTIFICATION = '#f8f8f8';
export const COLOR_BORDER = '#e1e5e8';
export const COLOR_UNREAD = '#e1e5e8';
export const COLOR_TOAST = '#0C0D0F';

View File

@ -14,6 +14,9 @@ export default {
CROWD_Enable: {
type: 'valueAsBoolean'
},
FEDERATION_Enabled: {
type: 'valueAsBoolean'
},
LDAP_Enable: {
type: 'valueAsBoolean'
},
@ -56,6 +59,12 @@ export default {
Assets_favicon_512: {
type: null
},
Message_Read_Receipt_Enabled: {
type: 'valueAsBoolean'
},
Message_Read_Receipt_Store_Users: {
type: 'valueAsBoolean'
},
Threads_enabled: {
type: null
},

18
app/containers/Check.js Normal file
View File

@ -0,0 +1,18 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({
icon: {
width: 22,
height: 22,
marginHorizontal: 15,
...sharedStyles.textColorDescription
}
});
const Check = React.memo(() => <CustomIcon style={styles.icon} size={22} name='check' />);
export default Check;

View File

@ -14,9 +14,12 @@ const styles = StyleSheet.create({
}
});
export const DisclosureImage = React.memo(() => <Image source={{ uri: 'disclosure_indicator' }} style={styles.disclosureIndicator} />);
const DisclosureIndicator = React.memo(() => (
<View style={styles.disclosureContainer}>
<Image source={{ uri: 'disclosure_indicator' }} style={styles.disclosureIndicator} />
<DisclosureImage />
</View>
));
export default DisclosureIndicator;

View File

@ -0,0 +1,93 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { RectButton } from 'react-native-gesture-handler';
import { COLOR_TEXT } from '../constants/colors';
import sharedStyles from '../views/Styles';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 56,
paddingHorizontal: 15
},
disabled: {
opacity: 0.3
},
textContainer: {
flex: 1,
justifyContent: 'center'
},
title: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
subtitle: {
fontSize: 14,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
}
});
const Content = React.memo(({
title, subtitle, disabled, testID, right
}) => (
<View style={[styles.container, disabled && styles.disabled]} testID={testID}>
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
{subtitle
? <Text style={styles.subtitle}>{subtitle}</Text>
: null
}
</View>
{right ? right() : null}
</View>
));
const Button = React.memo(({
onPress, ...props
}) => (
<RectButton
onPress={onPress}
activeOpacity={0.1}
underlayColor={COLOR_TEXT}
enabled={!props.disabled}
>
<Content {...props} />
</RectButton>
));
const Item = React.memo(({ ...props }) => {
if (props.onPress) {
return <Button {...props} />;
}
return <Content {...props} />;
});
Item.propTypes = {
onPress: PropTypes.func
};
Content.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
right: PropTypes.func,
disabled: PropTypes.bool,
testID: PropTypes.string
};
Button.propTypes = {
onPress: PropTypes.func,
disabled: PropTypes.bool
};
Button.defaultProps = {
disabled: false
};
export default Item;

View File

@ -17,6 +17,7 @@ import { vibrate } from '../utils/vibration';
import RocketChat from '../lib/rocketchat';
import I18n from '../i18n';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
@connect(
state => ({
@ -26,7 +27,8 @@ import log from '../utils/log';
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
}),
dispatch => ({
actionsHide: () => dispatch(actionsHideAction()),
@ -56,7 +58,8 @@ export default class MessageActions extends React.Component {
Message_AllowEditing: PropTypes.bool,
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool
Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool
};
constructor(props) {
@ -64,7 +67,7 @@ export default class MessageActions extends React.Component {
this.handleActionPress = this.handleActionPress.bind(this);
this.setPermissions();
const { Message_AllowStarring, Message_AllowPinning } = this.props;
const { Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users } = this.props;
// Cancel
this.options = [I18n.t('Cancel')];
@ -118,6 +121,12 @@ export default class MessageActions extends React.Component {
this.REACTION_INDEX = this.options.length - 1;
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
this.options.push(I18n.t('Read_Receipt'));
this.READ_RECEIPT_INDEX = this.options.length - 1;
}
// Report
this.options.push(I18n.t('Report'));
this.REPORT_INDEX = this.options.length - 1;
@ -302,6 +311,11 @@ export default class MessageActions extends React.Component {
toggleReactionPicker(actionMessage);
}
handleReadReceipt = () => {
const { actionMessage } = this.props;
Navigation.navigate('ReadReceiptsView', { messageId: actionMessage._id });
}
handleReport = async() => {
const { actionMessage } = this.props;
try {
@ -348,6 +362,9 @@ export default class MessageActions extends React.Component {
case this.DELETE_INDEX:
this.handleDelete();
break;
case this.READ_RECEIPT_INDEX:
this.handleReadReceipt();
break;
default:
break;
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity, ActivityIndicator } from 'react-native';
import FastImage from 'react-native-fast-image';
import styles from './styles';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_PRIMARY } from '../../constants/colors';
export default class CommandPreview extends React.PureComponent {
static propTypes = {
onPress: PropTypes.func,
item: PropTypes.object
};
constructor(props) {
super(props);
this.state = { loading: true };
}
render() {
const { onPress, item } = this.props;
const { loading } = this.state;
return (
<TouchableOpacity
style={styles.commandPreview}
onPress={() => onPress(item)}
testID={`command-preview-item${ item.id }`}
>
{item.type === 'image'
? (
<FastImage
style={styles.commandPreviewImage}
source={{ uri: item.value }}
resizeMode={FastImage.resizeMode.cover}
onLoadStart={() => this.setState({ loading: true })}
onLoad={() => this.setState({ loading: false })}
>
{ loading ? <ActivityIndicator /> : null }
</FastImage>
)
: <CustomIcon name='file-generic' size={36} color={COLOR_PRIMARY} />
}
</TouchableOpacity>
);
}
}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert
View, TextInput, FlatList, Text, TouchableOpacity, Alert, ScrollView
} from 'react-native';
import { connect } from 'react-redux';
import { emojify } from 'react-emojione';
@ -32,9 +32,12 @@ import { COLOR_TEXT_DESCRIPTION } from '../../constants/colors';
import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons';
import { isAndroid } from '../../utils/deviceInfo';
import CommandPreview from './CommandPreview';
const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
const MENTIONS_COUNT_TO_DISPLAY = 4;
const onlyUnique = function onlyUnique(value, index, self) {
return self.indexOf(({ _id }) => value._id === _id) === index;
@ -93,8 +96,11 @@ class MessageBox extends Component {
trackingType: '',
file: {
isVisible: false
}
},
commandPreview: []
};
this.showCommandPreview = false;
this.commands = [];
this.users = [];
this.rooms = [];
this.emojis = [];
@ -147,7 +153,7 @@ class MessageBox extends Component {
shouldComponentUpdate(nextProps, nextState) {
const {
showEmojiKeyboard, showSend, recording, mentions, file
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
} = this.state;
const {
roomType, replying, editing, isFocused
@ -176,6 +182,9 @@ class MessageBox extends Component {
if (!equal(nextState.mentions, mentions)) {
return true;
}
if (!equal(nextState.commandPreview, commandPreview)) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
@ -187,18 +196,36 @@ class MessageBox extends Component {
this.setShowSend(!isTextEmpty);
this.handleTyping(!isTextEmpty);
this.setInput(text);
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
if (slashCommand) {
const [, name, params] = slashCommand;
const command = database.objects('slashCommand').filtered('command == $0', name);
if (command && command[0] && command[0].providesPreview) {
return this.setCommandPreview(name, params);
}
}
if (!isTextEmpty) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText;
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
this.showCommandPreview = false;
if (!result) {
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
if (slash) {
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
}
return this.stopTrackingMention();
}
const [, lastChar, name] = result;
this.identifyMentionKeyword(name, lastChar);
} else {
this.stopTrackingMention();
this.showCommandPreview = false;
}
}, 100)
@ -218,13 +245,32 @@ class MessageBox extends Component {
const result = msg.substr(0, cursor).replace(regexp, '');
const mentionName = trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
? `${ item.name || item }:`
: (item.username || item.name);
: (item.username || item.name || item.command);
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) {
this.showCommandPreview = true;
}
this.setInput(text);
this.focus();
requestAnimationFrame(() => this.stopTrackingMention());
}
onPressCommandPreview = (item) => {
const { rid } = this.props;
const { text } = this;
const command = text.substr(0, text.indexOf(' ')).slice(1);
const params = text.substr(text.indexOf(' ') + 1) || 'params';
this.showCommandPreview = false;
this.setState({ commandPreview: [] });
this.stopTrackingMention();
this.clearInput();
try {
RocketChat.executeCommandPreview(command, params, rid, item);
} catch (e) {
log('onPressCommandPreview', e);
}
}
onEmojiSelected = (keyboardId, params) => {
const { text } = this;
const { emoji } = params;
@ -299,7 +345,7 @@ class MessageBox extends Component {
console.warn('spotlight canceled');
} finally {
delete this.oldPromise;
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice();
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.getFixedMentions(keyword);
this.setState({ mentions: this.users });
}
@ -349,13 +395,18 @@ class MessageBox extends Component {
getEmojis = (keyword) => {
if (keyword) {
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4);
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4);
const mergedEmojis = [...this.customEmojis, ...this.emojis];
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...this.customEmojis, ...this.emojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis });
}
}
getSlashCommands = (keyword) => {
this.commands = database.objects('slashCommand').filtered('command CONTAINS[c] $0', keyword);
this.setState({ mentions: this.commands });
}
focus = () => {
if (this.component && this.component.focus) {
this.component.focus();
@ -383,6 +434,18 @@ class MessageBox extends Component {
}, 1000);
}
setCommandPreview = async(command, params) => {
const { rid } = this.props;
try {
const { preview } = await RocketChat.getCommandPreview(command, rid, params);
this.showCommandPreview = true;
this.setState({ commandPreview: preview.items });
} catch (e) {
this.showCommandPreview = false;
log('command Preview', e);
}
}
setInput = (text) => {
this.text = text;
if (this.component && this.component.setNativeProps) {
@ -503,7 +566,7 @@ class MessageBox extends Component {
submit = async() => {
const {
message: editingMessage, editRequest, onSubmit
message: editingMessage, editRequest, onSubmit, rid: roomId
} = this.props;
const message = this.text;
@ -519,6 +582,22 @@ class MessageBox extends Component {
editing, replying
} = this.props;
// Slash command
if (message[0] === MENTIONS_TRACKING_TYPE_COMMANDS) {
const command = message.replace(/ .*/, '').slice(1);
const slashCommand = database.objects('slashCommand').filtered('command CONTAINS[c] $0', command);
if (slashCommand.length > 0) {
try {
const messageWithoutCommand = message.substr(message.indexOf(' ') + 1);
RocketChat.runSlashCommand(command, roomId, messageWithoutCommand);
} catch (e) {
log('slashCommand', e);
}
this.clearInput();
return;
}
}
// Edit
if (editing) {
const { _id, rid } = editingMessage;
@ -559,6 +638,8 @@ class MessageBox extends Component {
this.getUsers(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) {
this.getEmojis(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_COMMANDS) {
this.getSlashCommands(keyword);
} else {
this.getRooms(keyword);
}
@ -577,15 +658,16 @@ class MessageBox extends Component {
if (!trackingType) {
return;
}
this.setState({
mentions: [],
trackingType: ''
trackingType: '',
commandPreview: []
});
this.users = [];
this.rooms = [];
this.customEmojis = [];
this.emojis = [];
this.commands = [];
}
renderFixedMentionItem = item => (
@ -621,41 +703,67 @@ class MessageBox extends Component {
);
}
renderMentionItem = (item) => {
renderMentionItem = ({ item }) => {
const { trackingType } = this.state;
const { baseUrl, user } = this.props;
if (item.username === 'all' || item.username === 'here') {
return this.renderFixedMentionItem(item);
}
const defineTestID = (type) => {
switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`;
case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${ item.command || item }`;
default:
return `mention-item-${ item.username || item.name || item }`;
}
};
const testID = defineTestID(trackingType);
return (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this.onPressMention(item)}
testID={`mention-item-${ trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
testID={testID}
>
{trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
? (
<React.Fragment>
{this.renderMentionEmoji(item)}
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
</React.Fragment>
)
: (
<React.Fragment>
<Avatar
key='mention-item-avatar'
style={{ margin: 8 }}
text={item.username || item.name}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name }</Text>
</React.Fragment>
)
{(() => {
switch (trackingType) {
case MENTIONS_TRACKING_TYPE_EMOJIS:
return (
<React.Fragment>
{this.renderMentionEmoji(item)}
<Text key='mention-item-name' style={styles.mentionText}>:{ item.name || item }:</Text>
</React.Fragment>
);
case MENTIONS_TRACKING_TYPE_COMMANDS:
return (
<React.Fragment>
<Text key='mention-item-command' style={styles.slash}>/</Text>
<Text key='mention-item-param'>{ item.command}</Text>
</React.Fragment>
);
default:
return (
<React.Fragment>
<Avatar
key='mention-item-avatar'
style={styles.avatar}
text={item.username || item.name}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<Text key='mention-item-name' style={styles.mentionText}>{ item.username || item.name || item }</Text>
</React.Fragment>
);
}
})()
}
</TouchableOpacity>
);
@ -667,17 +775,45 @@ class MessageBox extends Component {
return null;
}
return (
<View testID='messagebox-container'>
<ScrollView
testID='messagebox-container'
style={styles.scrollViewMention}
keyboardShouldPersistTaps='always'
>
<FlatList
style={styles.mentionList}
data={mentions}
renderItem={({ item }) => this.renderMentionItem(item)}
keyExtractor={item => item._id || item.username || item}
renderItem={this.renderMentionItem}
keyExtractor={item => item._id || item.username || item.command || item}
keyboardShouldPersistTaps='always'
/>
</ScrollView>
);
};
renderCommandPreviewItem = ({ item }) => (
<CommandPreview item={item} onPress={this.onPressCommandPreview} />
);
renderCommandPreview = () => {
const { commandPreview } = this.state;
if (!this.showCommandPreview) {
return null;
}
return (
<View key='commandbox-container' testID='commandbox-container'>
<FlatList
style={styles.mentionList}
data={commandPreview}
renderItem={this.renderCommandPreviewItem}
keyExtractor={item => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
</View>
);
};
}
renderReplyPreview = () => {
const {
@ -698,6 +834,7 @@ class MessageBox extends Component {
}
return (
<React.Fragment>
{this.renderCommandPreview()}
{this.renderMentions()}
<View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()}

View File

@ -3,10 +3,11 @@ import { StyleSheet } from 'react-native';
import { isIOS } from '../../utils/deviceInfo';
import sharedStyles from '../../views/Styles';
import {
COLOR_BORDER, COLOR_SEPARATOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE
COLOR_BORDER, COLOR_SEPARATOR, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_PRIMARY
} from '../../constants/colors';
const MENTION_HEIGHT = 50;
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
export default StyleSheet.create({
textBox: {
@ -100,5 +101,35 @@ export default StyleSheet.create({
bottom: 0,
left: 0,
right: 0
},
slash: {
color: COLOR_PRIMARY,
backgroundColor: COLOR_BORDER,
height: 30,
width: 30,
padding: 5,
paddingHorizontal: 12,
marginHorizontal: 10,
borderRadius: 2
},
commandPreviewImage: {
justifyContent: 'center',
margin: 3,
width: 120,
height: 80,
borderRadius: 4
},
commandPreview: {
backgroundColor: COLOR_BACKGROUND_CONTAINER,
height: 100,
flex: 1,
flexDirection: 'row',
alignItems: 'center'
},
avatar: {
margin: 8
},
scrollViewMention: {
maxHeight: SCROLLVIEW_MENTION_HEIGHT
}
});

View File

@ -34,7 +34,7 @@ const styles = StyleSheet.create({
}
});
const SearchBox = ({ onChangeText, testID }) => (
const SearchBox = ({ onChangeText, onSubmitEditing, testID }) => (
<View style={styles.container}>
<View style={styles.searchBox}>
<CustomIcon name='magnifier' size={14} color='#8E8E93' />
@ -49,6 +49,7 @@ const SearchBox = ({ onChangeText, testID }) => (
testID={testID}
underlineColorAndroid='transparent'
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
/>
</View>
</View>
@ -56,6 +57,7 @@ const SearchBox = ({ onChangeText, testID }) => (
SearchBox.propTypes = {
onChangeText: PropTypes.func.isRequired,
onSubmitEditing: PropTypes.func,
testID: PropTypes.string
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { COLOR_SEPARATOR } from '../constants/colors';
const styles = StyleSheet.create({
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: COLOR_SEPARATOR
}
});
const Separator = React.memo(({ style }) => <View style={[styles.separator, style]} />);
Separator.propTypes = {
style: PropTypes.object
};
export default Separator;

View File

@ -16,6 +16,7 @@ import Reactions from './Reactions';
import Broadcast from './Broadcast';
import Discussion from './Discussion';
import Content from './Content';
import ReadReceipt from './ReadReceipt';
const MessageInner = React.memo((props) => {
if (props.type === 'discussion-created') {
@ -72,6 +73,10 @@ const Message = React.memo((props) => {
>
<MessageInner {...props} />
</View>
<ReadReceipt
isReadReceiptEnabled={props.isReadReceiptEnabled}
unread={props.unread}
/>
</View>
</View>
);
@ -119,7 +124,9 @@ Message.propTypes = {
hasError: PropTypes.bool,
style: PropTypes.any,
onLongPress: PropTypes.func,
onPress: PropTypes.func
onPress: PropTypes.func,
isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool
};
MessageInner.propTypes = {

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { COLOR_PRIMARY } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
const ReadReceipt = React.memo(({ isReadReceiptEnabled, unread }) => {
if (isReadReceiptEnabled && !unread && unread !== null) {
return <CustomIcon name='check' color={COLOR_PRIMARY} size={15} style={styles.readReceipt} />;
}
return null;
});
ReadReceipt.displayName = 'MessageReadReceipt';
ReadReceipt.propTypes = {
isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool
};
export default ReadReceipt;

View File

@ -24,6 +24,7 @@ export default class MessageContainer extends React.Component {
_updatedAt: PropTypes.instanceOf(Date),
baseUrl: PropTypes.string,
Message_GroupingPeriod: PropTypes.number,
isReadReceiptEnabled: PropTypes.bool,
useRealName: PropTypes.bool,
useMarkdown: PropTypes.bool,
status: PropTypes.number,
@ -57,6 +58,9 @@ export default class MessageContainer extends React.Component {
if (item.tmsg !== nextProps.item.tmsg) {
return true;
}
if (item.unread !== nextProps.item.unread) {
return true;
}
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
}
@ -187,10 +191,10 @@ export default class MessageContainer extends React.Component {
render() {
const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown, isReadReceiptEnabled
} = this.props;
const {
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread
} = item;
return (
@ -213,6 +217,8 @@ export default class MessageContainer extends React.Component {
broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread}
role={role}
drid={drid}
dcount={dcount}

View File

@ -234,5 +234,8 @@ export default StyleSheet.create({
flex: 1,
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
readReceipt: {
lineHeight: 20
}
});

View File

@ -127,6 +127,7 @@ export default {
Connected: 'Connected',
connecting_server: 'connecting to server',
Connecting: 'Connecting...',
Contact_us: 'Contact us',
Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy',
@ -142,9 +143,10 @@ export default {
DELETE: 'DELETE',
description: 'description',
Description: 'Description',
Directory: 'Directory',
Direct_Messages: 'Direct Messages',
Disable_notifications: 'Disable notifications',
Discussions: 'Discussions',
Direct_Messages: 'Direct Messages',
Dont_Have_An_Account: 'Don\'t have an account?',
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
edit: 'edit',
@ -189,6 +191,7 @@ export default {
leaving_room: 'leaving room',
leave: 'leave',
Legal: 'Legal',
License: 'License',
Livechat: 'Livechat',
Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.',
@ -232,6 +235,7 @@ export default {
No_Message: 'No Message',
No_messages_yet: 'No messages yet',
No_Reactions: 'No Reactions',
No_Read_Receipts: 'No Read Receipts',
Not_logged: 'Not logged',
Nothing_to_save: 'Nothing to save!',
Notify_active_in_this_room: 'Notify active users in this room',
@ -264,6 +268,7 @@ export default {
Reactions: 'Reactions',
Read_Only_Channel: 'Read Only Channel',
Read_Only: 'Read Only',
Read_Receipt: 'Read Receipt',
Register: 'Register',
Repeat_Password: 'Repeat Password',
Replied_on: 'Replied on:',
@ -294,18 +299,24 @@ export default {
saving_settings: 'saving settings',
Search_Messages: 'Search Messages',
Search: 'Search',
Search_by: 'Search by',
Search_global_users: 'Search for global users',
Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.',
Select_Avatar: 'Select Avatar',
Select_Users: 'Select Users',
Send: 'Send',
Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report',
Send_message: 'Send message',
Sent_an_attachment: 'Sent an attachment',
Server: 'Server',
Servers: 'Servers',
Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share',
Share_this_app: 'Share this app',
Sign_in_your_server: 'Sign in your server',
Sign_Up: 'Sign Up',
Some_field_is_invalid_or_empty: 'Some field is invalid or empty',
@ -322,6 +333,7 @@ export default {
tap_to_change_status: 'tap to change status',
Tap_to_view_servers_list: 'Tap to view servers list',
Terms_of_Service: ' Terms of Service ',
Theme: 'Theme',
The_URL_is_invalid: 'The URL you entered is invalid. Check it and try again, please!',
There_was_an_error_while_action: 'There was an error while {{action}}!',
This_room_is_blocked: 'This room is blocked',
@ -348,6 +360,7 @@ export default {
Updating: 'Updating...',
Uploading: 'Uploading',
Upload_file_question_mark: 'Upload file?',
Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_has_been_key: 'User has been {{key}}!',
User_is_no_longer_role_by_: '{{user}} is no longer {{role}} by {{userBy}}',
@ -374,5 +387,8 @@ export default {
you_were_mentioned: 'you were mentioned',
you: 'you',
You: 'You',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!'
Version_no: 'Version: {{version}}',
You_will_not_be_able_to_recover_this_message: 'You will not be able to recover this message!',
Change_Language: 'Change Language',
Crash_report_disclaimer: 'We never track the content of your chats. The crash report only contains relevant information for us in order '
};

View File

@ -146,11 +146,12 @@ export default {
delete: 'excluir',
Delete: 'Excluir',
DELETE: 'EXCLUIR',
Direct_Messages: 'Mensagens Diretas',
Directory: 'Diretório',
description: 'descrição',
Description: 'Descrição',
Disable_notifications: 'Desabilitar notificações',
Discussions: 'Discussões',
Direct_Messages: 'Mensagens Diretas',
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
edit: 'editar',
@ -265,6 +266,7 @@ export default {
Read_Only_Channel: 'Canal Somente Leitura',
Read_Only: 'Somente Leitura',
Register: 'Registrar',
Read_Receipt: 'Lida por',
Repeat_Password: 'Repetir Senha',
Replied_on: 'Respondido em:',
replies: 'respostas',
@ -293,6 +295,9 @@ export default {
saving_settings: 'salvando configurações',
Search_Messages: 'Buscar Mensagens',
Search: 'Buscar',
Search_by: 'Buscar por',
Search_global_users: 'Busca por usuários globais',
Search_global_users_description: 'Caso ativado, busca por usuários de outras empresas ou servidores.',
Select_Avatar: 'Selecionar Avatar',
Select_Users: 'Selecionar Usuários',
Send: 'Enviar',
@ -344,6 +349,7 @@ export default {
Updating: 'Atualizando...',
Uploading: 'Subindo arquivo',
Upload_file_question_mark: 'Enviar arquivo?',
Users: 'Usuários',
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Usuário foi {{key}}!',
User_is_no_longer_role_by_: '{{user}} não pertence mais à {{role}} por {{userBy}}',

View File

@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
import { useScreens } from 'react-native-screens'; // eslint-disable-line import/no-unresolved
import { Linking } from 'react-native';
import firebase from 'react-native-firebase';
import PropTypes from 'prop-types';
import { appInit } from './actions';
import { deepLinkingOpen } from './actions/deepLinking';
@ -16,17 +17,20 @@ import AuthLoadingView from './views/AuthLoadingView';
import RoomsListView from './views/RoomsListView';
import RoomView from './views/RoomView';
import NewMessageView from './views/NewMessageView';
import DirectoryView from './views/DirectoryView';
import LoginView from './views/LoginView';
import Navigation from './lib/Navigation';
import Sidebar from './views/SidebarView';
import ProfileView from './views/ProfileView';
import SettingsView from './views/SettingsView';
import LanguageView from './views/LanguageView';
import AdminPanelView from './views/AdminPanelView';
import RoomActionsView from './views/RoomActionsView';
import RoomInfoView from './views/RoomInfoView';
import RoomInfoEditView from './views/RoomInfoEditView';
import RoomMembersView from './views/RoomMembersView';
import SearchMessagesView from './views/SearchMessagesView';
import ReadReceiptsView from './views/ReadReceiptView';
import ThreadMessagesView from './views/ThreadMessagesView';
import MessagesView from './views/MessagesView';
import SelectedUsersView from './views/SelectedUsersView';
@ -38,8 +42,9 @@ import OAuthView from './views/OAuthView';
import SetUsernameView from './views/SetUsernameView';
import { HEADER_BACKGROUND, HEADER_TITLE, HEADER_BACK } from './constants/colors';
import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications, onNotification } from './push';
import { initializePushNotifications, onNotification } from './notifications/push';
import store from './lib/createStore';
import NotificationBadge from './notifications/inApp';
useScreens();
@ -110,7 +115,9 @@ const ChatsStack = createStackNavigator({
SearchMessagesView,
SelectedUsersView,
ThreadMessagesView,
MessagesView
MessagesView,
ReadReceiptsView,
DirectoryView
}, {
defaultNavigationOptions: defaultHeader
});
@ -142,7 +149,8 @@ ProfileStack.navigationOptions = ({ navigation }) => {
};
const SettingsStack = createStackNavigator({
SettingsView
SettingsView,
LanguageView
}, {
defaultNavigationOptions: defaultHeader
});
@ -193,10 +201,28 @@ const SetUsernameStack = createStackNavigator({
SetUsernameView
});
class CustomInsideStack extends React.Component {
static router = InsideStackModal.router;
static propTypes = {
navigation: PropTypes.object
}
render() {
const { navigation } = this.props;
return (
<React.Fragment>
<InsideStackModal navigation={navigation} />
<NotificationBadge navigation={navigation} />
</React.Fragment>
);
}
}
const App = createAppContainer(createSwitchNavigator(
{
OutsideStack: OutsideStackModal,
InsideStack: InsideStackModal,
InsideStack: CustomInsideStack,
AuthLoading: AuthLoadingView,
SetUsernameStack
},

View File

@ -0,0 +1,31 @@
import { InteractionManager } from 'react-native';
import database from '../realm';
import log from '../../utils/log';
export default async function() {
try {
// RC 0.60.2
const result = await this.sdk.get('commands.list');
if (!result.success) {
return log('getSlashCommand fetch', result);
}
const { commands } = result;
if (commands && commands.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => commands.forEach((command) => {
try {
database.create('slashCommand', command, true);
} catch (e) {
log('get_slash_command', e);
}
}));
});
}
} catch (e) {
log('err_get_slash_command', e);
}
}

View File

@ -26,6 +26,7 @@ export default (msg) => {
msg = normalizeAttachments(msg);
msg.reactions = msg.reactions || [];
msg.unread = msg.unread || false;
// TODO: api problems
// if (Array.isArray(msg.reactions)) {
// msg.reactions = msg.reactions.map((value, key) => ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) }));

View File

@ -6,6 +6,7 @@ import log from '../../../utils/log';
import random from '../../../utils/random';
import store from '../../createStore';
import { roomsRequest } from '../../../actions/rooms';
import { notificationReceived } from '../../../actions/notification';
const removeListener = listener => listener.stop();
@ -120,6 +121,10 @@ export default async function subscribeRooms() {
}
});
}
if (/notification/.test(ev)) {
const [notification] = ddpMessage.fields.args;
store.dispatch(notificationReceived(notification));
}
});
const stop = () => {

View File

@ -197,7 +197,8 @@ const messagesSchema = {
tlm: { type: 'date', optional: true },
replies: 'string[]',
mentions: { type: 'list', objectType: 'users' },
channels: { type: 'list', objectType: 'rooms' }
channels: { type: 'list', objectType: 'rooms' },
unread: { type: 'bool', optional: true }
}
};
@ -272,6 +273,18 @@ const frequentlyUsedEmojiSchema = {
}
};
const slashCommandSchema = {
name: 'slashCommand',
primaryKey: 'command',
properties: {
command: 'string',
params: { type: 'string', optional: true },
description: { type: 'string', optional: true },
clientOnly: { type: 'bool', optional: true },
providesPreview: { type: 'bool', optional: true }
}
};
const customEmojisSchema = {
name: 'customEmojis',
primaryKey: '_id',
@ -346,7 +359,8 @@ const schema = [
customEmojisSchema,
messagesReactionsSchema,
rolesSchema,
uploadsSchema
uploadsSchema,
slashCommandSchema
];
const inMemorySchema = [usersTypingSchema, activeUsersSchema];
@ -415,7 +429,7 @@ class DB {
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
schemaVersion: 11,
schemaVersion: 12,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
const newSubs = newRealm.objects('subscriptions');

View File

@ -5,7 +5,7 @@ import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import reduxStore from './createStore';
import defaultSettings from '../constants/settings';
import messagesStatus from '../constants/messagesStatus';
import database, { safeAddListener } from './realm';
import database from './realm';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import EventEmitter from '../utils/events';
@ -25,6 +25,7 @@ import getSettings from './methods/getSettings';
import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis';
import getSlashCommands from './methods/getSlashCommands';
import getRoles from './methods/getRoles';
import canOpenRoom from './methods/canOpenRoom';
@ -35,7 +36,7 @@ import loadThreadMessages from './methods/loadThreadMessages';
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import { getDeviceToken } from '../push';
import { getDeviceToken } from '../notifications/push';
import { roomsRequest } from '../actions/rooms';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
@ -57,23 +58,6 @@ const RocketChat = {
// RC 0.51.0
return this.sdk.methodCall(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
},
async createDirectMessageAndWait(username) {
const room = await RocketChat.createDirectMessage(username);
return new Promise((resolve) => {
const data = database.objects('subscriptions')
.filtered('rid = $1', room.rid);
if (data.length) {
return resolve(data[0]);
}
safeAddListener(data, () => {
if (!data.length) { return; }
data.removeAllListeners();
resolve(data[0]);
});
});
},
async getUserToken() {
try {
return await AsyncStorage.getItem(TOKEN_KEY);
@ -170,75 +154,80 @@ const RocketChat = {
this.getPermissions();
this.getCustomEmoji();
this.getRoles();
this.getSlashCommands();
this.registerPushToken().catch(e => console.log(e));
this.getUserPresence();
},
connect({ server, user }) {
database.setActiveDB(server);
reduxStore.dispatch(connectRequest());
return new Promise((resolve) => {
database.setActiveDB(server);
reduxStore.dispatch(connectRequest());
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
}
if (this.sdk) {
this.sdk.disconnect();
this.sdk = null;
}
if (this.sdk) {
this.sdk.disconnect();
this.sdk = null;
}
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
// Use useSsl: false only if server url starts with http://
const useSsl = !/http:\/\//.test(server);
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.getSettings();
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.getSettings();
this.sdk.connect()
.then(() => {
if (user && user.token) {
reduxStore.dispatch(loginRequest({ resume: user.token }));
this.sdk.connect()
.then(() => {
if (user && user.token) {
reduxStore.dispatch(loginRequest({ resume: user.token }));
}
})
.catch((err) => {
console.log('connect error', err);
// when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => {
this.connect({ server, user });
}, 10000);
});
this.sdk.onStreamData('connected', () => {
reduxStore.dispatch(connectSuccess());
const { isAuthenticated } = reduxStore.getState().login;
if (isAuthenticated) {
this.getUserPresence();
}
})
.catch((err) => {
console.log('connect error', err);
// when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => {
this.connect({ server, user });
}, 10000);
});
this.sdk.onStreamData('connected', () => {
reduxStore.dispatch(connectSuccess());
const { isAuthenticated } = reduxStore.getState().login;
if (isAuthenticated) {
this.getUserPresence();
}
});
this.sdk.onStreamData('close', () => {
reduxStore.dispatch(disconnect());
});
this.sdk.onStreamData('close', () => {
reduxStore.dispatch(disconnect());
});
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => {
const { eventName } = ddpMessage.fields;
if (eventName === 'user-status') {
const userStatus = ddpMessage.fields.args[0];
const [id, username, status] = userStatus;
if (username) {
database.memoryDatabase.write(() => {
try {
database.memoryDatabase.create('activeUsers', {
id, username, status: STATUSES[status]
}, true);
} catch (error) {
console.log(error);
}
});
this.sdk.onStreamData('stream-notify-logged', protectedFunction((ddpMessage) => {
const { eventName } = ddpMessage.fields;
if (eventName === 'user-status') {
const userStatus = ddpMessage.fields.args[0];
const [id, username, status] = userStatus;
if (username) {
database.memoryDatabase.write(() => {
try {
database.memoryDatabase.create('activeUsers', {
id, username, status: STATUSES[status]
}, true);
} catch (error) {
console.log(error);
}
});
}
}
}
}));
}));
resolve();
});
},
register(credentials) {
@ -480,6 +469,7 @@ const RocketChat = {
getSettings,
getPermissions,
getCustomEmoji,
getSlashCommands,
getRoles,
parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[defaultSettings[item._id].type];
@ -644,9 +634,9 @@ const RocketChat = {
// RC 0.55.0
return this.sdk.methodCall('saveRoomSettings', rid, params);
},
saveUserProfile(data) {
saveUserProfile(data, customFields) {
// RC 0.62.2
return this.sdk.post('users.updateOwnBasicInfo', { data });
return this.sdk.post('users.updateOwnBasicInfo', { data, customFields });
},
saveUserPreferences(params) {
// RC 0.51.0
@ -784,6 +774,12 @@ const RocketChat = {
sort: { ts: -1 }
});
},
getReadReceipts(messageId) {
return this.sdk.get('chat.getMessageReadReceipts', {
messageId
});
},
searchMessages(roomId, searchText) {
// RC 0.60.0
return this.sdk.get('chat.search', {
@ -810,6 +806,24 @@ const RocketChat = {
rid, updatedSince
});
},
runSlashCommand(command, roomId, params) {
// RC 0.60.2
return this.sdk.post('commands.run', {
command, roomId, params
});
},
getCommandPreview(command, roomId, params) {
// RC 0.65.0
return this.sdk.get('commands.preview', {
command, roomId, params
});
},
executeCommandPreview(command, params, roomId, previewItem) {
// RC 0.65.0
return this.sdk.post('commands.preview', {
command, params, roomId, previewItem
});
},
async getUserPresence() {
const serverVersion = reduxStore.getState().server.version;
@ -845,6 +859,14 @@ const RocketChat = {
this.sdk.subscribe('stream-notify-logged', 'user-status');
}
}
},
getDirectory({
query, count, offset, sort
}) {
// RC 1.0
return this.sdk.get('directory', {
query, count, offset, sort
});
}
};

View File

@ -0,0 +1,229 @@
import React from 'react';
import {
View, Text, StyleSheet, TouchableOpacity, Animated, Easing
} from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import equal from 'deep-equal';
import { responsive } from 'react-native-responsive-ui';
import Touchable from 'react-native-platform-touchable';
import { isNotch, isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_BACKGROUND_NOTIFICATION, COLOR_SEPARATOR, COLOR_TEXT } from '../../constants/colors';
import Avatar from '../../containers/Avatar';
import { removeNotification as removeNotificationAction } from '../../actions/notification';
import sharedStyles from '../../views/Styles';
import { ROW_HEIGHT } from '../../presentation/RoomItem';
const AVATAR_SIZE = 48;
const ANIMATION_DURATION = 300;
const NOTIFICATION_DURATION = 3000;
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
const ANIMATION_PROPS = {
duration: ANIMATION_DURATION,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true
};
const styles = StyleSheet.create({
container: {
height: ROW_HEIGHT,
paddingHorizontal: 14,
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
position: 'absolute',
zIndex: 2,
backgroundColor: COLOR_BACKGROUND_NOTIFICATION,
width: '100%',
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLOR_SEPARATOR
},
content: {
flex: 1,
flexDirection: 'row',
alignItems: 'center'
},
avatar: {
marginRight: 10
},
roomName: {
fontSize: 17,
lineHeight: 20,
...sharedStyles.textColorNormal,
...sharedStyles.textMedium
},
message: {
fontSize: 14,
lineHeight: 17,
...sharedStyles.textRegular,
...sharedStyles.textColorNormal
},
close: {
color: COLOR_TEXT,
marginLeft: 10
}
});
@responsive
@connect(
state => ({
userId: state.login.user && state.login.user.id,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
token: state.login.user && state.login.user.token,
notification: state.notification
}),
dispatch => ({
removeNotification: () => dispatch(removeNotificationAction())
})
)
export default class NotificationBadge extends React.Component {
static propTypes = {
navigation: PropTypes.object,
baseUrl: PropTypes.string,
token: PropTypes.string,
userId: PropTypes.string,
notification: PropTypes.object,
window: PropTypes.object,
removeNotification: PropTypes.func
}
constructor(props) {
super(props);
this.animatedValue = new Animated.Value(0);
}
shouldComponentUpdate(nextProps) {
const { notification: nextNotification } = nextProps;
const {
notification: { payload }, window
} = this.props;
if (!equal(nextNotification.payload, payload)) {
return true;
}
if (nextProps.window.width !== window.width) {
return true;
}
return false;
}
componentDidUpdate() {
const { notification: { payload }, navigation } = this.props;
const navState = this.getNavState(navigation.state);
if (payload.rid) {
if (navState && navState.routeName === 'RoomView' && navState.params && navState.params.rid === payload.rid) {
return;
}
this.show();
}
}
componentWillUnmount() {
this.clearTimeout();
}
show = () => {
Animated.timing(
this.animatedValue,
{
toValue: 1,
...ANIMATION_PROPS
},
).start(() => {
this.clearTimeout();
this.timeout = setTimeout(() => {
this.hide();
}, NOTIFICATION_DURATION);
});
}
hide = () => {
const { removeNotification } = this.props;
Animated.timing(
this.animatedValue,
{
toValue: 0,
...ANIMATION_PROPS
},
).start();
setTimeout(removeNotification, ANIMATION_DURATION);
}
clearTimeout = () => {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
getNavState = (routes) => {
if (!routes.routes) {
return routes;
}
return this.getNavState(routes.routes[routes.index]);
}
goToRoom = async() => {
const { notification: { payload }, navigation } = this.props;
const { rid, type, prid } = payload;
if (!rid) {
return;
}
const name = type === 'p' ? payload.name : payload.sender.username;
await navigation.navigate('RoomsListView');
navigation.navigate('RoomView', {
rid, name, t: type, prid
});
this.hide();
}
render() {
const {
baseUrl, token, userId, notification, window
} = this.props;
const { message, payload } = notification;
const { type } = payload;
const name = type === 'p' ? payload.name : payload.sender.username;
let top = 0;
if (isIOS) {
const portrait = window.height > window.width;
if (portrait) {
top = isNotch ? 45 : 20;
} else {
top = 0;
}
}
const maxWidthMessage = window.width - 110;
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-top - ROW_HEIGHT, top]
});
return (
<Animated.View style={[styles.container, { transform: [{ translateY }] }]}>
<Touchable
style={styles.content}
onPress={this.goToRoom}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<React.Fragment>
<Avatar text={name} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<View>
<Text style={styles.roomName}>{name}</Text>
<Text style={[styles.message, { maxWidth: maxWidthMessage }]} numberOfLines={1}>{message}</Text>
</View>
</React.Fragment>
</Touchable>
<TouchableOpacity onPress={this.hide}>
<CustomIcon name='circle-cross' style={styles.close} size={20} />
</TouchableOpacity>
</Animated.View>
);
}
}

View File

@ -1,8 +1,8 @@
import EJSON from 'ejson';
import PushNotification from './push';
import store from '../lib/createStore';
import { deepLinkingOpen } from '../actions/deepLinking';
import store from '../../lib/createStore';
import { deepLinkingOpen } from '../../actions/deepLinking';
export const onNotification = (notification) => {
if (notification) {

View File

@ -9,6 +9,7 @@ import selectedUsers from './selectedUsers';
import createChannel from './createChannel';
import app from './app';
import sortPreferences from './sortPreferences';
import notification from './notification';
import markdown from './markdown';
export default combineReducers({
@ -22,5 +23,6 @@ export default combineReducers({
app,
rooms,
sortPreferences,
notification,
markdown
});

View File

@ -0,0 +1,24 @@
import { NOTIFICATION } from '../actions/actionsTypes';
const initialState = {
message: '',
payload: {
type: 'p',
name: '',
rid: ''
}
};
export default function notification(state = initialState, action) {
switch (action.type) {
case NOTIFICATION.RECEIVED:
return {
...state,
...action.payload
};
case NOTIFICATION.REMOVE:
return initialState;
default:
return state;
}
}

View File

@ -11,11 +11,13 @@ import database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
const getServerInfo = function* getServerInfo({ server }) {
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
try {
const serverInfo = yield RocketChat.getServerInfo(server);
if (!serverInfo.success) {
Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions));
if (raiseError) {
Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions));
}
yield put(serverFailure());
return;
}
@ -32,27 +34,29 @@ const getServerInfo = function* getServerInfo({ server }) {
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try {
let serverInfo;
if (fetchVersion) {
serverInfo = yield getServerInfo({ server });
}
yield AsyncStorage.setItem('currentServer', server);
const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (userStringified) {
const user = JSON.parse(userStringified);
RocketChat.connect({ server, user });
yield RocketChat.connect({ server, user });
yield put(setUser(user));
yield put(actions.appStart('inside'));
} else {
RocketChat.connect({ server });
yield RocketChat.connect({ server });
yield put(actions.appStart('outside'));
}
const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
let serverInfo;
if (fetchVersion) {
serverInfo = yield getServerInfo({ server, raiseError: false });
}
// Return server version even when offline
yield put(selectServerSuccess(server, (serverInfo && serverInfo.version) || version));
} catch (e) {
log('err_select_server', e);
}
@ -62,7 +66,6 @@ const handleServerRequest = function* handleServerRequest({ server }) {
try {
const serverInfo = yield getServerInfo({ server });
// TODO: cai aqui O.o
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
Navigation.navigate('LoginView');

View File

@ -2,7 +2,7 @@ import { takeLatest, select } from 'redux-saga/effects';
import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate';
import RocketChat from '../lib/rocketchat';
import { setBadgeCount } from '../push';
import { setBadgeCount } from '../notifications/push';
import log from '../utils/log';
const appHasComeBackToForeground = function* appHasComeBackToForeground() {

View File

@ -8,11 +8,4 @@ export const isIOS = Platform.OS === 'ios';
export const isAndroid = !isIOS;
export const getReadableVersion = DeviceInfo.getReadableVersion();
export const getBundleId = DeviceInfo.getBundleId();
export default {
isNotch,
isIOS,
isAndroid,
getReadableVersion,
getBundleId
};
export const getDeviceModel = DeviceInfo.getModel();

View File

@ -0,0 +1,63 @@
import React from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Avatar from '../../containers/Avatar';
import Touch from '../../utils/touch';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles';
const DirectoryItemLabel = React.memo(({ text }) => {
if (!text) {
return null;
}
return <Text style={styles.directoryItemLabel}>{text}</Text>;
});
const DirectoryItem = ({
title, description, avatar, onPress, testID, style, baseUrl, user, rightLabel, type
}) => (
<Touch onPress={onPress} style={styles.directoryItemButton} testID={testID}>
<View style={[styles.directoryItemContainer, style]}>
<Avatar
text={avatar}
size={30}
type={type}
style={styles.directoryItemAvatar}
baseUrl={baseUrl}
userId={user.id}
token={user.token}
/>
<View style={styles.directoryItemTextContainer}>
<View style={styles.directoryItemTextTitle}>
<RoomTypeIcon type='c' />
<Text style={styles.directoryItemName} numberOfLines={1}>{title}</Text>
</View>
<Text style={styles.directoryItemUsername} numberOfLines={1}>{description}</Text>
</View>
<DirectoryItemLabel text={rightLabel} />
</View>
</Touch>
);
DirectoryItem.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
avatar: PropTypes.string,
type: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
baseUrl: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired,
style: PropTypes.any,
rightLabel: PropTypes.string
};
DirectoryItemLabel.propTypes = {
text: PropTypes.string
};
export default DirectoryItem;

View File

@ -0,0 +1,121 @@
import React, { PureComponent } from 'react';
import {
View, Text, Animated, Easing, TouchableWithoutFeedback, Switch
} from 'react-native';
import PropTypes from 'prop-types';
import Touch from '../../utils/touch';
import styles from './styles';
import { CustomIcon } from '../../lib/Icons';
import Check from '../../containers/Check';
import I18n from '../../i18n';
const ANIMATION_DURATION = 200;
const ANIMATION_PROPS = {
duration: ANIMATION_DURATION,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true
};
export default class DirectoryOptions extends PureComponent {
static propTypes = {
type: PropTypes.string,
globalUsers: PropTypes.bool,
isFederationEnabled: PropTypes.bool,
close: PropTypes.func,
changeType: PropTypes.func,
toggleWorkspace: PropTypes.func
}
constructor(props) {
super(props);
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
Animated.timing(
this.animatedValue,
{
toValue: 1,
...ANIMATION_PROPS
},
).start();
}
close = () => {
const { close } = this.props;
Animated.timing(
this.animatedValue,
{
toValue: 0,
...ANIMATION_PROPS
},
).start(() => close());
}
renderItem = (itemType) => {
const { changeType, type: propType } = this.props;
let text = 'Users';
let icon = 'user';
if (itemType === 'channels') {
text = 'Channels';
icon = 'hashtag';
}
return (
<Touch style={styles.dropdownItemButton} onPress={() => changeType(itemType)}>
<View style={styles.dropdownItemContainer}>
<CustomIcon style={styles.dropdownItemIcon} size={22} name={icon} />
<Text style={styles.dropdownItemText}>{I18n.t(text)}</Text>
{propType === itemType ? <Check /> : null}
</View>
</Touch>
);
}
render() {
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-326, 0]
});
const backdropOpacity = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.3]
});
const { globalUsers, toggleWorkspace, isFederationEnabled } = this.props;
return (
<React.Fragment>
<TouchableWithoutFeedback onPress={this.close}>
<Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} />
</TouchableWithoutFeedback>
<Animated.View style={[styles.dropdownContainer, { transform: [{ translateY }] }]}>
<Touch
onPress={this.close}
style={styles.dropdownContainerHeader}
>
<View style={styles.dropdownItemContainer}>
<Text style={styles.dropdownToggleText}>{I18n.t('Search_by')}</Text>
<CustomIcon style={[styles.dropdownItemIcon, styles.inverted]} size={22} name='arrow-down' />
</View>
</Touch>
{this.renderItem('channels')}
{this.renderItem('users')}
{isFederationEnabled
? (
<React.Fragment>
<View style={styles.dropdownSeparator} />
<View style={[styles.dropdownItemContainer, styles.globalUsersContainer]}>
<View style={styles.globalUsersTextContainer}>
<Text style={styles.dropdownItemText}>{I18n.t('Search_global_users')}</Text>
<Text style={styles.dropdownItemDescription}>{I18n.t('Search_global_users_description')}</Text>
</View>
<Switch value={globalUsers} onValueChange={toggleWorkspace} />
</View>
</React.Fragment>
)
: null}
</Animated.View>
</React.Fragment>
);
}
}

View File

@ -0,0 +1,248 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
View, FlatList, Text
} from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import RocketChat from '../../lib/rocketchat';
import DirectoryItem from './DirectoryItem';
import sharedStyles from '../Styles';
import I18n from '../../i18n';
import Touch from '../../utils/touch';
import SearchBox from '../../containers/SearchBox';
import { CustomIcon } from '../../lib/Icons';
import StatusBar from '../../containers/StatusBar';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import debounce from '../../utils/debounce';
import log from '../../utils/log';
import Options from './Options';
import styles from './styles';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
},
isFederationEnabled: state.settings.FEDERATION_Enabled
}))
export default class DirectoryView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Directory')
})
static propTypes = {
navigation: PropTypes.object,
baseUrl: PropTypes.string,
isFederationEnabled: PropTypes.bool,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
})
};
constructor(props) {
super(props);
this.state = {
data: [],
loading: false,
text: '',
total: -1,
showOptionsDropdown: false,
globalUsers: true,
type: 'channels'
};
}
componentDidMount() {
this.load({});
}
onSearchChangeText = (text) => {
this.setState({ text });
}
onPressItem = (item) => {
const { navigation } = this.props;
try {
const onPressItem = navigation.getParam('onPressItem', () => {});
onPressItem(item);
} catch (error) {
console.log('DirectoryView -> onPressItem -> error', error);
}
}
// eslint-disable-next-line react/sort-comp
load = debounce(async({ newSearch = false }) => {
if (newSearch) {
this.setState({ data: [], total: -1, loading: false });
}
const {
loading, text, total, data: { length }
} = this.state;
if (loading || length === total) {
return;
}
this.setState({ loading: true });
try {
const { data, type, globalUsers } = this.state;
const query = { text, type, workspace: globalUsers ? 'all' : 'local' };
const directories = await RocketChat.getDirectory({
query,
offset: data.length,
count: 50,
sort: (type === 'users') ? { username: 1 } : { usersCount: -1 }
});
if (directories.success) {
this.setState({
data: [...data, ...directories.result],
loading: false,
total: directories.total
});
} else {
this.setState({ loading: false });
}
} catch (error) {
log('err_load_directory', error);
this.setState({ loading: false });
}
}, 200)
search = () => {
this.load({ newSearch: true });
}
changeType = (type) => {
this.setState({ type, data: [] }, () => this.search());
}
toggleWorkspace = () => {
this.setState(({ globalUsers }) => ({ globalUsers: !globalUsers, data: [] }), () => this.search());
}
toggleDropdown = () => {
this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown }));
}
goRoom = async({ rid, name, t }) => {
const { navigation } = this.props;
await navigation.navigate('RoomsListView');
navigation.navigate('RoomView', { rid, name, t });
}
onPressItem = async(item) => {
const { type } = this.state;
if (type === 'users') {
const result = await RocketChat.createDirectMessage(item.username);
if (result.success) {
this.goRoom({ rid: result.room._id, name: item.username, t: 'd' });
}
} else {
this.goRoom({ rid: item._id, name: item.name, t: 'c' });
}
}
renderHeader = () => {
const { type } = this.state;
return (
<React.Fragment>
<SearchBox
onChangeText={this.onSearchChangeText}
onSubmitEditing={this.search}
testID='federation-view-search'
/>
<Touch onPress={this.toggleDropdown} testID='federation-view-create-channel'>
<View style={[sharedStyles.separatorVertical, styles.toggleDropdownContainer]}>
<CustomIcon style={styles.toggleDropdownIcon} size={20} name={type === 'users' ? 'user' : 'hashtag'} />
<Text style={styles.toggleDropdownText}>{type === 'users' ? I18n.t('Users') : I18n.t('Channels')}</Text>
<CustomIcon name='arrow-down' size={20} style={styles.toggleDropdownArrow} />
</View>
</Touch>
</React.Fragment>
);
}
renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />;
renderItem = ({ item, index }) => {
const { data, type } = this.state;
const { baseUrl, user } = this.props;
let style;
if (index === data.length - 1) {
style = sharedStyles.separatorBottom;
}
const commonProps = {
title: item.name,
onPress: () => this.onPressItem(item),
baseUrl,
testID: `federation-view-item-${ item.name }`,
style,
user
};
if (type === 'users') {
return (
<DirectoryItem
avatar={item.username}
description={item.username}
rightLabel={item.federation && item.federation.peer}
type='d'
{...commonProps}
/>
);
}
return (
<DirectoryItem
avatar={item.name}
description={item.topic}
rightLabel={I18n.t('N_users', { n: item.usersCount })}
type='c'
{...commonProps}
/>
);
}
render = () => {
const {
data, loading, showOptionsDropdown, type, globalUsers
} = this.state;
const { isFederationEnabled } = this.props;
return (
<SafeAreaView style={styles.safeAreaView} testID='directory-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
<FlatList
data={data}
style={styles.list}
contentContainerStyle={styles.listContainer}
extraData={this.state}
keyExtractor={item => item._id}
ListHeaderComponent={this.renderHeader}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
keyboardShouldPersistTaps='always'
ListFooterComponent={loading ? <RCActivityIndicator /> : null}
onEndReached={() => this.load({})}
/>
{showOptionsDropdown
? (
<Options
type={type}
globalUsers={globalUsers}
close={this.toggleDropdown}
changeType={this.changeType}
toggleWorkspace={this.toggleWorkspace}
isFederationEnabled={isFederationEnabled}
/>
)
: null}
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,151 @@
import { StyleSheet } from 'react-native';
import { COLOR_WHITE, COLOR_SEPARATOR, COLOR_PRIMARY } from '../../constants/colors';
import { isIOS } from '../../utils/deviceInfo';
import sharedStyles from '../Styles';
export default StyleSheet.create({
safeAreaView: {
flex: 1,
backgroundColor: isIOS ? '#F7F8FA' : '#E1E5E8'
},
list: {
flex: 1
},
listContainer: {
paddingBottom: 30
},
separator: {
marginLeft: 60
},
toggleDropdownContainer: {
height: 47,
backgroundColor: COLOR_WHITE,
flexDirection: 'row',
alignItems: 'center'
},
toggleDropdownIcon: {
color: COLOR_PRIMARY,
marginLeft: 20,
marginRight: 17
},
toggleDropdownText: {
flex: 1,
color: COLOR_PRIMARY,
fontSize: 17,
...sharedStyles.textRegular
},
toggleDropdownArrow: {
...sharedStyles.textColorDescription,
marginRight: 15
},
dropdownContainer: {
backgroundColor: COLOR_WHITE,
width: '100%',
position: 'absolute',
top: 0
},
backdrop: {
...StyleSheet.absoluteFill,
backgroundColor: '#000000'
},
dropdownContainerHeader: {
height: 47,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLOR_SEPARATOR,
alignItems: 'center',
backgroundColor: isIOS ? COLOR_WHITE : '#54585E',
flexDirection: 'row'
},
dropdownItemButton: {
height: 57,
justifyContent: 'center'
},
dropdownItemContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center'
},
dropdownItemText: {
fontSize: 18,
flex: 1,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
dropdownItemDescription: {
fontSize: 14,
flex: 1,
marginTop: 2,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
dropdownToggleText: {
fontSize: 15,
flex: 1,
marginLeft: 15,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
dropdownItemIcon: {
width: 22,
height: 22,
marginHorizontal: 15,
...sharedStyles.textColorDescription
},
dropdownSeparator: {
height: StyleSheet.hairlineWidth,
backgroundColor: COLOR_SEPARATOR,
marginHorizontal: 15,
flex: 1
},
directoryItemButton: {
height: 54,
backgroundColor: COLOR_WHITE
},
directoryItemContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 15
},
directoryItemAvatar: {
marginRight: 12
},
directoryItemTextTitle: {
flexDirection: 'row',
alignItems: 'center'
},
directoryItemTextContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
directoryItemName: {
flex: 1,
fontSize: 17,
...sharedStyles.textMedium,
...sharedStyles.textColorNormal
},
directoryItemUsername: {
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
directoryItemLabel: {
fontSize: 14,
paddingLeft: 10,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription
},
inverted: {
transform: [{ scaleY: -1 }]
},
globalUsersContainer: {
padding: 15
},
globalUsersTextContainer: {
flex: 1,
flexDirection: 'column'
}
});

View File

@ -0,0 +1,158 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView, NavigationActions } from 'react-navigation';
import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n';
import Loading from '../../containers/Loading';
import { showErrorAlert } from '../../utils/info';
import log from '../../utils/log';
import { setUser as setUserAction } from '../../actions/login';
import StatusBar from '../../containers/StatusBar';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../Styles';
import ListItem from '../../containers/ListItem';
import Separator from '../../containers/Separator';
const LANGUAGES = [
{
label: '简体中文',
value: 'zh-CN'
}, {
label: 'Deutsch',
value: 'de'
}, {
label: 'English',
value: 'en'
}, {
label: 'Français',
value: 'fr'
}, {
label: 'Português (BR)',
value: 'pt-BR'
}, {
label: 'Português (PT)',
value: 'pt-PT'
}, {
label: 'Russian',
value: 'ru'
}
];
@connect(state => ({
userLanguage: state.login.user && state.login.user.language
}), dispatch => ({
setUser: params => dispatch(setUserAction(params))
}))
/** @extends React.Component */
export default class LanguageView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Change_Language')
})
static propTypes = {
userLanguage: PropTypes.string,
navigation: PropTypes.object,
setUser: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
language: props.userLanguage ? props.userLanguage : 'en',
saving: false
};
}
shouldComponentUpdate(nextProps, nextState) {
const { language, saving } = this.state;
const { userLanguage } = this.props;
if (nextState.language !== language) {
return true;
}
if (nextState.saving !== saving) {
return true;
}
if (nextProps.userLanguage !== userLanguage) {
return true;
}
return false;
}
formIsChanged = (language) => {
const { userLanguage } = this.props;
return (userLanguage !== language);
}
submit = async(language) => {
if (!this.formIsChanged(language)) {
return;
}
this.setState({ saving: true });
const { userLanguage, setUser, navigation } = this.props;
const params = {};
// language
if (userLanguage !== language) {
params.language = language;
}
try {
await RocketChat.saveUserPreferences(params);
setUser({ language: params.language });
this.setState({ saving: false });
setTimeout(() => {
navigation.reset([NavigationActions.navigate({ routeName: 'SettingsView' })], 0);
navigation.navigate('RoomsListView');
}, 300);
} catch (e) {
this.setState({ saving: false });
setTimeout(() => {
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log('err_save_user_preferences', e);
}, 300);
}
}
renderSeparator = () => <Separator />
renderIcon = () => <CustomIcon name='check' size={20} style={sharedStyles.colorPrimary} />
renderItem = ({ item }) => {
const { value, label } = item;
const { language } = this.state;
const isSelected = language === value;
return (
<ListItem
title={label}
onPress={() => this.submit(value)}
testID={`language-view-${ value }`}
right={isSelected ? this.renderIcon : null}
/>
);
}
render() {
const { saving } = this.state;
return (
<SafeAreaView style={sharedStyles.listSafeArea} testID='language-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
<FlatList
data={LANGUAGES}
keyExtractor={item => item.value}
contentContainerStyle={sharedStyles.listContentContainer}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
/>
<Loading visible={saving} />
</SafeAreaView>
);
}
}

View File

@ -40,7 +40,8 @@ const styles = StyleSheet.create({
},
createChannelIcon: {
color: COLOR_PRIMARY,
marginHorizontal: 18
marginLeft: 18,
marginRight: 15
},
createChannelText: {
color: COLOR_PRIMARY,

View File

@ -210,12 +210,13 @@ export default class ProfileView extends React.Component {
}
}
params.customFields = customFields;
const result = await RocketChat.saveUserProfile(params, customFields);
const result = await RocketChat.saveUserProfile(params);
if (result.success) {
if (params.customFields) {
setUser({ customFields });
if (customFields) {
setUser({ customFields, ...params });
} else {
setUser({ ...params });
}
this.setState({ saving: false });
this.toast.show(I18n.t('Profile_saved_successfully'));

View File

@ -0,0 +1,146 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList, View, Text } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import moment from 'moment';
import { connect } from 'react-redux';
import Avatar from '../../containers/Avatar';
import styles from './styles';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar';
@connect(state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
userId: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
}))
export default class ReadReceiptsView extends React.Component {
static navigationOptions = {
title: I18n.t('Read_Receipt')
}
static propTypes = {
navigation: PropTypes.object,
Message_TimeFormat: PropTypes.string,
baseUrl: PropTypes.string,
userId: PropTypes.string,
token: PropTypes.string
}
constructor(props) {
super(props);
this.messageId = props.navigation.getParam('messageId');
this.state = {
loading: false,
receipts: []
};
}
componentDidMount() {
this.load();
}
shouldComponentUpdate(nextProps, nextState) {
const { loading, receipts } = this.state;
if (nextState.loading !== loading) {
return true;
}
if (!equal(nextState.receipts, receipts)) {
return true;
}
return false;
}
load = async() => {
const { loading } = this.state;
if (loading) {
return;
}
this.setState({ loading: true });
try {
const result = await RocketChat.getReadReceipts(this.messageId);
if (result.success) {
this.setState({
receipts: result.receipts,
loading: false
});
}
} catch (error) {
this.setState({ loading: false });
console.log('err_fetch_read_receipts', error);
}
}
renderEmpty = () => (
<View style={styles.listEmptyContainer} testID='read-receipt-view'>
<Text>{I18n.t('No_Read_Receipts')}</Text>
</View>
)
renderItem = ({ item }) => {
const {
Message_TimeFormat, userId, baseUrl, token
} = this.props;
const time = moment(item.ts).format(Message_TimeFormat);
return (
<View style={styles.itemContainer}>
<Avatar
text={item.user.username}
size={40}
baseUrl={baseUrl}
userId={userId}
token={token}
/>
<View style={styles.infoContainer}>
<View style={styles.item}>
<Text style={styles.name}>
{item.user.name}
</Text>
<Text>
{time}
</Text>
</View>
<Text>
{`@${ item.user.username }`}
</Text>
</View>
</View>
);
}
renderSeparator = () => <View style={styles.separator} />;
render() {
const { receipts, loading } = this.state;
if (!loading && receipts.length === 0) {
return this.renderEmpty();
}
return (
<SafeAreaView style={styles.container} testID='read-receipt-view' forceInset={{ bottom: 'always' }}>
<StatusBar />
<View>
{loading
? <RCActivityIndicator />
: (
<FlatList
data={receipts}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
style={styles.list}
keyExtractor={item => item._id}
/>
)}
</View>
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,50 @@
import { StyleSheet } from 'react-native';
import { COLOR_SEPARATOR, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER } from '../../constants/colors';
import sharedStyles from '../Styles';
export default StyleSheet.create({
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
item: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between'
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: COLOR_SEPARATOR
},
name: {
...sharedStyles.textRegular,
...sharedStyles.textColorTitle,
fontSize: 17
},
username: {
flex: 1,
...sharedStyles.textRegular,
...sharedStyles.textColorDescription,
fontSize: 14
},
infoContainer: {
flex: 1,
marginLeft: 10
},
itemContainer: {
flex: 1,
flexDirection: 'row',
padding: 10,
backgroundColor: COLOR_WHITE
},
container: {
flex: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
list: {
...sharedStyles.separatorVertical,
marginVertical: 10
}
});

View File

@ -60,7 +60,8 @@ import { Toast } from '../../utils/info';
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
Message_TimeFormat: state.settings.Message_TimeFormat,
useMarkdown: state.markdown.useMarkdown,
baseUrl: state.settings.baseUrl || state.server ? state.server.server : ''
baseUrl: state.settings.baseUrl || state.server ? state.server.server : '',
Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled
}), dispatch => ({
editCancel: () => dispatch(editCancelAction()),
replyCancel: () => dispatch(replyCancelAction()),
@ -116,6 +117,7 @@ export default class RoomView extends React.Component {
isAuthenticated: PropTypes.bool,
Message_GroupingPeriod: PropTypes.number,
Message_TimeFormat: PropTypes.string,
Message_Read_Receipt_Enabled: PropTypes.bool,
editing: PropTypes.bool,
replying: PropTypes.bool,
baseUrl: PropTypes.string,
@ -227,7 +229,7 @@ export default class RoomView extends React.Component {
componentWillUnmount() {
this.mounted = false;
const { editing, replying } = this.props;
if (!editing && this.messagebox && this.messagebox.current && this.messagebox.current.text) {
if (!editing && this.messagebox && this.messagebox.current) {
const { text } = this.messagebox.current;
let obj;
if (this.tmid) {
@ -499,7 +501,7 @@ export default class RoomView extends React.Component {
renderItem = (item, previousItem) => {
const { room, lastOpen } = this.state;
const {
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, useMarkdown, Message_Read_Receipt_Enabled
} = this.props;
let dateSeparator = null;
let showUnreadSeparator = false;
@ -541,6 +543,7 @@ export default class RoomView extends React.Component {
timeFormat={Message_TimeFormat}
useRealName={useRealName}
useMarkdown={useMarkdown}
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
/>
);

View File

@ -1,8 +0,0 @@
import React from 'react';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
const Check = React.memo(() => <CustomIcon style={styles.sortIcon} size={22} name='check' />);
export default Check;

View File

@ -60,7 +60,11 @@ const Header = React.memo(({
}
return (
<View style={styles.container}>
<TouchableOpacity onPress={onPress} testID='rooms-list-header-server-dropdown-button'>
<TouchableOpacity
onPress={onPress}
testID='rooms-list-header-server-dropdown-button'
disabled={connecting || isFetching}
>
{connecting ? <Text style={styles.updating}>{I18n.t('Connecting')}</Text> : null}
{isFetching ? <Text style={styles.updating}>{I18n.t('Updating')}</Text> : null}
<View style={styles.button}>

View File

@ -40,13 +40,14 @@ const styles = StyleSheet.create({
});
const HeaderTitle = React.memo(({ connecting, isFetching }) => {
let title = I18n.t('Messages');
if (connecting) {
return <Text style={styles.title}>{I18n.t('Connecting')}</Text>;
title = I18n.t('Connecting');
}
if (isFetching) {
return <Text style={styles.title}>{I18n.t('Updating')}</Text>;
title = I18n.t('Updating');
}
return <Text style={styles.title}>{I18n.t('Messages')}</Text>;
return <Text style={styles.title}>{title}</Text>;
});
const Header = React.memo(({
@ -57,6 +58,7 @@ const Header = React.memo(({
onPress={onPress}
testID='rooms-list-header-server-dropdown-button'
style={styles.container}
disabled={connecting || isFetching}
>
<HeaderTitle connecting={connecting} isFetching={isFetching} />
<View style={styles.button}>

View File

@ -11,7 +11,7 @@ import Header from './Header';
showServerDropdown: state.rooms.showServerDropdown,
showSortDropdown: state.rooms.showSortDropdown,
showSearchHeader: state.rooms.showSearchHeader,
connecting: state.meteor.connecting,
connecting: state.meteor.connecting || state.server.loading,
isFetching: state.rooms.isFetching,
serverName: state.settings.Site_Name
}), dispatch => ({

View File

@ -0,0 +1,30 @@
import React from 'react';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../../../lib/Icons';
import I18n from '../../../i18n';
import Touch from '../../../utils/touch';
import styles from '../styles';
import DisclosureIndicator from '../../../containers/DisclosureIndicator';
const Directory = React.memo(({ goDirectory }) => (
<Touch
key='rooms-list-view-sort'
onPress={goDirectory}
style={styles.dropdownContainerHeader}
>
<View style={styles.sortItemContainer}>
<CustomIcon style={styles.directoryIcon} size={22} name='discover' />
<Text style={styles.directoryText}>{I18n.t('Directory')}</Text>
<DisclosureIndicator />
</View>
</Touch>
));
Directory.propTypes = {
goDirectory: PropTypes.func
};
export default Directory;

View File

@ -2,13 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import SearchBar from './SearchBar';
import Directory from './Directory';
import Sort from './Sort';
const ListHeader = React.memo(({
searchLength, sortBy, onChangeSearchText, toggleSort
searchLength, sortBy, onChangeSearchText, toggleSort, goDirectory
}) => (
<React.Fragment>
<SearchBar onChangeSearchText={onChangeSearchText} />
<Directory goDirectory={goDirectory} />
<Sort searchLength={searchLength} sortBy={sortBy} toggleSort={toggleSort} />
</React.Fragment>
));
@ -17,7 +19,8 @@ ListHeader.propTypes = {
searchLength: PropTypes.number,
sortBy: PropTypes.string,
onChangeSearchText: PropTypes.func,
toggleSort: PropTypes.func
toggleSort: PropTypes.func,
goDirectory: PropTypes.func
};
export default ListHeader;

View File

@ -16,7 +16,7 @@ import Touch from '../../utils/touch';
import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n';
import EventEmitter from '../../utils/events';
import Check from './Check';
import Check from '../../containers/Check';
const ROW_HEIGHT = 68;
const ANIMATION_DURATION = 200;

View File

@ -12,7 +12,7 @@ import { setPreference } from '../../actions/sortPreferences';
import log from '../../utils/log';
import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons';
import Check from './Check';
import Check from '../../containers/Check';
const ANIMATION_DURATION = 200;
@ -106,7 +106,7 @@ export default class Sort extends PureComponent {
render() {
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-245, 41]
outputRange: [-326, 0]
});
const backdropOpacity = this.animatedValue.interpolate({
inputRange: [0, 1],
@ -117,14 +117,24 @@ export default class Sort extends PureComponent {
} = this.props;
return (
[
<React.Fragment>
<TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}>
<Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} />
</TouchableWithoutFeedback>,
</TouchableWithoutFeedback>
<Animated.View
key='sort-container'
style={[styles.dropdownContainer, { transform: [{ translateY }] }]}
>
<Touch
key='sort-toggle'
onPress={this.close}
style={styles.dropdownContainerHeader}
>
<View style={styles.sortItemContainer}>
<Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text>
<CustomIcon style={styles.sortIcon} size={22} name='sort1' />
</View>
</Touch>
<Touch key='sort-alphabetical' style={styles.sortItemButton} onPress={this.sortByName}>
<View style={styles.sortItemContainer}>
<CustomIcon style={styles.sortIcon} size={22} name='sort' />
@ -161,18 +171,8 @@ export default class Sort extends PureComponent {
{showUnread ? <Check /> : null}
</View>
</Touch>
</Animated.View>,
<Touch
key='sort-toggle'
onPress={this.close}
style={[styles.dropdownContainerHeader, styles.sortToggleContainerClose]}
>
<View style={styles.sortItemContainer}>
<Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text>
<CustomIcon style={styles.sortIcon} size={22} name='sort1' />
</View>
</Touch>
]
</Animated.View>
</React.Fragment>
);
}
}

View File

@ -19,8 +19,8 @@ import ServerDropdown from './ServerDropdown';
import {
toggleSortDropdown as toggleSortDropdownAction,
openSearchHeader as openSearchHeaderAction,
closeSearchHeader as closeSearchHeaderAction
// roomsRequest as roomsRequestAction
closeSearchHeader as closeSearchHeaderAction,
roomsRequest as roomsRequestAction
} from '../../actions/rooms';
import { appStart as appStartAction } from '../../actions';
import debounce from '../../utils/debounce';
@ -55,8 +55,8 @@ const keyExtractor = item => item.rid;
toggleSortDropdown: () => dispatch(toggleSortDropdownAction()),
openSearchHeader: () => dispatch(openSearchHeaderAction()),
closeSearchHeader: () => dispatch(closeSearchHeaderAction()),
appStart: () => dispatch(appStartAction())
// roomsRequest: () => dispatch(roomsRequestAction())
appStart: () => dispatch(appStartAction()),
roomsRequest: () => dispatch(roomsRequestAction())
}))
export default class RoomsListView extends React.Component {
static navigationOptions = ({ navigation }) => {
@ -104,12 +104,12 @@ export default class RoomsListView extends React.Component {
showUnread: PropTypes.bool,
useRealName: PropTypes.bool,
StoreLastMessage: PropTypes.bool,
// appState: PropTypes.string,
appState: PropTypes.string,
toggleSortDropdown: PropTypes.func,
openSearchHeader: PropTypes.func,
closeSearchHeader: PropTypes.func,
appStart: PropTypes.func
// roomsRequest: PropTypes.func
appStart: PropTypes.func,
roomsRequest: PropTypes.func
}
constructor(props) {
@ -185,7 +185,7 @@ export default class RoomsListView extends React.Component {
componentDidUpdate(prevProps) {
const {
sortBy, groupByType, showFavorites, showUnread
sortBy, groupByType, showFavorites, showUnread, appState, roomsRequest
} = this.props;
if (!(
@ -195,11 +195,9 @@ export default class RoomsListView extends React.Component {
&& (prevProps.showUnread === showUnread)
)) {
this.getSubscriptions();
} else if (appState === 'foreground' && appState !== prevProps.appState) {
roomsRequest();
}
// removed for now... we may not need it anymore
// else if (appState === 'foreground' && appState !== prevProps.appState) {
// // roomsRequest();
// }
}
componentWillUnmount() {
@ -381,6 +379,11 @@ export default class RoomsListView extends React.Component {
}, 100);
}
goDirectory = () => {
const { navigation } = this.props;
navigation.navigate('DirectoryView');
}
getScrollRef = ref => this.scroll = ref
renderListHeader = () => {
@ -392,6 +395,7 @@ export default class RoomsListView extends React.Component {
sortBy={sortBy}
onChangeSearchText={this.search}
toggleSort={this.toggleSort}
goDirectory={this.goDirectory}
/>
);
}

View File

@ -1,7 +1,7 @@
import { StyleSheet } from 'react-native';
import { isIOS } from '../../utils/deviceInfo';
import {
COLOR_SEPARATOR, COLOR_TEXT, COLOR_PRIMARY, COLOR_WHITE
COLOR_SEPARATOR, COLOR_TEXT, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION
} from '../../constants/colors';
import sharedStyles from '../Styles';
@ -147,5 +147,17 @@ export default StyleSheet.create({
height: StyleSheet.hairlineWidth,
backgroundColor: COLOR_SEPARATOR,
marginLeft: 72
},
directoryIcon: {
width: 22,
height: 22,
marginHorizontal: 15,
color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION
},
directoryText: {
fontSize: 15,
flex: 1,
color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION,
...sharedStyles.textRegular
}
});

View File

@ -1,236 +1,160 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
View, ScrollView, Switch, Text, StyleSheet, AsyncStorage
View, Linking, ScrollView, AsyncStorage, SafeAreaView, Switch
} from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import firebase from 'react-native-firebase';
import RocketChat, { MARKDOWN_KEY } 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, Toast } from '../../utils/info';
import log from '../../utils/log';
import { setUser as setUserAction } from '../../actions/login';
import { toggleMarkdown as toggleMarkdownAction } from '../../actions/markdown';
import { COLOR_DANGER, COLOR_SUCCESS } from '../../constants/colors';
import { DrawerButton } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
import { isAndroid } from '../../utils/deviceInfo';
import {
COLOR_WHITE, COLOR_SEPARATOR, COLOR_DANGER, COLOR_SUCCESS
} from '../../constants/colors';
import ListItem from '../../containers/ListItem';
import { DisclosureImage } from '../../containers/DisclosureIndicator';
import Separator from '../../containers/Separator';
import I18n from '../../i18n';
import { MARKDOWN_KEY } from '../../lib/rocketchat';
import { getReadableVersion, getDeviceModel, isAndroid } from '../../utils/deviceInfo';
import openLink from '../../utils/openLink';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert } from '../../utils/info';
import styles from './styles';
import sharedStyles from '../Styles';
const styles = StyleSheet.create({
swithContainer: {
backgroundColor: COLOR_WHITE,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row'
},
label: {
fontSize: 17,
flex: 1,
...sharedStyles.textMedium,
...sharedStyles.textColorNormal
},
separator: {
flex: 1,
height: 1,
backgroundColor: COLOR_SEPARATOR,
marginVertical: 10
}
});
const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
const SectionSeparator = React.memo(() => <View style={styles.sectionSeparatorBorder} />);
const SWITCH_TRACK_COLOR = {
false: isAndroid ? COLOR_DANGER : null,
true: COLOR_SUCCESS
};
@connect(state => ({
userLanguage: state.login.user && state.login.user.language,
server: state.server,
useMarkdown: state.markdown.useMarkdown
}), dispatch => ({
setUser: params => dispatch(setUserAction(params)),
toggleMarkdown: params => dispatch(toggleMarkdownAction(params))
}))
export default class SettingsView extends React.Component {
static navigationOptions = ({ navigation }) => ({
headerLeft: <DrawerButton navigation={navigation} />,
title: I18n.t('Settings')
})
});
static propTypes = {
componentId: PropTypes.string,
userLanguage: PropTypes.string,
navigation: PropTypes.object,
server: PropTypes.object,
useMarkdown: PropTypes.bool,
setUser: PropTypes.func,
toggleMarkdown: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
placeholder: {},
language: props.userLanguage ? props.userLanguage : 'en',
languages: [{
label: 'English',
value: 'en'
}, {
label: 'Português (BR)',
value: 'pt-BR'
}, {
label: 'Russian',
value: 'ru'
}, {
label: '简体中文',
value: 'zh-CN'
}, {
label: 'Français',
value: 'fr'
}, {
label: 'Deutsch',
value: 'de'
}, {
label: 'Português (PT)',
value: 'pt-PT'
}],
saving: false
};
}
shouldComponentUpdate(nextProps, nextState) {
const { language, saving } = this.state;
const { userLanguage, useMarkdown } = this.props;
if (nextState.language !== language) {
return true;
}
if (nextState.saving !== saving) {
return true;
}
if (nextProps.useMarkdown !== useMarkdown) {
return true;
}
if (nextProps.userLanguage !== userLanguage) {
return true;
}
return false;
}
getLabel = (language) => {
const { languages } = this.state;
const l = languages.find(i => i.value === language);
if (l && l.label) {
return l.label;
}
return null;
}
formIsChanged = () => {
const { userLanguage } = this.props;
const { language } = this.state;
return !(userLanguage === language);
}
submit = async() => {
this.setState({ saving: true });
const { language } = this.state;
const { userLanguage, setUser } = this.props;
if (!this.formIsChanged()) {
return;
}
const params = {};
// language
if (userLanguage !== language) {
params.language = language;
}
try {
await RocketChat.saveUserPreferences(params);
setUser({ language: params.language });
this.setState({ saving: false });
setTimeout(() => {
this.toast.show(I18n.t('Preferences_saved'));
}, 300);
} catch (e) {
this.setState({ saving: false });
setTimeout(() => {
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
log('err_save_user_preferences', e);
}, 300);
}
}
toggleMarkdown = (value) => {
AsyncStorage.setItem(MARKDOWN_KEY, JSON.stringify(value));
const { toggleMarkdown } = this.props;
toggleMarkdown(value);
firebase.analytics().logEvent('toggle_markdown', { value });
}
navigateToRoom = (room) => {
const { navigation } = this.props;
navigation.navigate(room);
}
sendEmail = async() => {
const subject = encodeURI('React Native App Support');
const email = encodeURI('support@rocket.chat');
const description = encodeURI(`
version: ${ getReadableVersion }
device: ${ getDeviceModel }
`);
try {
await Linking.openURL(`mailto:${ email }?subject=${ subject }&body=${ description }`);
} catch (e) {
showErrorAlert(I18n.t('error-email-send-failed', { message: 'support@rocket.chat' }));
}
}
onPressLicense = () => openLink(LICENSE_LINK)
renderDisclosure = () => <DisclosureImage />
renderMarkdownSwitch = () => {
const { useMarkdown } = this.props;
return (
<Switch
value={useMarkdown}
trackColor={SWITCH_TRACK_COLOR}
onValueChange={this.toggleMarkdown}
/>
);
}
render() {
const {
language, languages, placeholder, saving
} = this.state;
const { useMarkdown } = this.props;
const { server } = this.props;
return (
<KeyboardView
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}
>
<SafeAreaView style={sharedStyles.listSafeArea} testID='settings-view'>
<StatusBar />
<ScrollView
contentContainerStyle={sharedStyles.containerScrollView}
testID='settings-view-list'
{...scrollPersistTaps}
contentContainerStyle={sharedStyles.listContentContainer}
showsVerticalScrollIndicator={false}
testID='settings-view-list'
>
<SafeAreaView style={sharedStyles.container} testID='settings-view' forceInset={{ bottom: 'never' }}>
<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={this.getLabel(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 style={styles.separator} />
<View style={styles.swithContainer}>
<Text style={styles.label}>{I18n.t('Enable_markdown')}</Text>
<Switch
value={useMarkdown}
onValueChange={this.toggleMarkdown}
onTintColor={COLOR_SUCCESS}
tintColor={isAndroid ? COLOR_DANGER : null}
/>
</View>
<Loading visible={saving} />
<Toast ref={toast => this.toast = toast} />
</SafeAreaView>
<ListItem
title={I18n.t('Contact_us')}
onPress={this.sendEmail}
showActionIndicator
testID='settings-view-contact'
right={this.renderDisclosure}
/>
<Separator />
<ListItem
title={I18n.t('Language')}
onPress={() => this.navigateToRoom('LanguageView')}
showActionIndicator
testID='settings-view-language'
right={this.renderDisclosure}
/>
<Separator />
<ListItem
title={I18n.t('Theme')}
showActionIndicator
disabled
testID='settings-view-theme'
/>
<Separator />
<ListItem
title={I18n.t('Share_this_app')}
showActionIndicator
disabled
testID='settings-view-share-app'
/>
<SectionSeparator />
<ListItem
title={I18n.t('License')}
onPress={this.onPressLicense}
showActionIndicator
testID='settings-view-license'
right={this.renderDisclosure}
/>
<Separator />
<ListItem title={I18n.t('Version_no', { version: getReadableVersion })} testID='settings-view-version' />
<Separator />
<ListItem
title={I18n.t('Server_version', { version: server.version })}
subtitle={`${ server.server.split('//')[1] }`}
testID='settings-view-server-version'
/>
<SectionSeparator />
<ListItem
title={I18n.t('Enable_markdown')}
testID='settings-view-markdown'
right={() => this.renderMarkdownSwitch()}
/>
</ScrollView>
</KeyboardView>
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,12 @@
import { StyleSheet } from 'react-native';
import { COLOR_BACKGROUND_CONTAINER } from '../../constants/colors';
import sharedStyles from '../Styles';
export default StyleSheet.create({
sectionSeparatorBorder: {
...sharedStyles.separatorVertical,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
height: 10
}
});

View File

@ -15,7 +15,6 @@ import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { getReadableVersion } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import SidebarItem from './SidebarItem';
@ -279,9 +278,6 @@ export default class Sidebar extends Component {
{!showStatus ? this.renderNavigation() : null}
{showStatus ? this.renderStatus() : null}
</ScrollView>
<Text style={styles.version}>
{getReadableVersion}
</Text>
</SafeAreaView>
);
}

View File

@ -1,7 +1,7 @@
import { StyleSheet, Platform } from 'react-native';
import {
COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_SEPARATOR, COLOR_TEXT, COLOR_TEXT_DESCRIPTION, COLOR_TITLE
COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_SEPARATOR, COLOR_TEXT, COLOR_TEXT_DESCRIPTION, COLOR_TITLE, COLOR_BACKGROUND_CONTAINER, COLOR_WHITE, COLOR_PRIMARY
} from '../constants/colors';
export default StyleSheet.create({
@ -176,7 +176,21 @@ export default StyleSheet.create({
textColorDescription: {
color: COLOR_TEXT_DESCRIPTION
},
colorPrimary: {
color: COLOR_PRIMARY
},
inputLastChild: {
marginBottom: 15
},
listSafeArea: {
flex: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER
},
listContentContainer: {
borderColor: COLOR_SEPARATOR,
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
backgroundColor: COLOR_WHITE,
marginVertical: 10
}
});

View File

@ -32,7 +32,7 @@ describe('Forgot password screen', () => {
describe('Usage', async() => {
it('should reset password and navigate to login', async() => {
await element(by.id('forgot-password-view-email')).replaceText('diego.mello@rocket.chat');
await element(by.id('forgot-password-view-email')).replaceText(data.existingEmail);
await element(by.id('forgot-password-view-submit')).tap();
await element(by.text('OK')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000);

View File

@ -72,7 +72,7 @@ describe('Create user screen', () => {
const invalidEmail = 'invalidemail';
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-username')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText('diego.mello@rocket.chat');
await element(by.id('register-view-email')).replaceText(data.existingEmail);
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Email already exists. [403]')).atIndex(0)).toExist().withTimeout(10000);
@ -83,7 +83,7 @@ describe('Create user screen', () => {
it('should submit email already taken and raise error', async() => {
const invalidEmail = 'invalidemail';
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-username')).replaceText('diego.mello');
await element(by.id('register-view-username')).replaceText(data.existingName);
await element(by.id('register-view-email')).replaceText(data.email);
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();

View File

@ -158,6 +158,31 @@ describe('Room screen', () => {
await expect(element(by.id('messagebox-input'))).toHaveText('#general ');
await element(by.id('messagebox-input')).clearText();
});
// it('should show and tap on slash command autocomplete and send slash command', async() => {
// await element(by.id('messagebox-input')).tap();
// await element(by.id('messagebox-input')).typeText('/');
// await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(10000);
// await expect(element(by.id('messagebox-container'))).toBeVisible();
// await element(by.id('mention-item-shrug')).tap();
// await expect(element(by.id('messagebox-input'))).toHaveText('/shrug ');
// await element(by.id('messagebox-input')).typeText('joy'); // workaround for number keyboard
// await element(by.id('messagebox-send-message')).tap();
// await waitFor(element(by.text(`joy ¯\_(ツ)_/¯`))).toBeVisible().withTimeout(60000);
// });
// it('should show command Preview', async() => {
// await element(by.id('messagebox-input')).tap();
// await element(by.id('messagebox-input')).replaceText('/giphy');
// await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(10000);
// await expect(element(by.id('messagebox-container'))).toBeVisible();
// await element(by.id('mention-item-giphy')).tap();
// await expect(element(by.id('messagebox-input'))).toHaveText('/giphy ');
// await element(by.id('messagebox-input')).typeText('no'); // workaround for number keyboard
// await waitFor(element(by.id('commandbox-container'))).toBeVisible().withTimeout(10000);
// await expect(element(by.id('commandbox-container'))).toBeVisible();
// await element(by.id('messagebox-input')).clearText();
// });
});
describe('Message', async() => {
@ -360,4 +385,4 @@ describe('Room screen', () => {
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
});
});
});

94
e2e/14-setting.spec.js Normal file
View File

@ -0,0 +1,94 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { logout, navigateToLogin, login } = require('./helpers/app');
describe('Settings screen', () => {
before(async() => {
await device.reloadReactNative();
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000);
await element(by.id('sidebar-settings')).tap();
await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000);
});
describe('Render', async() => {
it('should have settings view', async() => {
await expect(element(by.id('settings-view'))).toBeVisible();
});
it('should have language', async() => {
await expect(element(by.id('settings-view-language'))).toExist();
});
it('should have theme', async() => {
await expect(element(by.id('settings-view-theme'))).toExist();
});
it('should have share app', async() => {
await expect(element(by.id('settings-view-share-app'))).toExist();
});
it('should have licence', async() => {
await expect(element(by.id('settings-view-license'))).toExist();
});
it('should have version no', async() => {
await expect(element(by.id('settings-view-version'))).toExist();
});
it('should have server version', async() => {
await expect(element(by.id('settings-view-server-version'))).toExist();
});
it('should have enable markdown', async() => {
await expect(element(by.id('settings-view-markdown'))).toExist();
});
after(async() => {
takeScreenshot();
});
});
describe('Language', async() => {
it('should navigate to language view', async() => {
await element(by.id('settings-view-language')).tap();
await waitFor(element(by.id('language-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('language-view-zh-CN'))).toExist();
await expect(element(by.id('language-view-de'))).toExist();
await expect(element(by.id('language-view-en'))).toExist();
await expect(element(by.id('language-view-fr'))).toExist();
await expect(element(by.id('language-view-pt-BR'))).toExist();
await expect(element(by.id('language-view-pt-PT'))).toExist();
await expect(element(by.id('language-view-ru'))).toExist();
});
it('should navigate to change language', async() => {
await expect(element(by.id('language-view-zh-CN'))).toExist();
await element(by.id('language-view-zh-CN')).tap()
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.text('设置'))).toBeVisible().withTimeout(2000);
await element(by.text('设置')).tap();
await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000);
await element(by.id('settings-view-language')).tap();
await element(by.id('language-view-en')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Settings'))).toBeVisible();
await element(by.text('Settings')).tap();
await expect(element(by.id('settings-view'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
});

View File

@ -171,22 +171,23 @@ describe('Join public room', () => {
await expect(element(by.id('room-actions-leave-channel'))).toBeVisible();
});
it('should leave room', async() => {
await element(by.id('room-actions-leave-channel')).tap();
await waitFor(element(by.text('Yes, leave it!'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Yes, leave it!'))).toBeVisible();
await element(by.text('Yes, leave it!')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
await element(by.id('rooms-list-view-search')).replaceText('');
await sleep(2000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
});
it('should navigate to room and user should be joined', async() => {
await navigateToRoom();
await expect(element(by.id('room-view-join'))).toBeVisible();
})
// TODO: fix CI to pass with this test
// it('should leave room', async() => {
// await element(by.id('room-actions-leave-channel')).tap();
// await waitFor(element(by.text('Yes, leave it!'))).toBeVisible().withTimeout(5000);
// await expect(element(by.text('Yes, leave it!'))).toBeVisible();
// await element(by.text('Yes, leave it!')).tap();
// await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
// await element(by.id('rooms-list-view-search')).replaceText('');
// await sleep(2000);
// await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
// await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
// });
//
// it('should navigate to room and user should be joined', async() => {
// await navigateToRoom();
// await expect(element(by.id('room-view-join'))).toBeVisible();
// })
after(async() => {
takeScreenshot();

52
e2e/README.md Normal file
View File

@ -0,0 +1,52 @@
### Contents:
1. [Prepare test environment](##-1.-Prepare-test-environment)
2. [Prepare test data](##-2.-Prepare-test-data)
3. [Running tests](##-3.-Running-tests)
4. [FAQ](##-FAQ)
### 1. Prepare test environment
##### 1.1. Set up local Rocket Chat server
* Install Rocket Chat meteor app by following this [guide](https://rocket.chat/docs/developer-guides/quick-start).
##### 1.2. Set up detox
* Install dependencies by following this [guide](https://github.com/wix/Detox/blob/master/docs/Introduction.GettingStarted.md#step-1-install-dependencies) (only Step 1).
### 2. Prepare test data
* Run Rocket Chat meteor app: `meteor npm start` (make sure you to run this command from project that you created on Step 1.1.).
* Open `localhost:3000` in browser.
* Sign up as admin.
* Create public room `detox-public`.
* Create user with role: `user`, username: `detoxrn`, email: `YOUR@EMAIL.COM`, password: `123`.
* Create user with role: `user`, username: `YOUR.NAME`, email: `YOUR.SECOND@EMAIL.COM`, password: `123`.
* In file `e2e/data.js` change values `existingEmail` with `YOUR.SECOND@EMAIL.COM`, `existingName` with `YOUR.NAME`.
* Login as user `detoxrn` -> open My Account -> Settings tab -> click Enable 2FA -> copy TTOLP code -> paste TTOLP code into `./e2e/data.js` file into field: `alternateUserTOTPSecret`.
### 3. Running tests
#### 3.1. iOS
* Build app with detox: `detox build -c ios.sim.release`
* Open Simulator which is used in tests (check in package.json under detox section) from Xcode and make sure that software keyboard is being displayed. To toggle keyboard press `cmd+K`.
* Run tests: `detox test -c ios.sim.release`
#### 3.1. Android
* Build app with detox: `detox build -c android.emu.debug`
* Run: `react-native start`
* Run Android emulator with name `ANDROID_API_28` via Android studio or `cd /Users/USERNAME/Library/Android/sdk/emulator/ && ./emulator -avd ANDROID_API_28`
Note: if you need to run tests on different Android emulator then simply change emulator name in ./package.json detox configurations
* Run tests: `detox test -c android.emu.debug`
### 4. FAQ
#### 4.1. Detox build fails
* Delete `node_modules`, `ios/build`, `android/build`:
`rm -rf node_modules && rm -rf ios/build && rm -rf android/build`
* Install packages: `yarn install`
* Kill metro bundler server by closing terminal or with following command: `lsof -ti:8081 | xargs kill`
* Clear metro bundler cache: `watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*`
* Make sure you have all required [environment](##-1.-Prepare-test-environment).
* Now try building again with `detox build` (with specific configuration).
#### 4.2. Detox iOS test run fails
* Check if your meteor app is running by opening `localhost:3000` in browser.
* Make sure software keyboard is displayed in simulator when focusing some input. To enable keyboard press `cmd+K`.
* Make sure you have prepared all [test data](##-2.-Prepare-test-data).
* Sometimes detox e2e tests fail for no reason so all you can do is simply re-run again.

View File

@ -1,13 +1,15 @@
const random = require('./helpers/random');
const value = random(20);
const data = {
server: 'http://localhost:3000',
server: 'https://ilarion.rocket.chat',
alternateServer: 'https://stable.rocket.chat',
user: `user${ value }`,
password: `password${ value }`,
alternateUser: 'detoxrn',
alternateUserPassword: '123',
alternateUserTOTPSecret: 'I5SGETK3GBXXA7LNLMZTEJJRIN3G6LTEEE4G4PS3EQRXU4LNPU7A',
alternateUserTOTPSecret: 'NFXHKKC6FJXESL25HBYTYNSFKR4WCTSXFRKUUVKEOBBC6I3JKI7A',
existingEmail: 'diego.mello@rocket.chat',
existingName: 'diego.mello',
email: `diego.mello+e2e${ value }@rocket.chat`,
random: value
}

View File

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

View File

@ -25,7 +25,7 @@
"ejson": "^2.1.2",
"js-base64": "^2.5.1",
"js-sha256": "^0.9.0",
"jsc-android": "241213.1.0",
"jsc-android": "^241213.2.0",
"lodash": "^4.17.11",
"markdown-it-flowdock": "^0.3.7",
"moment": "^2.24.0",

View File

@ -311,6 +311,30 @@ export default (
}]}
/>
<Separator title='Message with read receipt' />
<Message
msg="I'm fine!"
isReadReceiptEnabled
unread
/>
<Message
msg="I'm fine!"
isReadReceiptEnabled
unread
isHeader={false}
/>
<Message
msg="I'm fine!"
isReadReceiptEnabled
read
/>
<Message
msg="I'm fine!"
isReadReceiptEnabled
read
isHeader={false}
/>
<Separator title='Message with thread' />
<Message
msg='How are you?'

View File

@ -8524,10 +8524,10 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
jsc-android@241213.1.0:
version "241213.1.0"
resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-241213.1.0.tgz#8f940d7c7f6bebf14eda32bef42a76182e336452"
integrity sha512-AH8NYyMNLNhcUEF97QbMxPNLNW+oiSBlvm1rsMNzgJ1d5TQzdh/AOJGsxeeESp3m9YIWGLCgUvGTVoVLs0p68A==
jsc-android@^241213.2.0:
version "241213.2.0"
resolved "https://registry.yarnpkg.com/jsc-android/-/jsc-android-241213.2.0.tgz#a43b78e4dace997be533e7cb812d9714878b069f"
integrity sha512-nfddejB9jxFSG+Uewf+zwATFi8F2CZEEgoHLoOj13egiBDoC7zMoxK1c5/Ycf3AGmGuwCgjpn3LWe0f4tKYbjw==
jsdom@^11.5.1:
version "11.12.0"