Merge branch 'develop' into beta
This commit is contained in:
commit
89ad22cd1d
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
createSagaMonitor: () => {}
|
||||
};
|
|
@ -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>
|
||||
I’m 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>
|
||||
I’m 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>
|
||||
I’m 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>
|
||||
I’m 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 [
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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()}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
|
@ -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}
|
||||
|
|
|
@ -234,5 +234,8 @@ export default StyleSheet.create({
|
|||
flex: 1,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
readReceipt: {
|
||||
lineHeight: 20
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 '
|
||||
};
|
||||
|
|
|
@ -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}}',
|
||||
|
|
34
app/index.js
34
app/index.js
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 })) }));
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -40,7 +40,8 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
createChannelIcon: {
|
||||
color: COLOR_PRIMARY,
|
||||
marginHorizontal: 18
|
||||
marginLeft: 18,
|
||||
marginRight: 15
|
||||
},
|
||||
createChannelText: {
|
||||
color: COLOR_PRIMARY,
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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?'
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue