Update dependencies (#431)

* Update dependencies

* Lint and test

* Added react-native fork

* rn 57

* Lint and tests updated

* Update xcode on circleci

* Use legacy build system

* Update tests
This commit is contained in:
Diego Mello 2018-09-25 16:28:42 -03:00 committed by GitHub
parent e06ba0139d
commit 81c53acd60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 7613 additions and 6092 deletions

View File

@ -1,5 +1,5 @@
{ {
"presets": ["react-native"], "presets": ["module:metro-react-native-babel-preset"],
"plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]], "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]],
"env": { "env": {
"production": { "production": {

View File

@ -37,7 +37,7 @@ jobs:
e2e-test: e2e-test:
macos: macos:
xcode: "9.0" xcode: "10.0.0"
environment: environment:
BASH_ENV: "~/.nvm/nvm.sh" BASH_ENV: "~/.nvm/nvm.sh"
@ -87,7 +87,8 @@ jobs:
- image: circleci/android:api-27-node8-alpha - image: circleci/android:api-27-node8-alpha
environment: environment:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError" # GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
GRADLE_OPTS: -Xmx2048m -Dorg.gradle.daemon=false
JVM_OPTS: -Xmx4096m JVM_OPTS: -Xmx4096m
TERM: dumb TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh" BASH_ENV: "~/.nvm/nvm.sh"
@ -170,7 +171,7 @@ jobs:
ios-build: ios-build:
macos: macos:
xcode: "9.0" xcode: "10.0.0"
environment: environment:
BASH_ENV: "~/.nvm/nvm.sh" BASH_ENV: "~/.nvm/nvm.sh"
@ -236,7 +237,7 @@ jobs:
ios-testflight: ios-testflight:
macos: macos:
xcode: "9.0" xcode: "10.0.0"
steps: steps:
- checkout - checkout

View File

@ -13,7 +13,8 @@ module.exports = {
"ecmaVersion": 2017, "ecmaVersion": 2017,
"ecmaFeatures": { "ecmaFeatures": {
"experimentalObjectRestSpread" : true, "experimentalObjectRestSpread" : true,
"jsx": true "jsx": true,
"legacyDecorators": true
} }
}, },
"plugins": [ "plugins": [
@ -65,6 +66,7 @@ module.exports = {
"no-dupe-args": 2, "no-dupe-args": 2,
"no-dupe-class-members": 2, "no-dupe-class-members": 2,
"no-duplicate-case": 2, "no-duplicate-case": 2,
"no-else-return": [0, {allowElseIf: true}],
"no-empty": 2, "no-empty": 2,
"no-empty-character-class": 2, "no-empty-character-class": 2,
"no-ex-assign": 2, "no-ex-assign": 2,
@ -125,7 +127,8 @@ module.exports = {
"object-shorthand": 2, "object-shorthand": 2,
"consistent-return": 0, "consistent-return": 0,
"global-require": "off", "global-require": "off",
"react-native/no-unused-styles": 2 "react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0
}, },
"globals": { "globals": {
"__DEV__": true "__DEV__": true

View File

@ -4,7 +4,6 @@ exports[`render channel 1`] = `
<View> <View>
<View <View
accessibilityLabel="general, last message Nov 10" accessibilityLabel="general, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -200,7 +199,6 @@ exports[`render no icon 1`] = `
<View> <View>
<View <View
accessibilityLabel="name, last message Nov 10" accessibilityLabel="name, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -396,7 +394,6 @@ exports[`render private group 1`] = `
<View> <View>
<View <View
accessibilityLabel="private-group, last message Nov 10" accessibilityLabel="private-group, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -593,7 +590,6 @@ exports[`render unread +999 1`] = `
<View> <View>
<View <View
accessibilityLabel="name, 1000 alerts, last message Nov 10" accessibilityLabel="name, 1000 alerts, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -856,7 +852,6 @@ exports[`render unread 1`] = `
<View> <View>
<View <View
accessibilityLabel="name, 1 alert, last message Nov 10" accessibilityLabel="name, 1 alert, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -1119,7 +1114,6 @@ exports[`renders correctly 1`] = `
<View> <View>
<View <View
accessibilityLabel="name, last message Nov 10" accessibilityLabel="name, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}

View File

@ -300,7 +300,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
<View> <View>
<View <View
accessibilityLabel="rocket.cat, last message Nov 10" accessibilityLabel="rocket.cat, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -532,7 +531,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="rocket.cat, last message Nov 10" accessibilityLabel="rocket.cat, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -768,7 +766,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="rocket.cat, 1 alert, last message Nov 10" accessibilityLabel="rocket.cat, 1 alert, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -1026,7 +1023,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 9 alerts, last message Nov 10" accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 9 alerts, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -1288,7 +1284,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 99 alerts, last message Nov 10" accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 99 alerts, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -1546,7 +1541,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100 alerts, last message Nov 10" accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100 alerts, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -1804,7 +1798,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100000 alerts, last message Nov 10" accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100000 alerts, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -2062,7 +2055,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100000 alerts, you were mentioned, last message Nov 10" accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, 100000 alerts, you were mentioned, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -2320,7 +2312,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="W, last message Nov 10" accessibilityLabel="W, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -2552,7 +2543,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel="WW, last message Nov 10" accessibilityLabel="WW, last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}
@ -2784,7 +2774,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View> </View>
<View <View
accessibilityLabel=", last message Nov 10" accessibilityLabel=", last message Nov 10"
accessibilityTraits="selected"
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
onResponderGrant={[Function]} onResponderGrant={[Function]}

View File

@ -198,24 +198,23 @@ dependencies {
implementation project(':react-native-audio') implementation project(':react-native-audio')
implementation project(":reactnativekeyboardinput") implementation project(":reactnativekeyboardinput")
implementation project(':react-native-video') implementation project(':react-native-video')
implementation project(':react-native-svg')
implementation project(':react-native-vector-icons') implementation project(':react-native-vector-icons')
implementation project(':rn-fetch-blob') implementation project(':rn-fetch-blob')
implementation project(':react-native-zeroconf')
implementation project(':@remobile/react-native-toast') implementation project(':@remobile/react-native-toast')
implementation project(':react-native-fast-image') implementation project(':react-native-fast-image')
implementation project(':realm') implementation project(':realm')
implementation project(':react-native-navigation') implementation project(':react-native-navigation')
implementation project(':reactnativenotifications') implementation project(':reactnativenotifications')
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:27.1.0" implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.android.support:support-v4:27.1.+" implementation "com.android.support:support-v4:27.1.1"
implementation 'com.android.support:customtabs:27.1.0' implementation 'com.android.support:customtabs:27.1.1'
implementation 'com.android.support:design:27.1.0' implementation 'com.android.support:design:27.1.1'
implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.facebook.fresco:animated-gif:1.9.0' implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-webp:1.9.0' implementation 'com.facebook.fresco:animated-gif:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.9.0' implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') { implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') {
transitive = true; transitive = true;
} }

View File

@ -6,13 +6,11 @@ import android.os.Bundle;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage; import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage; import com.reactnative.ivpusic.imagepicker.PickerPackage;
import com.RNFetchBlob.RNFetchBlobPackage; import com.RNFetchBlob.RNFetchBlobPackage;
import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage;
import com.brentvatne.react.ReactVideoPackage; import com.brentvatne.react.ReactVideoPackage;
import com.crashlytics.android.Crashlytics; import com.crashlytics.android.Crashlytics;
import com.dylanvann.fastimage.FastImageViewPackage; import com.dylanvann.fastimage.FastImageViewPackage;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
import com.horcrux.svg.SvgPackage;
import com.oblador.vectoricons.VectorIconsPackage; import com.oblador.vectoricons.VectorIconsPackage;
import com.reactnativenavigation.NavigationApplication; import com.reactnativenavigation.NavigationApplication;
import com.remobile.toast.RCTToastPackage; import com.remobile.toast.RCTToastPackage;
@ -62,10 +60,8 @@ public class MainApplication extends NavigationApplication implements INotificat
new RNDeviceInfo(), new RNDeviceInfo(),
new RNGestureHandlerPackage(), new RNGestureHandlerPackage(),
new PickerPackage(), new PickerPackage(),
new SvgPackage(),
new VectorIconsPackage(), new VectorIconsPackage(),
new RNFetchBlobPackage(), new RNFetchBlobPackage(),
new ZeroconfReactPackage(),
new RealmReactPackage(), new RealmReactPackage(),
new ReactVideoPackage(), new ReactVideoPackage(),
new RCTToastPackage(), new RCTToastPackage(),

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -21,12 +21,8 @@ include ':reactnativekeyboardinput'
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android') project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android')
include ':react-native-video' include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android') project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-vector-icons' include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-zeroconf'
project(':react-native-zeroconf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zeroconf/android')
include ':realm' include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-navigation' include ':react-native-navigation'

View File

@ -3,12 +3,17 @@ class NavigationActionsClass {
this.navigator = navigator; this.navigator = navigator;
} }
push = params => this.navigator && this.navigator.push(params); push = params => this.navigator && this.navigator.push(params)
pop = params => this.navigator && this.navigator.pop(params);
popToRoot = params => this.navigator && this.navigator.popToRoot(params); pop = params => this.navigator && this.navigator.pop(params)
resetTo = params => this.navigator && this.navigator.resetTo(params);
toggleDrawer = params => this.navigator && this.navigator.toggleDrawer(params); popToRoot = params => this.navigator && this.navigator.popToRoot(params)
dismissModal = params => this.navigator && this.navigator.dismissModal(params);
resetTo = params => this.navigator && this.navigator.resetTo(params)
toggleDrawer = params => this.navigator && this.navigator.toggleDrawer(params)
dismissModal = params => this.navigator && this.navigator.dismissModal(params)
} }
export const NavigationActions = new NavigationActionsClass(); export const NavigationActions = new NavigationActionsClass();

View File

@ -103,4 +103,3 @@ export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL
export const INCREMENT = 'INCREMENT'; export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT'; export const DECREMENT = 'DECREMENT';

View File

@ -26,4 +26,3 @@ export function setLoading(loading) {
loading loading
}; };
} }

View File

@ -9,6 +9,7 @@ export default class Panel extends React.Component {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
style: PropTypes.object style: PropTypes.object
} }
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -18,15 +19,22 @@ export default class Panel extends React.Component {
this.open = false; this.open = false;
this.opacity = 0; this.opacity = 0;
} }
componentDidMount() { componentDidMount() {
const initialValue = !this.props.open ? this.height : 0; const { animation } = this.state;
this.state.animation.setValue(initialValue); const { open } = this.props;
const initialValue = !open ? this.height : 0;
animation.setValue(initialValue);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { animation } = this.state;
const { open } = this.props;
if (this.first) { if (this.first) {
this.first = false; this.first = false;
if (!this.props.open) { if (!open) {
this.state.animation.setValue(0); animation.setValue(0);
return; return;
} }
} }
@ -37,9 +45,9 @@ export default class Panel extends React.Component {
const initialValue = !nextProps.open ? this.height : 0; const initialValue = !nextProps.open ? this.height : 0;
const finalValue = !nextProps.open ? 0 : this.height; const finalValue = !nextProps.open ? 0 : this.height;
this.state.animation.setValue(initialValue); animation.setValue(initialValue);
Animated.timing( Animated.timing(
this.state.animation, animation,
{ {
toValue: finalValue, toValue: finalValue,
duration: 150, duration: 150,
@ -47,16 +55,21 @@ export default class Panel extends React.Component {
} }
).start(); ).start();
} }
set _height(h) { set _height(h) {
this.height = h || this.height; this.height = h || this.height;
} }
render() { render() {
const { animation } = this.state;
const { style, children } = this.props;
return ( return (
<Animated.View <Animated.View
style={[{ height: this.state.animation }, this.props.style]} style={[{ height: animation }, style]}
> >
<View onLayout={({ nativeEvent }) => this._height = nativeEvent.layout.height} style={{ position: !this.first ? 'relative' : 'absolute' }}> <View onLayout={({ nativeEvent }) => this._height = nativeEvent.layout.height} style={{ position: !this.first ? 'relative' : 'absolute' }}>
{this.props.children} {children}
</View> </View>
</Animated.View> </Animated.View>
); );

View File

@ -14,10 +14,11 @@ export default class Fade extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { visible } = this.props;
this.state = { this.state = {
visible: props.visible visible
}; };
this._visibility = new Animated.Value(this.props.visible ? 1 : 0); this._visibility = new Animated.Value(visible ? 1 : 0);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -34,6 +35,7 @@ export default class Fade extends React.Component {
} }
render() { render() {
const { visible } = this.state;
const { style, children, ...rest } = this.props; const { style, children, ...rest } = this.props;
const containerStyle = { const containerStyle = {
@ -53,8 +55,8 @@ export default class Fade extends React.Component {
const combinedStyle = [containerStyle, style]; const combinedStyle = [containerStyle, style];
return ( return (
<Animated.View style={this.state.visible ? combinedStyle : containerStyle} {...rest}> <Animated.View style={visible ? combinedStyle : containerStyle} {...rest}>
<Text>{this.state.visible ? children : null}</Text> <Text>{visible ? children : null}</Text>
</Animated.View> </Animated.View>
); );
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; import {
StyleSheet, Text, View, ViewPropTypes
} from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
@ -29,14 +31,16 @@ export default class Avatar extends React.PureComponent {
type: PropTypes.string, type: PropTypes.string,
children: PropTypes.object, children: PropTypes.object,
forceInitials: PropTypes.bool forceInitials: PropTypes.bool
}; }
static defaultProps = { static defaultProps = {
text: '', text: '',
size: 25, size: 25,
type: 'd', type: 'd',
borderRadius: 4, borderRadius: 4,
forceInitials: false forceInitials: false
}; }
state = { showInitials: true }; state = { showInitials: true };
// componentDidMount() { // componentDidMount() {
@ -93,8 +97,9 @@ export default class Avatar extends React.PureComponent {
// } // }
render() { render() {
const { showInitials } = this.state;
const { const {
text, size, baseUrl, borderRadius, style, avatar, type, forceInitials text, size, baseUrl, borderRadius, style, avatar, type, forceInitials, children
} = this.props; } = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`); const { initials, color } = avatarInitialsAndColor(`${ text }`);
@ -133,17 +138,19 @@ export default class Avatar extends React.PureComponent {
return ( return (
<View style={[styles.iconContainer, iconContainerStyle, style]}> <View style={[styles.iconContainer, iconContainerStyle, style]}>
{this.state.showInitials ? {showInitials
<Text ? (
style={[styles.avatarInitials, avatarInitialsStyle]} <Text
allowFontScaling={false} style={[styles.avatarInitials, avatarInitialsStyle]}
> allowFontScaling={false}
{initials} >
</Text> {initials}
</Text>
)
: null : null
} }
{image} {image}
{this.props.children} {children}
</View> </View>
); );
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet, View, Text, Platform, ActivityIndicator } from 'react-native'; import {
StyleSheet, View, Text, Platform, ActivityIndicator
} from 'react-native';
import { COLOR_BUTTON_PRIMARY, COLOR_TEXT } from '../../constants/colors'; import { COLOR_BUTTON_PRIMARY, COLOR_TEXT } from '../../constants/colors';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
@ -92,9 +94,9 @@ export default class Button extends React.PureComponent {
]} ]}
> >
{ {
loading ? loading
<ActivityIndicator color={colors[`text_color_${ type }`]} /> : ? <ActivityIndicator color={colors[`text_color_${ type }`]} />
<Text style={[styles.text, styles[`text_color_${ type }`]]}>{title}</Text> : <Text style={[styles.text, styles[`text_color_${ type }`]]}>{title}</Text>
} }
</View> </View>
</Touch> </Touch>

View File

@ -8,9 +8,11 @@ export default class CustomEmoji extends React.Component {
emoji: PropTypes.object.isRequired, emoji: PropTypes.object.isRequired,
style: ViewPropTypes.style style: ViewPropTypes.style
} }
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
render() { render() {
const { baseUrl, emoji, style } = this.props; const { baseUrl, emoji, style } = this.props;
return ( return (

View File

@ -8,7 +8,7 @@ import styles from './styles';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9; const EMOJIS_PER_ROW = Platform.OS === 'ios' ? 8 : 9;
const renderEmoji = (emoji, size, baseUrl) => { const renderEmoji = (emoji, size, baseUrl) => {
if (emoji.isCustom) { if (emoji.isCustom) {
@ -31,12 +31,14 @@ export default class EmojiCategory extends React.Component {
onEmojiSelected: PropTypes.func, onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number, emojisPerRow: PropTypes.number,
width: PropTypes.number width: PropTypes.number
}; }
constructor(props) { constructor(props) {
super(props); super(props);
const { width, height } = this.props.window; const { window, width, emojisPerRow } = this.props;
const { width: widthWidth, height: windowHeight } = window;
this.size = Math.min(this.props.width || width, height) / (this.props.emojisPerRow || emojisPerRow); this.size = Math.min(width || widthWidth, windowHeight) / (emojisPerRow || EMOJIS_PER_ROW);
this.emojis = props.emojis; this.emojis = props.emojis;
} }
@ -45,12 +47,12 @@ export default class EmojiCategory extends React.Component {
} }
renderItem(emoji, size) { renderItem(emoji, size) {
const { baseUrl } = this.props; const { baseUrl, onEmojiSelected } = this.props;
return ( return (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
key={emoji.isCustom ? emoji.content : emoji} key={emoji.isCustom ? emoji.content : emoji}
onPress={() => this.props.onEmojiSelected(emoji)} onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`} testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
> >
{renderEmoji(emoji, size, baseUrl)} {renderEmoji(emoji, size, baseUrl)}
@ -58,12 +60,14 @@ export default class EmojiCategory extends React.Component {
} }
render() { render() {
const { emojis } = this.props;
return ( return (
<OptimizedFlatList <OptimizedFlatList
keyExtractor={item => (item.isCustom && item.content) || item} keyExtractor={item => (item.isCustom && item.content) || item}
data={this.props.emojis} data={emojis}
renderItem={({ item }) => this.renderItem(item, this.size)} renderItem={({ item }) => this.renderItem(item, this.size)}
numColumns={emojisPerRow} numColumns={EMOJIS_PER_ROW}
initialNumToRender={45} initialNumToRender={45}
getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })} getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })}
removeClippedSubviews removeClippedSubviews

View File

@ -12,18 +12,22 @@ export default class TabBar extends React.PureComponent {
} }
render() { render() {
const {
tabs, goToPage, tabEmojiStyle, activeTab
} = this.props;
return ( return (
<View style={styles.tabsContainer}> <View style={styles.tabsContainer}>
{this.props.tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
key={tab} key={tab}
onPress={() => this.props.goToPage(i)} onPress={() => goToPage(i)}
style={styles.tab} style={styles.tab}
testID={`reaction-picker-${ tab }`} testID={`reaction-picker-${ tab }`}
> >
<Text style={[styles.tabEmoji, this.props.tabEmojiStyle]}>{tab}</Text> <Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />} {activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>

View File

@ -49,36 +49,41 @@ export default class EmojiPicker extends Component {
this.updateFrequentlyUsed(); this.updateFrequentlyUsed();
this.updateCustomEmojis(); this.updateCustomEmojis();
} }
componentWillUnmount() { componentWillUnmount() {
this.frequentlyUsed.removeAllListeners(); this.frequentlyUsed.removeAllListeners();
this.customEmojis.removeAllListeners(); this.customEmojis.removeAllListeners();
} }
onEmojiSelected(emoji) { onEmojiSelected(emoji) {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) { if (emoji.isCustom) {
const count = this._getFrequentlyUsedCount(emoji.content); const count = this._getFrequentlyUsedCount(emoji.content);
this._addFrequentlyUsed({ this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, count, isCustom: true content: emoji.content, extension: emoji.extension, count, isCustom: true
}); });
this.props.onEmojiSelected(`:${ emoji.content }:`); onEmojiSelected(`:${ emoji.content }:`);
} else { } else {
const content = emoji; const content = emoji;
const count = this._getFrequentlyUsedCount(content); const count = this._getFrequentlyUsedCount(content);
this._addFrequentlyUsed({ content, count, isCustom: false }); this._addFrequentlyUsed({ content, count, isCustom: false });
const shortname = `:${ emoji }:`; const shortname = `:${ emoji }:`;
this.props.onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname); onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname);
} }
} }
// eslint-disable-next-line react/sort-comp
_addFrequentlyUsed = protectedFunction((emoji) => { _addFrequentlyUsed = protectedFunction((emoji) => {
database.write(() => { database.write(() => {
database.create('frequentlyUsedEmoji', emoji, true); database.create('frequentlyUsedEmoji', emoji, true);
}); });
}) })
_getFrequentlyUsedCount = (content) => { _getFrequentlyUsedCount = (content) => {
const emojiRow = this.frequentlyUsed.filtered('content == $0', content); const emojiRow = this.frequentlyUsed.filtered('content == $0', content);
return emojiRow.length ? emojiRow[0].count + 1 : 1; return emojiRow.length ? emojiRow[0].count + 1 : 1;
} }
updateFrequentlyUsed() { updateFrequentlyUsed() {
const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => { const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) { if (item.isCustom) {
@ -90,17 +95,19 @@ export default class EmojiPicker extends Component {
} }
updateCustomEmojis() { updateCustomEmojis() {
const customEmojis = map(this.customEmojis.slice(), item => const customEmojis = map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true }));
({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis }); this.setState({ customEmojis });
} }
renderCategory(category, i) { renderCategory(category, i) {
const { frequentlyUsed, customEmojis } = this.state;
const { emojisPerRow, width, baseUrl } = this.props;
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
emojis = this.state.frequentlyUsed; emojis = frequentlyUsed;
} else if (i === 1) { } else if (i === 1) {
emojis = this.state.customEmojis; emojis = customEmojis;
} else { } else {
emojis = emojisByCategory[category]; emojis = emojisByCategory[category];
} }
@ -109,21 +116,23 @@ export default class EmojiPicker extends Component {
emojis={emojis} emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)} onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
style={styles.categoryContainer} style={styles.categoryContainer}
size={this.props.emojisPerRow} size={emojisPerRow}
width={this.props.width} width={width}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/> />
); );
} }
render() { render() {
if (!this.state.show) { const { show } = this.state;
const { tabEmojiStyle } = this.props;
if (!show) {
return null; return null;
} }
return ( return (
// <View style={styles.container}>
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />} renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} />}
contentProps={scrollProps} contentProps={scrollProps}
style={styles.background} style={styles.background}
> >
@ -140,7 +149,6 @@ export default class EmojiPicker extends Component {
)) ))
} }
</ScrollableTabView> </ScrollableTabView>
// </View>
); );
} }
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet, View, Modal, Animated } from 'react-native'; import {
StyleSheet, View, Modal, Animated
} from 'react-native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -27,8 +29,11 @@ export default class Loading extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
const { opacity, scale } = this.state;
const { visible } = this.props;
this.opacityAnimation = Animated.timing( this.opacityAnimation = Animated.timing(
this.state.opacity, opacity,
{ {
toValue: 1, toValue: 1,
duration: 1000, duration: 1000,
@ -37,7 +42,7 @@ export default class Loading extends React.PureComponent {
); );
this.scaleAnimation = Animated.loop(Animated.sequence([ this.scaleAnimation = Animated.loop(Animated.sequence([
Animated.timing( Animated.timing(
this.state.scale, scale,
{ {
toValue: 0, toValue: 0,
duration: 1000, duration: 1000,
@ -45,7 +50,7 @@ export default class Loading extends React.PureComponent {
} }
), ),
Animated.timing( Animated.timing(
this.state.scale, scale,
{ {
toValue: 1, toValue: 1,
duration: 1000, duration: 1000,
@ -54,13 +59,14 @@ export default class Loading extends React.PureComponent {
) )
])); ]));
if (this.props.visible) { if (visible) {
this.startAnimations(); this.startAnimations();
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.visible && this.props.visible !== prevProps.visible) { const { visible } = this.props;
if (visible && visible !== prevProps.visible) {
this.startAnimations(); this.startAnimations();
} }
} }
@ -84,13 +90,16 @@ export default class Loading extends React.PureComponent {
} }
render() { render() {
const scale = this.state.scale.interpolate({ const { opacity, scale } = this.state;
const { visible } = this.props;
const scaleAnimation = scale.interpolate({
inputRange: [0, 0.5, 1], inputRange: [0, 0.5, 1],
outputRange: [1, 1.1, 1] outputRange: [1, 1.1, 1]
}); });
return ( return (
<Modal <Modal
visible={this.props.visible} visible={visible}
transparent transparent
onRequestClose={() => {}} onRequestClose={() => {}}
> >
@ -98,9 +107,9 @@ export default class Loading extends React.PureComponent {
<Animated.Image <Animated.Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
style={[styles.image, { style={[styles.image, {
opacity: this.state.opacity, opacity,
transform: [{ transform: [{
scale scale: scaleAnimation
}] }]
}]} }]}
/> />

View File

@ -1,18 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert, Clipboard, Vibration, Share } from 'react-native'; import {
Alert, Clipboard, Vibration, Share
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import * as moment from 'moment'; import * as moment from 'moment';
import { import {
deleteRequest, actionsHide as actionsHideAction,
editInit, deleteRequest as deleteRequestAction,
toggleStarRequest, editInit as editInitAction,
togglePinRequest, replyInit as replyInitAction,
actionsHide, togglePinRequest as togglePinRequestAction,
toggleReactionPicker, toggleReactionPicker as toggleReactionPickerAction,
replyInit toggleStarRequest as toggleStarRequestAction
} from '../actions/messages'; } from '../actions/messages';
import { showToast } from '../utils/info'; import { showToast } from '../utils/info';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -29,13 +31,13 @@ import I18n from '../i18n';
Message_AllowStarring: state.settings.Message_AllowStarring Message_AllowStarring: state.settings.Message_AllowStarring
}), }),
dispatch => ({ dispatch => ({
actionsHide: () => dispatch(actionsHide()), actionsHide: () => dispatch(actionsHideAction()),
deleteRequest: message => dispatch(deleteRequest(message)), deleteRequest: message => dispatch(deleteRequestAction(message)),
editInit: message => dispatch(editInit(message)), editInit: message => dispatch(editInitAction(message)),
toggleStarRequest: message => dispatch(toggleStarRequest(message)), toggleStarRequest: message => dispatch(toggleStarRequestAction(message)),
togglePinRequest: message => dispatch(togglePinRequest(message)), togglePinRequest: message => dispatch(togglePinRequestAction(message)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)), toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
replyInit: (message, mention) => dispatch(replyInit(message, mention)) replyInit: (message, mention) => dispatch(replyInitAction(message, mention))
}) })
) )
export default class MessageActions extends React.Component { export default class MessageActions extends React.Component {
@ -63,6 +65,8 @@ export default class MessageActions extends React.Component {
this.handleActionPress = this.handleActionPress.bind(this); this.handleActionPress = this.handleActionPress.bind(this);
this.setPermissions(); this.setPermissions();
const { Message_AllowStarring, Message_AllowPinning } = this.props;
// Cancel // Cancel
this.options = [I18n.t('Cancel')]; this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
@ -98,13 +102,13 @@ export default class MessageActions extends React.Component {
} }
// Star // Star
if (this.props.Message_AllowStarring) { if (Message_AllowStarring) {
this.options.push(I18n.t(props.actionMessage.starred ? 'Unstar' : 'Star')); this.options.push(I18n.t(props.actionMessage.starred ? 'Unstar' : 'Star'));
this.STAR_INDEX = this.options.length - 1; this.STAR_INDEX = this.options.length - 1;
} }
// Pin // Pin
if (this.props.Message_AllowPinning) { if (Message_AllowPinning) {
this.options.push(I18n.t(props.actionMessage.pinned ? 'Unpin' : 'Pin')); this.options.push(I18n.t(props.actionMessage.pinned ? 'Unpin' : 'Pin'));
this.PIN_INDEX = this.options.length - 1; this.PIN_INDEX = this.options.length - 1;
} }
@ -129,8 +133,9 @@ export default class MessageActions extends React.Component {
} }
setPermissions() { setPermissions() {
const { room } = this.props;
const permissions = ['edit-message', 'delete-message', 'force-delete-message']; const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
const result = RocketChat.hasPermission(permissions, this.props.room.rid); const result = RocketChat.hasPermission(permissions, room.rid);
this.hasEditPermission = result[permissions[0]]; this.hasEditPermission = result[permissions[0]];
this.hasDeletePermission = result[permissions[1]]; this.hasDeletePermission = result[permissions[1]];
this.hasForceDeletePermission = result[permissions[2]]; this.hasForceDeletePermission = result[permissions[2]];
@ -146,20 +151,27 @@ export default class MessageActions extends React.Component {
isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id; isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id;
isRoomReadOnly = () => this.props.room.ro; isRoomReadOnly = () => {
const { room } = this.props;
return room.ro;
}
canReactWhenReadOnly = () => this.props.room.reactWhenReadOnly; canReactWhenReadOnly = () => {
const { room } = this.props;
return room.reactWhenReadOnly;
}
allowEdit = (props) => { allowEdit = (props) => {
if (this.isRoomReadOnly()) { if (this.isRoomReadOnly()) {
return false; return false;
} }
const editOwn = this.isOwn(props); const editOwn = this.isOwn(props);
const { Message_AllowEditing: isEditAllowed } = this.props; const { Message_AllowEditing: isEditAllowed, Message_AllowEditing_BlockEditInMinutes } = this.props;
if (!(this.hasEditPermission || (isEditAllowed && editOwn))) { if (!(this.hasEditPermission || (isEditAllowed && editOwn))) {
return false; return false;
} }
const blockEditInMinutes = this.props.Message_AllowEditing_BlockEditInMinutes; const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) { if (blockEditInMinutes) {
let msgTs; let msgTs;
if (props.actionMessage.ts != null) { if (props.actionMessage.ts != null) {
@ -179,14 +191,14 @@ export default class MessageActions extends React.Component {
return false; return false;
} }
const deleteOwn = this.isOwn(props); const deleteOwn = this.isOwn(props);
const { Message_AllowDeleting: isDeleteAllowed } = this.props; const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props;
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) { if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {
return false; return false;
} }
if (this.hasForceDeletePermission) { if (this.hasForceDeletePermission) {
return true; return true;
} }
const blockDeleteInMinutes = this.props.Message_AllowDeleting_BlockDeleteInMinutes; const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs; let msgTs;
if (props.actionMessage.ts != null) { if (props.actionMessage.ts != null) {
@ -201,7 +213,8 @@ export default class MessageActions extends React.Component {
return true; return true;
} }
handleDelete() { handleDelete = () => {
const { deleteRequest, actionMessage } = this.props;
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
I18n.t('You_will_not_be_able_to_recover_this_message'), I18n.t('You_will_not_be_able_to_recover_this_message'),
@ -213,56 +226,66 @@ export default class MessageActions extends React.Component {
{ {
text: I18n.t('Yes_action_it', { action: 'delete' }), text: I18n.t('Yes_action_it', { action: 'delete' }),
style: 'destructive', style: 'destructive',
onPress: () => this.props.deleteRequest(this.props.actionMessage) onPress: () => deleteRequest(actionMessage)
} }
], ],
{ cancelable: false } { cancelable: false }
); );
} }
handleEdit() { handleEdit = () => {
const { _id, msg, rid } = this.props.actionMessage; const { actionMessage, editInit } = this.props;
this.props.editInit({ _id, msg, rid }); const { _id, msg, rid } = actionMessage;
editInit({ _id, msg, rid });
} }
handleCopy = async() => { handleCopy = async() => {
await Clipboard.setString(this.props.actionMessage.msg); const { actionMessage } = this.props;
await Clipboard.setString(actionMessage.msg);
showToast(I18n.t('Copied_to_clipboard')); showToast(I18n.t('Copied_to_clipboard'));
} }
handleShare = async() => { handleShare = async() => {
const { actionMessage } = this.props;
Share.share({ Share.share({
message: this.props.actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '') message: actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '')
}); });
}; };
handleStar() { handleStar = () => {
this.props.toggleStarRequest(this.props.actionMessage); const { actionMessage, toggleStarRequest } = this.props;
toggleStarRequest(actionMessage);
} }
async handlePermalink() { handlePermalink = async() => {
const permalink = await this.getPermalink(this.props.actionMessage); const { actionMessage } = this.props;
const permalink = await this.getPermalink(actionMessage);
Clipboard.setString(permalink); Clipboard.setString(permalink);
showToast(I18n.t('Permalink_copied_to_clipboard')); showToast(I18n.t('Permalink_copied_to_clipboard'));
} }
handlePin() { handlePin = () => {
this.props.togglePinRequest(this.props.actionMessage); const { actionMessage, togglePinRequest } = this.props;
togglePinRequest(actionMessage);
} }
handleReply() { handleReply = () => {
this.props.replyInit(this.props.actionMessage, true); const { actionMessage, replyInit } = this.props;
replyInit(actionMessage, true);
} }
handleQuote() { handleQuote = () => {
this.props.replyInit(this.props.actionMessage, false); const { actionMessage, replyInit } = this.props;
replyInit(actionMessage, false);
} }
handleReaction() { handleReaction = () => {
this.props.toggleReactionPicker(this.props.actionMessage); const { actionMessage, toggleReactionPicker } = this.props;
toggleReactionPicker(actionMessage);
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { actionsHide } = this.props;
switch (actionIndex) { switch (actionIndex) {
case this.REPLY_INDEX: case this.REPLY_INDEX:
this.handleReply(); this.handleReply();
@ -297,7 +320,7 @@ export default class MessageActions extends React.Component {
default: default:
break; break;
} }
this.props.actionsHide(); actionsHide();
} }
render() { render() {

View File

@ -12,9 +12,11 @@ export default class EmojiKeyboard extends React.PureComponent {
const state = store.getState(); const state = store.getState();
this.baseUrl = state.settings.Site_Url || state.server ? state.server.server : ''; this.baseUrl = state.settings.Site_Url || state.server ? state.server.server : '';
} }
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
} }
render() { render() {
return ( return (
<View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'> <View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>

View File

@ -34,7 +34,7 @@ export default class FilesActions extends Component {
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { takePhoto, chooseFromLibrary } = this.props; const { takePhoto, chooseFromLibrary, hideActions } = this.props;
switch (actionIndex) { switch (actionIndex) {
case this.PHOTO_INDEX: case this.PHOTO_INDEX:
takePhoto(); takePhoto();
@ -45,7 +45,7 @@ export default class FilesActions extends Component {
default: default:
break; break;
} }
this.props.hideActions(); hideActions();
} }
render() { render() {

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, SafeAreaView, Platform, PermissionsAndroid, Text } from 'react-native'; import {
View, SafeAreaView, Platform, PermissionsAndroid, Text
} from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio'; import { AudioRecorder, AudioUtils } from 'react-native-audio';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import styles from './styles'; import styles from './styles';
@ -61,7 +63,7 @@ export default class extends React.PureComponent {
// //
AudioRecorder.onFinished = (data) => { AudioRecorder.onFinished = (data) => {
if (!this.recordingCanceled && Platform.OS === 'ios') { if (!this.recordingCanceled && Platform.OS === 'ios') {
this._finishRecording(data.status === 'OK', data.audioFileURL); this.finishRecording(data.status === 'OK', data.audioFileURL);
} }
}; };
AudioRecorder.startRecording(); AudioRecorder.startRecording();
@ -73,9 +75,10 @@ export default class extends React.PureComponent {
} }
} }
_finishRecording(didSucceed, filePath) { finishRecording = (didSucceed, filePath) => {
const { onFinish } = this.props;
if (!didSucceed) { if (!didSucceed) {
return this.props.onFinish && this.props.onFinish(didSucceed); return onFinish && onFinish(didSucceed);
} }
const path = filePath.startsWith('file://') ? filePath.split('file://')[1] : filePath; const path = filePath.startsWith('file://') ? filePath.split('file://')[1] : filePath;
@ -84,7 +87,7 @@ export default class extends React.PureComponent {
store: 'Uploads', store: 'Uploads',
path path
}; };
return this.props.onFinish && this.props.onFinish(fileInfo); return onFinish && onFinish(fileInfo);
} }
finishAudioMessage = async() => { finishAudioMessage = async() => {
@ -92,10 +95,10 @@ export default class extends React.PureComponent {
this.recording = false; this.recording = false;
const filePath = await AudioRecorder.stopRecording(); const filePath = await AudioRecorder.stopRecording();
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
this._finishRecording(true, filePath); this.finishRecording(true, filePath);
} }
} catch (err) { } catch (err) {
this._finishRecording(false); this.finishRecording(false);
console.error(err); console.error(err);
} }
} }
@ -104,10 +107,12 @@ export default class extends React.PureComponent {
this.recording = false; this.recording = false;
this.recordingCanceled = true; this.recordingCanceled = true;
await AudioRecorder.stopRecording(); await AudioRecorder.stopRecording();
return this._finishRecording(false); return this.finishRecording(false);
} }
render() { render() {
const { currentTime } = this.state;
return ( return (
<SafeAreaView <SafeAreaView
key='messagebox-recording' key='messagebox-recording'
@ -123,7 +128,7 @@ export default class extends React.PureComponent {
accessibilityTraits='button' accessibilityTraits='button'
onPress={this.cancelAudioMessage} onPress={this.cancelAudioMessage}
/> />
<Text key='currentTime' style={styles.textBoxInput}>{this.state.currentTime}</Text> <Text key='currentTime' style={styles.textBoxInput}>{currentTime}</Text>
<Icon <Icon
style={[styles.actionButtons, { color: 'green' }]} style={[styles.actionButtons, { color: 'green' }]}
name='check' name='check'

View File

@ -57,7 +57,8 @@ export default class ReplyPreview extends Component {
} }
close = () => { close = () => {
this.props.close(); const { close } = this.props;
close();
} }
render() { render() {

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View, Text, StyleSheet, Image, ScrollView, Platform } from 'react-native'; import {
View, Text, StyleSheet, Image, ScrollView, Platform
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
@ -85,9 +87,9 @@ export default class UploadModal extends Component {
return ( return (
<Modal <Modal
isVisible={isVisible} isVisible={isVisible}
style={{ alignItems: 'center' }} // TODO: need this? style={{ alignItems: 'center' }}
onBackdropPress={() => this.props.close()} onBackdropPress={() => close()}
onBackButtonPress={() => this.props.close()} onBackButtonPress={() => close()}
animationIn='fadeIn' animationIn='fadeIn'
animationOut='fadeOut' animationOut='fadeOut'
useNativeDriver useNativeDriver

View File

@ -1,15 +1,21 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, TextInput, FlatList, Text, TouchableOpacity, Alert } from 'react-native'; import {
View, TextInput, FlatList, Text, TouchableOpacity, Alert
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input'; import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker from 'react-native-image-crop-picker';
import { userTyping } from '../../actions/room'; import { userTyping as userTypingAction } from '../../actions/room';
import {
editRequest as editRequestAction,
editCancel as editCancelAction,
replyCancel as replyCancelAction
} from '../../actions/messages';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import { editRequest, editCancel, replyCancel } from '../../actions/messages';
import styles from './styles'; import styles from './styles';
import MyIcon from '../icons'; import MyIcon from '../icons';
import database from '../../lib/realm'; import database from '../../lib/realm';
@ -48,10 +54,10 @@ const imagePickerConfig = {
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
username: state.login.user && state.login.user.username username: state.login.user && state.login.user.username
}), dispatch => ({ }), dispatch => ({
editCancel: () => dispatch(editCancel()), editCancel: () => dispatch(editCancelAction()),
editRequest: message => dispatch(editRequest(message)), editRequest: message => dispatch(editRequestAction(message)),
typing: status => dispatch(userTyping(status)), typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancel()) closeReply: () => dispatch(replyCancelAction())
})) }))
export default class MessageBox extends React.PureComponent { export default class MessageBox extends React.PureComponent {
static propTypes = { static propTypes = {
@ -86,13 +92,15 @@ export default class MessageBox extends React.PureComponent {
this.rooms = []; this.rooms = [];
this.emojis = []; this.emojis = [];
this.customEmojis = []; this.customEmojis = [];
this._onEmojiSelected = this._onEmojiSelected.bind(this); this.onEmojiSelected = this.onEmojiSelected.bind(this);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.message !== nextProps.message && nextProps.message.msg) { const { message, replyMessage } = this.props;
if (message !== nextProps.message && nextProps.message.msg) {
this.setState({ text: nextProps.message.msg }); this.setState({ text: nextProps.message.msg });
this.component.focus(); this.component.focus();
} else if (this.props.replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) { } else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
this.component.focus(); this.component.focus();
} else if (!nextProps.message) { } else if (!nextProps.message) {
this.setState({ text: '' }); this.setState({ text: '' });
@ -100,8 +108,10 @@ export default class MessageBox extends React.PureComponent {
} }
onChangeText(text) { onChangeText(text) {
const { typing } = this.props;
this.setState({ text }); this.setState({ text });
this.props.typing(text.length > 0); typing(text.length > 0);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const { start, end } = this.component._lastNativeSelection; const { start, end } = this.component._lastNativeSelection;
@ -126,45 +136,95 @@ export default class MessageBox extends React.PureComponent {
this.closeEmoji(); this.closeEmoji();
} }
get leftButtons() { onPressMention(item) {
const { editing } = this.props; const { trackingType } = this.state;
if (editing) {
return (<Icon const msg = this.component._lastNativeText;
style={styles.actionButtons}
name='close' const { start, end } = this.component._lastNativeSelection;
accessibilityLabel={I18n.t('Cancel_editing')}
accessibilityTraits='button' const cursor = Math.max(start, end);
onPress={() => this.editCancel()}
testID='messagebox-cancel-editing' const regexp = /([a-z0-9._-]+)$/im;
/>);
} const result = msg.substr(0, cursor).replace(regexp, '');
return !this.state.showEmojiKeyboard ? (<Icon const mentionName = trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
style={styles.actionButtons} ? `${ item.name || item }:`
onPress={() => this.openEmoji()} : (item.username || item.name);
accessibilityLabel={I18n.t('Open_emoji_selector')} const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
accessibilityTraits='button' this.component.setNativeProps({ text });
name='mood' this.setState({ text });
testID='messagebox-open-emoji' this.component.focus();
/>) : (<Icon requestAnimationFrame(() => this.stopTrackingMention());
onPress={() => this.closeEmoji()}
style={styles.actionButtons}
accessibilityLabel={I18n.t('Close_emoji_selector')}
accessibilityTraits='button'
name='keyboard'
testID='messagebox-close-emoji'
/>);
} }
onEmojiSelected(keyboardId, params) {
const { text } = this.state;
const { emoji } = params;
let newText = '';
// if messagebox has an active cursor
if (this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {
// if messagebox doesn't have a cursor, just append selected emoji
newText = `${ text }${ emoji }`;
}
this.component.setNativeProps({ text: newText });
this.setState({ text: newText });
}
get leftButtons() {
const { showEmojiKeyboard } = this.state;
const { editing } = this.props;
if (editing) {
return (
<Icon
style={styles.actionButtons}
name='close'
accessibilityLabel={I18n.t('Cancel_editing')}
accessibilityTraits='button'
onPress={() => this.editCancel()}
testID='messagebox-cancel-editing'
/>
);
}
return !showEmojiKeyboard
? (
<Icon
style={styles.actionButtons}
onPress={() => this.openEmoji()}
accessibilityLabel={I18n.t('Open_emoji_selector')}
accessibilityTraits='button'
name='mood'
testID='messagebox-open-emoji'
/>)
: (
<Icon
onPress={() => this.closeEmoji()}
style={styles.actionButtons}
accessibilityLabel={I18n.t('Close_emoji_selector')}
accessibilityTraits='button'
name='keyboard'
testID='messagebox-close-emoji'
/>);
}
get rightButtons() { get rightButtons() {
const { text } = this.state;
const icons = []; const icons = [];
if (this.state.text) { if (text) {
icons.push(<MyIcon icons.push(<MyIcon
style={[styles.actionButtons, { color: '#1D74F5' }]} style={[styles.actionButtons, { color: '#1D74F5' }]}
name='send' name='send'
key='sendIcon' key='sendIcon'
accessibilityLabel={I18n.t('Send message')} accessibilityLabel={I18n.t('Send message')}
accessibilityTraits='button' accessibilityTraits='button'
onPress={() => this.submit(this.state.text)} onPress={() => this.submit(text)}
testID='messagebox-send-message' testID='messagebox-send-message'
/>); />);
return icons; return icons;
@ -198,123 +258,7 @@ export default class MessageBox extends React.PureComponent {
} }
} }
toggleFilesActions = () => { getFixedMentions(keyword) {
this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction }));
}
sendImageMessage = async(file) => {
this.setState({ file: { isVisible: false } });
const fileInfo = {
name: file.name,
description: file.description,
size: file.size,
type: file.mime,
store: 'Uploads',
path: file.path
};
try {
await RocketChat.sendFileMessage(this.props.rid, fileInfo);
} catch (e) {
log('sendImageMessage', e);
}
}
takePhoto = async() => {
try {
const image = await ImagePicker.openCamera(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('takePhoto', e);
}
}
chooseFromLibrary = async() => {
try {
const image = await ImagePicker.openPicker(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('chooseFromLibrary', e);
}
}
showUploadModal = (file) => {
this.setState({ file: { ...file, isVisible: true } });
}
editCancel() {
this.props.editCancel();
this.setState({ text: '' });
}
async openEmoji() {
await this.setState({
showEmojiKeyboard: true
});
}
async recordAudioMessage() {
const recording = await Recording.permission();
this.setState({ recording });
}
finishAudioMessage = async(fileInfo) => {
this.setState({
recording: false
});
if (fileInfo) {
try {
await RocketChat.sendFileMessage(this.props.rid, fileInfo);
} catch (e) {
if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error));
}
log('finishAudioMessage', e);
}
}
}
closeEmoji() {
this.setState({ showEmojiKeyboard: false });
}
async submit(message) {
this.setState({ text: '' });
this.closeEmoji();
this.stopTrackingMention();
this.props.typing(false);
if (message.trim() === '') {
return;
}
// if is editing a message
const {
editing, replying
} = this.props;
if (editing) {
const { _id, rid } = this.props.message;
this.props.editRequest({ _id, msg: message, rid });
} else if (replying) {
const {
username, replyMessage, roomType, closeReply
} = this.props;
const permalink = await this.getPermalink(replyMessage);
let msg = `[ ](${ permalink }) `;
// if original message wasn't sent by current user and neither from a direct room
if (username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
msg += `@${ replyMessage.u.username } `;
}
msg = `${ msg } ${ message }`;
this.props.onSubmit(msg);
closeReply();
} else {
// if is submiting a new message
this.props.onSubmit(message);
}
}
_getFixedMentions(keyword) {
if ('all'.indexOf(keyword) !== -1) { if ('all'.indexOf(keyword) !== -1) {
this.users = [{ _id: -1, username: 'all' }, ...this.users]; this.users = [{ _id: -1, username: 'all' }, ...this.users];
} }
@ -323,12 +267,12 @@ export default class MessageBox extends React.PureComponent {
} }
} }
async _getUsers(keyword) { async getUsers(keyword) {
this.users = database.objects('users'); this.users = database.objects('users');
if (keyword) { if (keyword) {
this.users = this.users.filtered('username CONTAINS[c] $0', keyword); this.users = this.users.filtered('username CONTAINS[c] $0', keyword);
} }
this._getFixedMentions(keyword); this.getFixedMentions(keyword);
this.setState({ mentions: this.users.slice() }); this.setState({ mentions: this.users.slice() });
const usernames = []; const usernames = [];
@ -359,12 +303,12 @@ export default class MessageBox extends React.PureComponent {
} finally { } finally {
delete this.oldPromise; 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();
this._getFixedMentions(keyword); this.getFixedMentions(keyword);
this.setState({ mentions: this.users }); this.setState({ mentions: this.users });
} }
} }
async _getRooms(keyword = '') { async getRooms(keyword = '') {
this.roomsCache = this.roomsCache || []; this.roomsCache = this.roomsCache || [];
this.rooms = database.objects('subscriptions') this.rooms = database.objects('subscriptions')
.filtered('t != $0', 'd'); .filtered('t != $0', 'd');
@ -406,7 +350,7 @@ export default class MessageBox extends React.PureComponent {
} }
} }
_getEmojis(keyword) { getEmojis(keyword) {
if (keyword) { if (keyword) {
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4); 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); this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4);
@ -415,6 +359,149 @@ export default class MessageBox extends React.PureComponent {
} }
} }
toggleFilesActions = () => {
this.setState(prevState => ({ showFilesAction: !prevState.showFilesAction }));
}
sendImageMessage = async(file) => {
const { rid } = this.props;
this.setState({ file: { isVisible: false } });
const fileInfo = {
name: file.name,
description: file.description,
size: file.size,
type: file.mime,
store: 'Uploads',
path: file.path
};
try {
await RocketChat.sendFileMessage(rid, fileInfo);
} catch (e) {
log('sendImageMessage', e);
}
}
takePhoto = async() => {
try {
const image = await ImagePicker.openCamera(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('takePhoto', e);
}
}
chooseFromLibrary = async() => {
try {
const image = await ImagePicker.openPicker(imagePickerConfig);
this.showUploadModal(image);
} catch (e) {
log('chooseFromLibrary', e);
}
}
showUploadModal = (file) => {
this.setState({ file: { ...file, isVisible: true } });
}
editCancel = () => {
const { editCancel } = this.props;
editCancel();
this.setState({ text: '' });
}
openEmoji = async() => {
await this.setState({
showEmojiKeyboard: true
});
}
recordAudioMessage = async() => {
const recording = await Recording.permission();
this.setState({ recording });
}
finishAudioMessage = async(fileInfo) => {
const { rid } = this.props;
this.setState({
recording: false
});
if (fileInfo) {
try {
await RocketChat.sendFileMessage(rid, fileInfo);
} catch (e) {
if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error));
}
log('finishAudioMessage', e);
}
}
}
closeEmoji = () => {
this.setState({ showEmojiKeyboard: false });
}
submit = async(message) => {
const {
typing, message: editingMessage, editRequest, onSubmit
} = this.props;
this.setState({ text: '' });
this.closeEmoji();
this.stopTrackingMention();
typing(false);
if (message.trim() === '') {
return;
}
// if is editing a message
const {
editing, replying
} = this.props;
if (editing) {
const { _id, rid } = editingMessage;
editRequest({ _id, msg: message, rid });
} else if (replying) {
const {
username, replyMessage, roomType, closeReply
} = this.props;
const permalink = await this.getPermalink(replyMessage);
let msg = `[ ](${ permalink }) `;
// if original message wasn't sent by current user and neither from a direct room
if (username !== replyMessage.u.username && roomType !== 'd' && replyMessage.mention) {
msg += `@${ replyMessage.u.username } `;
}
msg = `${ msg } ${ message }`;
onSubmit(msg);
closeReply();
} else {
// if is submiting a new message
onSubmit(message);
}
}
updateMentions = (keyword, type) => {
if (type === MENTIONS_TRACKING_TYPE_USERS) {
this.getUsers(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) {
this.getEmojis(keyword);
} else {
this.getRooms(keyword);
}
}
identifyMentionKeyword(keyword, type) {
this.setState({
showEmojiKeyboard: false,
trackingType: type
});
this.updateMentions(keyword, type);
}
stopTrackingMention() { stopTrackingMention() {
this.setState({ this.setState({
mentions: [], mentions: [],
@ -426,76 +513,26 @@ export default class MessageBox extends React.PureComponent {
this.emojis = []; this.emojis = [];
} }
identifyMentionKeyword(keyword, type) {
this.setState({
showEmojiKeyboard: false,
trackingType: type
});
this.updateMentions(keyword, type);
}
updateMentions = (keyword, type) => {
if (type === MENTIONS_TRACKING_TYPE_USERS) {
this._getUsers(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) {
this._getEmojis(keyword);
} else {
this._getRooms(keyword);
}
}
_onPressMention(item) {
const msg = this.component._lastNativeText;
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, '');
const mentionName = this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
`${ item.name || item }:` : (item.username || item.name);
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
this.component.setNativeProps({ text });
this.setState({ text });
this.component.focus();
requestAnimationFrame(() => this.stopTrackingMention());
}
_onEmojiSelected(keyboardId, params) {
const { text } = this.state;
const { emoji } = params;
let newText = '';
// if messagebox has an active cursor
if (this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {
// if messagebox doesn't have a cursor, just append selected emoji
newText = `${ text }${ emoji }`;
}
this.component.setNativeProps({ text: newText });
this.setState({ text: newText });
}
renderFixedMentionItem = item => ( renderFixedMentionItem = item => (
<TouchableOpacity <TouchableOpacity
style={styles.mentionItem} style={styles.mentionItem}
onPress={() => this._onPressMention(item)} onPress={() => this.onPressMention(item)}
> >
<Text style={styles.fixedMentionAvatar}>{item.username}</Text> <Text style={styles.fixedMentionAvatar}>{item.username}</Text>
<Text>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text> <Text>{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}</Text>
</TouchableOpacity> </TouchableOpacity>
) )
renderMentionEmoji = (item) => { renderMentionEmoji = (item) => {
const { baseUrl } = this.props;
if (item.name) { if (item.name) {
return ( return (
<CustomEmoji <CustomEmoji
key='mention-item-avatar' key='mention-item-avatar'
style={styles.mentionItemCustomEmoji} style={styles.mentionItemCustomEmoji}
emoji={item} emoji={item}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/> />
); );
} }
@ -508,18 +545,22 @@ export default class MessageBox extends React.PureComponent {
</Text> </Text>
); );
} }
renderMentionItem = (item) => { renderMentionItem = (item) => {
const { trackingType } = this.state;
const { baseUrl } = this.props;
if (item.username === 'all' || item.username === 'here') { if (item.username === 'all' || item.username === 'here') {
return this.renderFixedMentionItem(item); return this.renderFixedMentionItem(item);
} }
return ( return (
<TouchableOpacity <TouchableOpacity
style={styles.mentionItem} style={styles.mentionItem}
onPress={() => this._onPressMention(item)} onPress={() => this.onPressMention(item)}
testID={`mention-item-${ this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`} testID={`mention-item-${ trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
> >
{this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? {trackingType === MENTIONS_TRACKING_TYPE_EMOJIS
[ ? [
this.renderMentionEmoji(item), this.renderMentionEmoji(item),
<Text key='mention-item-name'>:{ item.name || item }:</Text> <Text key='mention-item-name'>:{ item.name || item }:</Text>
] ]
@ -530,7 +571,7 @@ export default class MessageBox extends React.PureComponent {
text={item.username || item.name} text={item.username || item.name}
size={30} size={30}
type={item.username ? 'd' : 'c'} type={item.username ? 'd' : 'c'}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/>, />,
<Text key='mention-item-name'>{ item.username || item.name }</Text> <Text key='mention-item-name'>{ item.username || item.name }</Text>
] ]
@ -538,6 +579,7 @@ export default class MessageBox extends React.PureComponent {
</TouchableOpacity> </TouchableOpacity>
); );
} }
renderMentions = () => { renderMentions = () => {
const { mentions, trackingType } = this.state; const { mentions, trackingType } = this.state;
if (!trackingType) { if (!trackingType) {
@ -567,7 +609,9 @@ export default class MessageBox extends React.PureComponent {
}; };
renderFilesActions = () => { renderFilesActions = () => {
if (!this.state.showFilesAction) { const { showFilesAction } = this.state;
if (!showFilesAction) {
return null; return null;
} }
return ( return (
@ -581,7 +625,10 @@ export default class MessageBox extends React.PureComponent {
} }
renderContent() { renderContent() {
if (this.state.recording) { const { recording, text } = this.state;
const { editing } = this.props;
if (recording) {
return (<Recording onFinish={this.finishAudioMessage} />); return (<Recording onFinish={this.finishAudioMessage} />);
} }
return ( return (
@ -590,7 +637,7 @@ export default class MessageBox extends React.PureComponent {
<View style={styles.composer} key='messagebox'> <View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()} {this.renderReplyPreview()}
<View <View
style={[styles.textArea, this.props.editing && styles.editing]} style={[styles.textArea, editing && styles.editing]}
testID='messagebox' testID='messagebox'
> >
{this.leftButtons} {this.leftButtons}
@ -601,8 +648,8 @@ export default class MessageBox extends React.PureComponent {
keyboardType='twitter' keyboardType='twitter'
blurOnSubmit={false} blurOnSubmit={false}
placeholder={I18n.t('New_Message')} placeholder={I18n.t('New_Message')}
onChangeText={text => this.onChangeText(text)} onChangeText={t => this.onChangeText(t)}
value={this.state.text} value={text}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
defaultValue='' defaultValue=''
multiline multiline
@ -617,15 +664,16 @@ export default class MessageBox extends React.PureComponent {
} }
render() { render() {
const { showEmojiKeyboard, file } = this.state;
return ( return (
[ [
<KeyboardAccessoryView <KeyboardAccessoryView
key='input' key='input'
renderContent={() => this.renderContent()} renderContent={() => this.renderContent()}
kbInputRef={this.component} kbInputRef={this.component}
kbComponent={this.state.showEmojiKeyboard ? 'EmojiKeyboard' : null} kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
onKeyboardResigned={() => this.onKeyboardResigned()} onKeyboardResigned={() => this.onKeyboardResigned()}
onItemSelected={this._onEmojiSelected} onItemSelected={this.onEmojiSelected}
trackInteractive trackInteractive
// revealKeyboardInteractive // revealKeyboardInteractive
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
@ -634,8 +682,8 @@ export default class MessageBox extends React.PureComponent {
this.renderFilesActions(), this.renderFilesActions(),
<UploadModal <UploadModal
key='upload-modal' key='upload-modal'
isVisible={(this.state.file && this.state.file.isVisible)} isVisible={(file && file.isVisible)}
file={this.state.file} file={file}
close={() => this.setState({ file: {} })} close={() => this.setState({ file: {} })}
submit={this.sendImageMessage} submit={this.sendImageMessage}
/> />

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import { errorActionsHide } from '../actions/messages'; import { errorActionsHide as errorActionsHideAction } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import protectedFunction from '../lib/methods/helpers/protectedFunction'; import protectedFunction from '../lib/methods/helpers/protectedFunction';
@ -14,7 +14,7 @@ import I18n from '../i18n';
actionMessage: state.messages.actionMessage actionMessage: state.messages.actionMessage
}), }),
dispatch => ({ dispatch => ({
errorActionsHide: () => dispatch(errorActionsHide()) errorActionsHide: () => dispatch(errorActionsHideAction())
}) })
) )
export default class MessageErrorActions extends React.Component { export default class MessageErrorActions extends React.Component {
@ -23,6 +23,7 @@ export default class MessageErrorActions extends React.Component {
actionMessage: PropTypes.object actionMessage: PropTypes.object
}; };
// eslint-disable-next-line react/sort-comp
constructor(props) { constructor(props) {
super(props); super(props);
this.handleActionPress = this.handleActionPress.bind(this); this.handleActionPress = this.handleActionPress.bind(this);
@ -35,16 +36,21 @@ export default class MessageErrorActions extends React.Component {
} }
} }
handleResend = protectedFunction(() => RocketChat.resendMessage(this.props.actionMessage._id)); handleResend = protectedFunction(() => {
const { actionMessage } = this.props;
RocketChat.resendMessage(actionMessage._id);
});
handleDelete = protectedFunction(() => { handleDelete = protectedFunction(() => {
const { actionMessage } = this.props;
database.write(() => { database.write(() => {
const msg = database.objects('messages').filtered('_id = $0', this.props.actionMessage._id); const msg = database.objects('messages').filtered('_id = $0', actionMessage._id);
database.delete(msg); database.delete(msg);
}); });
}) })
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { errorActionsHide } = this.props;
switch (actionIndex) { switch (actionIndex) {
case this.RESEND_INDEX: case this.RESEND_INDEX:
this.handleResend(); this.handleResend();
@ -55,7 +61,7 @@ export default class MessageErrorActions extends React.Component {
default: default:
break; break;
} }
this.props.errorActionsHide(); errorActionsHide();
} }
render() { render() {

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet, Image, TextInput, Platform } from 'react-native'; import {
View, StyleSheet, Image, TextInput, Platform
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import I18n from '../i18n'; import I18n from '../i18n';

View File

@ -1,13 +1,15 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, SafeAreaView } from 'react-native'; import {
ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { appStart } from '../actions'; import { appStart as appStartAction } from '../actions';
import { logout } from '../actions/login'; import { logout as logoutAction } from '../actions/login';
import Avatar from '../containers/Avatar'; import Avatar from './Avatar';
import Status from '../containers/status'; import Status from './status';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import { STATUS_COLORS } from '../constants/colors'; import { STATUS_COLORS } from '../constants/colors';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -87,8 +89,8 @@ const keyExtractor = item => item.id;
}, },
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({ }), dispatch => ({
logout: () => dispatch(logout()), logout: () => dispatch(logoutAction()),
appStart: () => dispatch(appStart('outside')) appStart: () => dispatch(appStartAction('outside'))
})) }))
export default class Sidebar extends Component { export default class Sidebar extends Component {
static propTypes = { static propTypes = {
@ -112,7 +114,8 @@ export default class Sidebar extends Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.user && this.props.user && this.props.user.language !== nextProps.user.language) { const { user } = this.props;
if (nextProps.user && user && user.language !== nextProps.user.language) {
this.setStatus(); this.setStatus();
} }
} }
@ -138,7 +141,8 @@ export default class Sidebar extends Component {
} }
closeDrawer = () => { closeDrawer = () => {
this.props.navigator.toggleDrawer({ const { navigator } = this.props;
navigator.toggleDrawer({
side: 'left', side: 'left',
animated: true, animated: true,
to: 'close' to: 'close'
@ -147,7 +151,7 @@ export default class Sidebar extends Component {
toggleStatus = () => { toggleStatus = () => {
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
this.setState({ showStatus: !this.state.showStatus }); this.setState(prevState => ({ showStatus: !prevState.showStatus }));
} }
sidebarNavigate = (screen, title) => { sidebarNavigate = (screen, title) => {
@ -178,67 +182,79 @@ export default class Sidebar extends Component {
</Touch> </Touch>
) )
renderStatusItem = ({ item }) => ( renderStatusItem = ({ item }) => {
this.renderItem({ const { user } = this.props;
text: item.name, return (
left: <View style={[styles.status, { backgroundColor: STATUS_COLORS[item.id] }]} />, this.renderItem({
selected: this.props.user.status === item.id, text: item.name,
onPress: () => { left: <View style={[styles.status, { backgroundColor: STATUS_COLORS[item.id] }]} />,
this.closeDrawer(); selected: user.status === item.id,
this.toggleStatus(); onPress: () => {
if (this.props.user.status !== item.id) { this.closeDrawer();
try { this.toggleStatus();
RocketChat.setUserPresenceDefaultStatus(item.id); if (user.status !== item.id) {
} catch (e) { try {
log('setUserPresenceDefaultStatus', e); RocketChat.setUserPresenceDefaultStatus(item.id);
} catch (e) {
log('setUserPresenceDefaultStatus', e);
}
} }
} }
}
})
)
renderNavigation = () => (
[
this.renderItem({
text: I18n.t('Chats'),
left: <Icon name='chat-bubble' size={20} />,
onPress: () => this.sidebarNavigate('RoomsListView', I18n.t('Messages')),
testID: 'sidebar-chats'
}),
this.renderItem({
text: I18n.t('Profile'),
left: <Icon name='person' size={20} />,
onPress: () => this.sidebarNavigate('ProfileView', I18n.t('Profile')),
testID: 'sidebar-profile'
}),
this.renderItem({
text: I18n.t('Settings'),
left: <Icon name='settings' size={20} />,
onPress: () => this.sidebarNavigate('SettingsView', I18n.t('Settings')),
testID: 'sidebar-settings'
}),
this.renderSeparator('separator-logout'),
this.renderItem({
text: I18n.t('Logout'),
left: <Icon name='exit-to-app' size={20} />,
onPress: () => this.props.logout(),
testID: 'sidebar-logout'
}) })
] );
) }
renderStatus = () => ( renderNavigation = () => {
<FlatList const { logout } = this.props;
key='status-list' return (
data={this.state.status} [
extraData={this.props.user} this.renderItem({
renderItem={this.renderStatusItem} text: I18n.t('Chats'),
keyExtractor={keyExtractor} left: <Icon name='chat-bubble' size={20} />,
/> onPress: () => this.sidebarNavigate('RoomsListView', I18n.t('Messages')),
) testID: 'sidebar-chats'
}),
this.renderItem({
text: I18n.t('Profile'),
left: <Icon name='person' size={20} />,
onPress: () => this.sidebarNavigate('ProfileView', I18n.t('Profile')),
testID: 'sidebar-profile'
}),
this.renderItem({
text: I18n.t('Settings'),
left: <Icon name='settings' size={20} />,
onPress: () => this.sidebarNavigate('SettingsView', I18n.t('Settings')),
testID: 'sidebar-settings'
}),
this.renderSeparator('separator-logout'),
this.renderItem({
text: I18n.t('Logout'),
left: <Icon name='exit-to-app' size={20} />,
onPress: () => logout(),
testID: 'sidebar-logout'
})
]
);
}
renderStatus = () => {
const { status } = this.state;
const { user } = this.props;
return (
<FlatList
key='status-list'
data={status}
extraData={user}
renderItem={this.renderStatusItem}
keyExtractor={keyExtractor}
/>
);
}
render() { render() {
const { showStatus } = this.state;
const { user, server, baseUrl } = this.props; const { user, server, baseUrl } = this.props;
if (!user) { if (!user) {
return null; return null;
} }
@ -266,7 +282,7 @@ export default class Sidebar extends Component {
<Text style={styles.currentServerText} numberOfLines={1}>{server}</Text> <Text style={styles.currentServerText} numberOfLines={1}>{server}</Text>
</View> </View>
<Icon <Icon
name={this.state.showStatus ? 'keyboard-arrow-up' : 'keyboard-arrow-down'} name={showStatus ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
size={30} size={30}
style={{ paddingHorizontal: 10 }} style={{ paddingHorizontal: 10 }}
/> />
@ -275,8 +291,8 @@ export default class Sidebar extends Component {
{this.renderSeparator('separator-header')} {this.renderSeparator('separator-header')}
{!this.state.showStatus ? this.renderNavigation() : null} {!showStatus ? this.renderNavigation() : null}
{this.state.showStatus ? this.renderStatus() : null} {showStatus ? this.renderStatus() : null}
</ScrollView> </ScrollView>
<Text style={styles.version}> <Text style={styles.version}>
{DeviceInfo.getReadableVersion()} {DeviceInfo.getReadableVersion()}

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet, Text, TextInput, ViewPropTypes, Platform } from 'react-native'; import {
View, StyleSheet, Text, TextInput, ViewPropTypes, Platform
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
@ -68,9 +70,11 @@ export default class RCTextInput extends React.PureComponent {
iconLeft: PropTypes.string, iconLeft: PropTypes.string,
placeholder: PropTypes.string placeholder: PropTypes.string
} }
static defaultProps = { static defaultProps = {
error: {} error: {}
} }
state = { state = {
showPassword: false showPassword: false
} }
@ -82,21 +86,30 @@ export default class RCTextInput extends React.PureComponent {
testID testID
}) => <Icon name={name} style={[styles.icon, style]} size={20} onPress={onPress} testID={testID} /> }) => <Icon name={name} style={[styles.icon, style]} size={20} onPress={onPress} testID={testID} />
iconLeft = name => this.icon({ iconLeft = (name) => {
name, const { testID } = this.props;
onPress: null, return this.icon({
style: { left: 0 }, name,
testID: this.props.testID ? `${ this.props.testID }-icon-left` : null onPress: null,
}); style: { left: 0 },
testID: testID ? `${ testID }-icon-left` : null
});
}
iconPassword = name => this.icon({ iconPassword = (name) => {
name, const { testID } = this.props;
onPress: () => this.tooglePassword(), return this.icon({
style: { right: 0 }, name,
testID: this.props.testID ? `${ this.props.testID }-icon-right` : null onPress: () => this.tooglePassword(),
}); style: { right: 0 },
testID: testID ? `${ testID }-icon-right` : null
});
}
tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }); tooglePassword = () => {
const { showPassword } = this.state;
this.setState({ showPassword: !showPassword });
}
render() { render() {
const { const {

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, StyleSheet, Text, Keyboard, LayoutAnimation } from 'react-native'; import {
View, StyleSheet, Text, Keyboard, LayoutAnimation
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import I18n from '../i18n'; import I18n from '../i18n';
@ -22,18 +24,24 @@ const styles = StyleSheet.create({
})) }))
export default class Typing extends React.Component { export default class Typing extends React.Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return this.props.usersTyping.join() !== nextProps.usersTyping.join(); const { usersTyping } = this.props;
return usersTyping.join() !== nextProps.usersTyping.join();
} }
componentWillUpdate() { componentWillUpdate() {
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
} }
onPress = () => { onPress = () => {
Keyboard.dismiss(); Keyboard.dismiss();
} }
get usersTyping() { get usersTyping() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username); const { usersTyping, username } = this.props;
const users = usersTyping.filter(_username => username !== _username);
return users.length ? `${ users.join(', ') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : ''; return users.length ? `${ users.join(', ') } ${ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }` : '';
} }
render() { render() {
const { usersTyping } = this; const { usersTyping } = this;

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, StyleSheet, Text, Easing, Image } from 'react-native'; import {
View, StyleSheet, Text, Easing, Image
} from 'react-native';
import Video from 'react-native-video'; import Video from 'react-native-video';
import Slider from 'react-native-slider'; import Slider from 'react-native-slider';
import moment from 'moment'; import moment from 'moment';
@ -72,7 +74,8 @@ export default class Audio extends React.PureComponent {
} }
onProgress(data) { onProgress(data) {
if (data.currentTime <= this.state.duration) { const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime }); this.setState({ currentTime: data.currentTime });
} }
} }
@ -85,15 +88,19 @@ export default class Audio extends React.PureComponent {
} }
getDuration() { getDuration() {
return formatTime(this.state.duration); const { duration } = this.state;
return formatTime(duration);
} }
togglePlayPause() { togglePlayPause() {
this.setState({ paused: !this.state.paused }); const { paused } = this.state;
this.setState({ paused: !paused });
} }
render() { render() {
const { uri, paused } = this.state; const {
uri, paused, currentTime, duration
} = this.state;
const { const {
user, baseUrl, customEmojis, file user, baseUrl, customEmojis, file
} = this.props; } = this.props;
@ -122,15 +129,15 @@ export default class Audio extends React.PureComponent {
onPress={() => this.togglePlayPause()} onPress={() => this.togglePlayPause()}
> >
{ {
paused ? paused
<Image source={{ uri: 'play' }} style={styles.playPauseImage} /> : ? <Image source={{ uri: 'play' }} style={styles.playPauseImage} />
<Image source={{ uri: 'pause' }} style={styles.playPauseImage} /> : <Image source={{ uri: 'pause' }} style={styles.playPauseImage} />
} }
</BorderlessButton> </BorderlessButton>
<Slider <Slider
style={styles.slider} style={styles.slider}
value={this.state.currentTime} value={currentTime}
maximumValue={this.state.duration} maximumValue={duration}
minimumValue={0} minimumValue={0}
animateTransitions animateTransitions
animationConfig={{ animationConfig={{

View File

@ -14,7 +14,8 @@ export default class Emoji extends React.PureComponent {
PropTypes.array, PropTypes.array,
PropTypes.object PropTypes.object
]) ])
}; }
render() { render() {
const { const {
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl

View File

@ -40,7 +40,7 @@ export default class extends React.PureComponent {
} }
render() { render() {
const { isPressed } = this.state; const { modalVisible, isPressed } = this.state;
const { baseUrl, file, user } = this.props; const { baseUrl, file, user } = this.props;
const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
@ -69,7 +69,7 @@ export default class extends React.PureComponent {
title={file.title} title={file.title}
description={file.description} description={file.description}
image={img} image={img}
isVisible={this.state.modalVisible} isVisible={modalVisible}
onClose={() => this.setState({ modalVisible: false })} onClose={() => this.setState({ modalVisible: false })}
/> />
] ]

View File

@ -9,16 +9,17 @@ import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin'; import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
// Support <http://link|Text> // Support <http://link|Text>
const formatText = text => const formatText = text => text.replace(
text.replace( new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'), (match, url, title) => `[${ title }](${ url })`
(match, url, title) => `[${ title }](${ url })` );
);
export default class Markdown extends React.Component { export default class Markdown extends React.Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return nextProps.msg !== this.props.msg; const { msg } = this.props;
return nextProps.msg !== msg;
} }
render() { render() {
const { const {
msg, customEmojis, style, rules, baseUrl, username, edited msg, customEmojis, style, rules, baseUrl, username, edited
@ -33,8 +34,10 @@ export default class Markdown extends React.Component {
<MarkdownRenderer <MarkdownRenderer
rules={{ rules={{
paragraph: (node, children) => ( paragraph: (node, children) => (
// eslint-disable-next-line
<Text key={node.key} style={styles.paragraph}> <Text key={node.key} style={styles.paragraph}>
{children}{edited ? <Text style={styles.edited}> (edited)</Text> : null} {children}
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
</Text> </Text>
), ),
mention: (node) => { mention: (node) => {

View File

@ -1,6 +1,8 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, ViewPropTypes, Image as ImageRN } from 'react-native'; import {
View, Text, ViewPropTypes, Image as ImageRN
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment'; import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
@ -137,22 +139,29 @@ export default class Message extends PureComponent {
KeyboardUtils.dismiss(); KeyboardUtils.dismiss();
} }
isInfoMessage() { isInfoMessage = () => {
return SYSTEM_MESSAGES.includes(this.props.type); const { type } = this.props;
return SYSTEM_MESSAGES.includes(type);
} }
isOwn = () => this.props.author._id === this.props.user.id; isOwn = () => {
const { author, user } = this.props;
return author._id === user.id;
}
isDeleted() { isDeleted() {
return this.props.type === 'rm'; const { type } = this.props;
return type === 'rm';
} }
isTemp() { isTemp() {
return this.props.status === messagesStatus.TEMP || this.props.status === messagesStatus.ERROR; const { status } = this.props;
return status === messagesStatus.TEMP || status === messagesStatus.ERROR;
} }
hasError() { hasError() {
return this.props.status === messagesStatus.ERROR; const { status } = this.props;
return status === messagesStatus.ERROR;
} }
renderAvatar = () => { renderAvatar = () => {
@ -242,19 +251,23 @@ export default class Message extends PureComponent {
if (!this.hasError()) { if (!this.hasError()) {
return null; return null;
} }
return <Icon name='error-outline' color='red' size={20} style={styles.errorIcon} onPress={this.props.onErrorPress} />; const { onErrorPress } = this.props;
return <Icon name='error-outline' color='red' size={20} style={styles.errorIcon} onPress={onErrorPress} />;
} }
renderReaction = (reaction) => { renderReaction = (reaction) => {
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1; const {
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
} = this.props;
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
const underlayColor = reacted ? '#fff' : '#e1e5e8'; const underlayColor = reacted ? '#fff' : '#e1e5e8';
return ( return (
<LongPressGestureHandler <LongPressGestureHandler
key={reaction.emoji} key={reaction.emoji}
onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && this.props.onReactionLongPress()} onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()}
> >
<RectButton <RectButton
onPress={() => this.props.onReactionPress(reaction.emoji)} onPress={() => onReactionPress(reaction.emoji)}
testID={`message-reaction-${ reaction.emoji }`} testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]} style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]}
activeOpacity={0.8} activeOpacity={0.8}
@ -263,10 +276,10 @@ export default class Message extends PureComponent {
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}> <View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
<Emoji <Emoji
content={reaction.emoji} content={reaction.emoji}
customEmojis={this.props.customEmojis} customEmojis={customEmojis}
standardEmojiStyle={styles.reactionEmoji} standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji} customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/> />
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text> <Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View> </View>
@ -276,7 +289,7 @@ export default class Message extends PureComponent {
} }
renderReactions() { renderReactions() {
const { reactions } = this.props; const { reactions, toggleReactionPicker } = this.props;
if (reactions.length === 0) { if (reactions.length === 0) {
return null; return null;
} }
@ -284,7 +297,7 @@ export default class Message extends PureComponent {
<View style={styles.reactionsContainer}> <View style={styles.reactionsContainer}>
{reactions.map(this.renderReaction)} {reactions.map(this.renderReaction)}
<RectButton <RectButton
onPress={this.props.toggleReactionPicker} onPress={toggleReactionPicker}
key='message-add-reaction' key='message-add-reaction'
testID='message-add-reaction' testID='message-add-reaction'
style={styles.reactionButton} style={styles.reactionButton}
@ -300,10 +313,11 @@ export default class Message extends PureComponent {
} }
renderBroadcastReply() { renderBroadcastReply() {
if (this.props.broadcast && !this.isOwn()) { const { broadcast, replyBroadcast } = this.props;
if (broadcast && !this.isOwn()) {
return ( return (
<RectButton <RectButton
onPress={this.props.replyBroadcast} onPress={replyBroadcast}
style={styles.broadcastButton} style={styles.broadcastButton}
activeOpacity={0.5} activeOpacity={0.5}
underlayColor='#fff' underlayColor='#fff'
@ -349,15 +363,17 @@ export default class Message extends PureComponent {
{this.renderBroadcastReply()} {this.renderBroadcastReply()}
</View> </View>
</View> </View>
{reactionsModal ? {reactionsModal
<ReactionsModal ? (
isVisible={reactionsModal} <ReactionsModal
reactions={reactions} isVisible={reactionsModal}
user={user} reactions={reactions}
customEmojis={customEmojis} user={user}
baseUrl={baseUrl} customEmojis={customEmojis}
close={closeReactions} baseUrl={baseUrl}
/> close={closeReactions}
/>
)
: null : null
} }
</View> </View>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet } from 'react-native'; import {
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet
} from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
@ -45,6 +47,7 @@ export default class PhotoModal extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
window: PropTypes.object window: PropTypes.object
} }
render() { render() {
const { const {
image, isVisible, onClose, title, description, window: { width, height } image, isVisible, onClose, title, description, window: { width, height }

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'react-native'; import {
View, Text, TouchableWithoutFeedback, FlatList, StyleSheet
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
@ -66,10 +68,12 @@ export default class ReactionsModal extends React.PureComponent {
PropTypes.object PropTypes.object
]) ])
} }
renderItem = (item) => { renderItem = (item) => {
const { user, customEmojis, baseUrl } = this.props;
const count = item.usernames.length; const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3) let usernames = item.usernames.slice(0, 3)
.map(username => (username.value === this.props.user.username ? I18n.t('you') : username.value)).join(', '); .map(username => (username.value === user.username ? I18n.t('you') : username.value)).join(', ');
if (count > 3) { if (count > 3) {
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`; usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
} else { } else {
@ -82,8 +86,8 @@ export default class ReactionsModal extends React.PureComponent {
content={item.emoji} content={item.emoji}
standardEmojiStyle={standardEmojiStyle} standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle} customEmojiStyle={customEmojiStyle}
customEmojis={this.props.customEmojis} customEmojis={customEmojis}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/> />
</View> </View>
<View style={styles.peopleItemContainer}> <View style={styles.peopleItemContainer}>

View File

@ -41,7 +41,7 @@ export default class User extends React.PureComponent {
render() { render() {
const { const {
username, alias, ts, temp username, alias, ts, temp, timeFormat
} = this.props; } = this.props;
const extraStyle = {}; const extraStyle = {};
@ -50,7 +50,7 @@ export default class User extends React.PureComponent {
} }
const aliasUsername = alias ? (<Text style={styles.alias}>@{username}</Text>) : null; const aliasUsername = alias ? (<Text style={styles.alias}>@{username}</Text>) : null;
const time = moment(ts).format(this.props.timeFormat); const time = moment(ts).format(timeFormat);
return ( return (
<View style={styles.usernameView}> <View style={styles.usernameView}>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet, Image, Platform, View } from 'react-native'; import {
StyleSheet, Image, Platform, View
} from 'react-native';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls'; import VideoPlayer from 'react-native-video-controls';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
@ -42,19 +44,20 @@ export default class Video extends React.PureComponent {
state = { isVisible: false }; state = { isVisible: false };
get uri() { get uri() {
const { video_url } = this.props.file; const { baseUrl, user, file } = this.props;
const { baseUrl, user } = this.props; const { video_url } = file;
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
} }
toggleModal() { toggleModal() {
this.setState({ this.setState(prevState => ({
isVisible: !this.state.isVisible isVisible: !prevState.isVisible
}); }));
} }
open() { open() {
if (isTypeSupported(this.props.file.video_type)) { const { file } = this.props;
if (isTypeSupported(file.video_type)) {
return this.toggleModal(); return this.toggleModal();
} }
openLink(this.uri); openLink(this.uri);
@ -62,8 +65,10 @@ export default class Video extends React.PureComponent {
render() { render() {
const { isVisible } = this.state; const { isVisible } = this.state;
const { description } = this.props.file; const {
const { baseUrl, user, customEmojis } = this.props; baseUrl, user, customEmojis, file
} = this.props;
const { description } = file;
if (!baseUrl) { if (!baseUrl) {
return null; return null;

View File

@ -5,7 +5,11 @@ import { connect } from 'react-redux';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Message from './Message'; import Message from './Message';
import { errorActionsShow, toggleReactionPicker, replyBroadcast } from '../../actions/messages'; import {
errorActionsShow as errorActionsShowAction,
toggleReactionPicker as toggleReactionPickerAction,
replyBroadcast as replyBroadcastAction
} from '../../actions/messages';
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -16,9 +20,9 @@ import { errorActionsShow, toggleReactionPicker, replyBroadcast } from '../../ac
message: state.messages.message, message: state.messages.message,
useRealName: state.settings.UI_Use_Real_Name useRealName: state.settings.UI_Use_Real_Name
}), dispatch => ({ }), dispatch => ({
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)), errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
replyBroadcast: message => dispatch(replyBroadcast(message)), replyBroadcast: message => dispatch(replyBroadcastAction(message)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
})) }))
export default class MessageContainer extends React.Component { export default class MessageContainer extends React.Component {
static propTypes = { static propTypes = {
@ -67,39 +71,47 @@ export default class MessageContainer extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
if (this.state.reactionsModal !== nextState.reactionsModal) { const { reactionsModal } = this.state;
const {
status, reactions, broadcast, editing, _updatedAt
} = this.props;
if (reactionsModal !== nextState.reactionsModal) {
return true; return true;
} }
if (this.props.status !== nextProps.status) { if (status !== nextProps.status) {
return true; return true;
} }
// eslint-disable-next-line // eslint-disable-next-line
if (!!this.props._updatedAt ^ !!nextProps._updatedAt) { if (!!_updatedAt ^ !!nextProps._updatedAt) {
return true; return true;
} }
if (!equal(this.props.reactions, nextProps.reactions)) { if (!equal(reactions, nextProps.reactions)) {
return true; return true;
} }
if (this.props.broadcast !== nextProps.broadcast) { if (broadcast !== nextProps.broadcast) {
return true; return true;
} }
if (this.props.editing !== nextProps.editing) { if (editing !== nextProps.editing) {
return true; return true;
} }
return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString(); return _updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
} }
onLongPress = () => { onLongPress = () => {
this.props.onLongPress(this.parseMessage()); const { onLongPress } = this.props;
onLongPress(this.parseMessage());
} }
onErrorPress = () => { onErrorPress = () => {
this.props.errorActionsShow(this.parseMessage()); const { errorActionsShow } = this.props;
errorActionsShow(this.parseMessage());
} }
onReactionPress = (emoji) => { onReactionPress = (emoji) => {
this.props.onReactionPress(emoji, this.props.item._id); const { onReactionPress, item } = this.props;
onReactionPress(emoji, item._id);
} }
@ -118,30 +130,38 @@ export default class MessageContainer extends React.Component {
} }
isHeader = () => { isHeader = () => {
const { item, previousItem } = this.props; const {
item, previousItem, broadcast, Message_GroupingPeriod
} = this.props;
if (previousItem && ( if (previousItem && (
(previousItem.ts.toDateString() === item.ts.toDateString()) && (previousItem.ts.toDateString() === item.ts.toDateString())
(previousItem.u.username === item.u.username) && && (previousItem.u.username === item.u.username)
!(previousItem.groupable === false || item.groupable === false || this.props.broadcast === true) && && !(previousItem.groupable === false || item.groupable === false || broadcast === true)
(previousItem.status === item.status) && && (previousItem.status === item.status)
(item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000) && (item.ts - previousItem.ts < Message_GroupingPeriod * 1000)
)) { )) {
return false; return false;
} }
return true; return true;
} }
parseMessage = () => JSON.parse(JSON.stringify(this.props.item)); parseMessage = () => {
const { item } = this.props;
return JSON.parse(JSON.stringify(item));
}
toggleReactionPicker = () => { toggleReactionPicker = () => {
this.props.toggleReactionPicker(this.parseMessage()); const { toggleReactionPicker } = this.props;
toggleReactionPicker(this.parseMessage());
} }
replyBroadcast = () => { replyBroadcast = () => {
this.props.replyBroadcast(this.parseMessage()); const { replyBroadcast } = this.props;
replyBroadcast(this.parseMessage());
} }
render() { render() {
const { reactionsModal } = this.state;
const { const {
item, message, editing, user, style, archived, baseUrl, customEmojis, useRealName, broadcast item, message, editing, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
} = this.props; } = this.props;
@ -171,7 +191,7 @@ export default class MessageContainer extends React.Component {
broadcast={broadcast} broadcast={broadcast}
baseUrl={baseUrl} baseUrl={baseUrl}
customEmojis={customEmojis} customEmojis={customEmojis}
reactionsModal={this.state.reactionsModal} reactionsModal={reactionsModal}
useRealName={useRealName} useRealName={useRealName}
role={role} role={role}
closeReactions={this.closeReactions} closeReactions={this.closeReactions}

View File

@ -42,6 +42,7 @@ export default class Status extends React.PureComponent {
} }
render() { render() {
return (<View style={[styles.status, this.props.style, { backgroundColor: STATUS_COLORS[this.status] }]} />); const { style } = this.props;
return (<View style={[styles.status, style, { backgroundColor: STATUS_COLORS[this.status] }]} />);
} }
} }

View File

@ -24,6 +24,7 @@ class EventEmitter {
constructor() { constructor() {
this.events = {}; this.events = {};
} }
on(event, listener) { on(event, listener) {
if (typeof this.events[event] !== 'object') { if (typeof this.events[event] !== 'object') {
this.events[event] = []; this.events[event] = [];
@ -31,6 +32,7 @@ class EventEmitter {
this.events[event].push(listener); this.events[event].push(listener);
return listener; return listener;
} }
removeListener(event, listener) { removeListener(event, listener) {
if (typeof this.events[event] === 'object') { if (typeof this.events[event] === 'object') {
const idx = this.events[event].indexOf(listener); const idx = this.events[event].indexOf(listener);
@ -42,6 +44,7 @@ class EventEmitter {
} }
} }
} }
emit(event, ...args) { emit(event, ...args) {
if (typeof this.events[event] === 'object') { if (typeof this.events[event] === 'object') {
this.events[event].forEach((listener) => { this.events[event].forEach((listener) => {
@ -53,6 +56,7 @@ class EventEmitter {
}); });
} }
} }
once(event, listener) { once(event, listener) {
return this.on(event, function g(...args) { return this.on(event, function g(...args) {
this.removeListener(event, g); this.removeListener(event, g);
@ -135,6 +139,7 @@ export default class Socket extends EventEmitter {
this._connect().catch(e => log('ddp.constructor._connect', e)); this._connect().catch(e => log('ddp.constructor._connect', e));
} }
check() { check() {
if (!this.lastping) { if (!this.lastping) {
return false; return false;
@ -144,6 +149,7 @@ export default class Socket extends EventEmitter {
} }
return true; return true;
} }
async login(params) { async login(params) {
try { try {
this.emit('login', params); this.emit('login', params);
@ -165,6 +171,7 @@ export default class Socket extends EventEmitter {
return Promise.reject(error); return Promise.reject(error);
} }
} }
async send(obj, ignore) { async send(obj, ignore) {
console.log('send', obj); console.log('send', obj);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -187,9 +194,11 @@ export default class Socket extends EventEmitter {
}); });
}); });
} }
get status() { get status() {
return this.connection && this.connection.readyState === 1 && this.check() && !!this._logged; return this.connection && this.connection.readyState === 1 && this.check() && !!this._logged;
} }
_close() { _close() {
try { try {
// this.connection && this.connection.readyState > 1 && this.connection.close && this.connection.close(300, 'disconnect'); // this.connection && this.connection.readyState > 1 && this.connection.close && this.connection.close(300, 'disconnect');
@ -201,6 +210,7 @@ export default class Socket extends EventEmitter {
// console.log(e); // console.log(e);
} }
} }
_connect() { _connect() {
return new Promise((resolve) => { return new Promise((resolve) => {
this.lastping = new Date(); this.lastping = new Date();
@ -240,12 +250,14 @@ export default class Socket extends EventEmitter {
}; };
}); });
} }
logout() { logout() {
this._login = null; this._login = null;
return this.call('logout') return this.call('logout')
.catch(e => log('logout', e)) .catch(e => log('logout', e))
.finally(() => this.subscriptions = {}); .finally(() => this.subscriptions = {});
} }
disconnect() { disconnect() {
this._logged = false; this._logged = false;
this._login = null; this._login = null;
@ -256,6 +268,7 @@ export default class Socket extends EventEmitter {
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
} }
async reconnect() { async reconnect() {
if (this._timer || this.forceDisconnect) { if (this._timer || this.forceDisconnect) {
return; return;
@ -272,6 +285,7 @@ export default class Socket extends EventEmitter {
} }
}, 1000); }, 1000);
} }
call(method, ...params) { call(method, ...params) {
return this.send({ return this.send({
msg: 'method', method, params msg: 'method', method, params
@ -283,6 +297,7 @@ export default class Socket extends EventEmitter {
return Promise.reject(err); return Promise.reject(err);
}); });
} }
unsubscribe(id) { unsubscribe(id) {
if (!this.subscriptions[id]) { if (!this.subscriptions[id]) {
return Promise.reject(id); return Promise.reject(id);
@ -296,6 +311,7 @@ export default class Socket extends EventEmitter {
return Promise.reject(err); return Promise.reject(err);
}); });
} }
subscribe(name, ...params) { subscribe(name, ...params) {
console.log(name, params); console.log(name, params);
return this.send({ return this.send({

View File

@ -21,9 +21,11 @@ export default async function() {
return permission; return permission;
}); });
InteractionManager.runAfterInteractions(() => InteractionManager.runAfterInteractions(
database.write(() => () => database.write(
permissions.forEach(permission => database.create('permissions', permission, true)))); () => permissions.forEach(permission => database.create('permissions', permission, true))
)
);
} catch (e) { } catch (e) {
log('getPermissions', e); log('getPermissions', e);
} }

View File

@ -27,15 +27,17 @@ export default async function() {
const filteredSettings = this._prepareSettings(this._filterSettings(data)); const filteredSettings = this._prepareSettings(this._filterSettings(data));
InteractionManager.runAfterInteractions(() => InteractionManager.runAfterInteractions(
database.write(() => () => database.write(
filteredSettings.forEach((setting) => { () => filteredSettings.forEach((setting) => {
database.create('settings', { ...setting, _updatedAt: new Date() }, true); database.create('settings', { ...setting, _updatedAt: new Date() }, true);
if (setting._id === 'Site_Name') { if (setting._id === 'Site_Name') {
updateServer.call(this, { name: setting.valueAsString }); updateServer.call(this, { name: setting.valueAsString });
} }
}))); })
)
);
reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings))); reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings)));
const iconSetting = data.find(item => item._id === 'Assets_favicon_512'); const iconSetting = data.find(item => item._id === 'Assets_favicon_512');

View File

@ -20,7 +20,7 @@ export default async function readMessages(rid) {
const { database: db } = database; const { database: db } = database;
try { try {
// eslint-disable-next-line // eslint-disable-next-line
const data = await (this.ddp.status ? readMessagesDDP.call(this, rid) : readMessagesREST.call(this, rid)); const data = await (this.ddp.status && false ? readMessagesDDP.call(this, rid) : readMessagesREST.call(this, rid));
const [subscription] = db.objects('subscriptions').filtered('rid = $0', rid); const [subscription] = db.objects('subscriptions').filtered('rid = $0', rid);
db.write(() => { db.write(() => {
subscription.open = true; subscription.open = true;

View File

@ -1,7 +1,7 @@
import Random from 'react-native-meteor/lib/Random'; import Random from 'react-native-meteor/lib/Random';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import buildMessage from '../methods/helpers/buildMessage'; import buildMessage from './helpers/buildMessage';
import { post } from './helpers/rest'; import { post } from './helpers/rest';
import database from '../realm'; import database from '../realm';
import reduxStore from '../createStore'; import reduxStore from '../createStore';
@ -46,7 +46,7 @@ function sendMessageByDDP(message) {
export async function _sendMessageCall(message) { export async function _sendMessageCall(message) {
try { try {
// eslint-disable-next-line // eslint-disable-next-line
const data = await (this.ddp.status ? sendMessageByDDP.call(this, message) : sendMessageByRest.call(this, message)); const data = await (this.ddp.status && false ? sendMessageByDDP.call(this, message) : sendMessageByRest.call(this, message));
return data; return data;
} catch (e) { } catch (e) {
database.write(() => { database.write(() => {

View File

@ -9,10 +9,9 @@ const subscribe = (ddp, rid) => Promise.all([
ddp.subscribe('stream-room-messages', rid, false), ddp.subscribe('stream-room-messages', rid, false),
ddp.subscribe('stream-notify-room', `${ rid }/typing`, false) ddp.subscribe('stream-notify-room', `${ rid }/typing`, false)
]); ]);
const unsubscribe = subscriptions => const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch((e) => {
subscriptions.forEach(sub => sub.unsubscribe().catch((e) => { log('unsubscribeRoom', e);
log('unsubscribeRoom', e); }));
}));
let timer = null; let timer = null;
let promises; let promises;

View File

@ -309,22 +309,28 @@ class DB {
], ],
deleteRealmIfMigrationNeeded: true deleteRealmIfMigrationNeeded: true
}) })
}; }
deleteAll(...args) { deleteAll(...args) {
return this.database.write(() => this.database.deleteAll(...args)); return this.database.write(() => this.database.deleteAll(...args));
} }
delete(...args) { delete(...args) {
return this.database.delete(...args); return this.database.delete(...args);
} }
write(...args) { write(...args) {
return this.database.write(...args); return this.database.write(...args);
} }
create(...args) { create(...args) {
return this.database.create(...args); return this.database.create(...args);
} }
objects(...args) { objects(...args) {
return this.database.objects(...args); return this.database.objects(...args);
} }
get database() { get database() {
return this.databases.activeDB; return this.databases.activeDB;
} }

View File

@ -10,7 +10,9 @@ import database from './realm';
import log from '../utils/log'; import log from '../utils/log';
// import * as actions from '../actions'; // import * as actions from '../actions';
import { setUser, setLoginServices, removeLoginServices, loginRequest, loginSuccess, loginFailure, logout } from '../actions/login'; import {
setUser, setLoginServices, removeLoginServices, loginRequest, loginSuccess, loginFailure, logout
} from '../actions/login';
import { disconnect, connectSuccess, connectFailure } from '../actions/connect'; import { disconnect, connectSuccess, connectFailure } from '../actions/connect';
import { setActiveUser } from '../actions/activeUsers'; import { setActiveUser } from '../actions/activeUsers';
import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages'; import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages';
@ -598,8 +600,8 @@ const RocketChat = {
getPermissions, getPermissions,
getCustomEmoji, getCustomEmoji,
parseSettings: settings => settings.reduce((ret, item) => { parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[defaultSettings[item._id].type] || item.valueAsString || item.valueAsNumber || ret[item._id] = item[defaultSettings[item._id].type] || item.valueAsString || item.valueAsNumber
item.valueAsBoolean || item.value; || item.valueAsBoolean || item.value;
return ret; return ret;
}, {}), }, {}),
_prepareSettings(settings) { _prepareSettings(settings) {

View File

@ -17,17 +17,21 @@ export default class KeyboardView extends React.PureComponent {
} }
render() { render() {
const {
style, contentContainerStyle, scrollEnabled, keyboardVerticalOffset, children
} = this.props;
return ( return (
<KeyboardAwareScrollView <KeyboardAwareScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
style={this.props.style} style={style}
contentContainerStyle={this.props.contentContainerStyle} contentContainerStyle={contentContainerStyle}
scrollEnabled={this.props.scrollEnabled} scrollEnabled={scrollEnabled}
alwaysBounceVertical={false} alwaysBounceVertical={false}
extraHeight={this.props.keyboardVerticalOffset} extraHeight={keyboardVerticalOffset}
behavior='position' behavior='position'
> >
{this.props.children} {children}
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
); );
} }

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, StyleSheet, Image, Platform } from 'react-native'; import {
View, Text, StyleSheet, Image, Platform
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
@ -151,18 +153,22 @@ export default class RoomItem extends React.Component {
showLastMessage: true, showLastMessage: true,
avatarSize: 48 avatarSize: 48
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const oldlastMessage = this.props.lastMessage; const { lastMessage, _updatedAt } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage; const newLastmessage = nextProps.lastMessage;
if (oldlastMessage && newLastmessage && oldlastMessage.ts.toGMTString() !== newLastmessage.ts.toGMTString()) { if (oldlastMessage && newLastmessage && oldlastMessage.ts.toGMTString() !== newLastmessage.ts.toGMTString()) {
return true; return true;
} }
if (this.props._updatedAt && nextProps._updatedAt && nextProps._updatedAt.toGMTString() !== this.props._updatedAt.toGMTString()) { if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toGMTString() !== _updatedAt.toGMTString()) {
return true; return true;
} }
// eslint-disable-next-line react/destructuring-assignment
return attrs.some(key => nextProps[key] !== this.props[key]); return attrs.some(key => nextProps[key] !== this.props[key]);
} }
get avatar() { get avatar() {
const { const {
type, name, avatarSize, baseUrl type, name, avatarSize, baseUrl
@ -172,10 +178,10 @@ export default class RoomItem extends React.Component {
get lastMessage() { get lastMessage() {
const { const {
lastMessage, type, showLastMessage lastMessage, type, showLastMessage, StoreLastMessage, username
} = this.props; } = this.props;
if (!this.props.StoreLastMessage || !showLastMessage) { if (!StoreLastMessage || !showLastMessage) {
return ''; return '';
} }
if (!lastMessage) { if (!lastMessage) {
@ -184,7 +190,7 @@ export default class RoomItem extends React.Component {
let prefix = ''; let prefix = '';
if (lastMessage.u.username === this.props.username) { if (lastMessage.u.username === username) {
prefix = I18n.t('You_colon'); prefix = I18n.t('You_colon');
} else if (type !== 'd') { } else if (type !== 'd') {
prefix = `${ lastMessage.u.username }: `; prefix = `${ lastMessage.u.username }: `;
@ -223,7 +229,7 @@ export default class RoomItem extends React.Component {
render() { render() {
const { const {
favorite, unread, userMentions, name, _updatedAt, alert, testID, height favorite, unread, userMentions, name, _updatedAt, alert, testID, height, onPress, onLongPress
} = this.props; } = this.props;
const date = this.formatDate(_updatedAt); const date = this.formatDate(_updatedAt);
@ -245,8 +251,8 @@ export default class RoomItem extends React.Component {
return ( return (
<Touch <Touch
onPress={this.props.onPress} onPress={onPress}
onLongPress={this.props.onLongPress} onLongPress={onLongPress}
underlayColor='#FFFFFF' underlayColor='#FFFFFF'
activeOpacity={0.5} activeOpacity={0.5}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Text, View, StyleSheet, Platform, ViewPropTypes, Image } from 'react-native'; import {
Text, View, StyleSheet, Platform, ViewPropTypes, Image
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';

View File

@ -1,4 +1,6 @@
import { call, takeLatest, select, put } from 'redux-saga/effects'; import {
call, takeLatest, select, put
} from 'redux-saga/effects';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { METEOR } from '../actions/actionsTypes'; import { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';

View File

@ -1,5 +1,7 @@
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { select, put, call, take, takeLatest } from 'redux-saga/effects'; import {
select, put, call, take, takeLatest
} from 'redux-saga/effects';
import { NavigationActions } from '../Navigation'; import { NavigationActions } from '../Navigation';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes'; import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';

View File

@ -1,5 +1,7 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { takeLatest, take, select, put } from 'redux-saga/effects'; import {
takeLatest, take, select, put
} from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { appStart } from '../actions'; import { appStart } from '../actions';

View File

@ -1,6 +1,8 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; import {
put, call, take, takeLatest, select, all
} from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { appStart } from '../actions'; import { appStart } from '../actions';

View File

@ -1,5 +1,7 @@
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects'; import {
put, call, takeLatest, take, select, race, fork, cancel, takeEvery
} from 'redux-saga/effects';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { BACKGROUND } from 'redux-enhancer-react-native-appstate'; import { BACKGROUND } from 'redux-enhancer-react-native-appstate';
@ -161,7 +163,7 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
yield goRoomsListAndDelete(rid); yield goRoomsListAndDelete(rid);
} catch (e) { } catch (e) {
if (e.error === 'error-you-are-last-owner') { if (e.error === 'error-you-are-last-owner') {
Alert.alert(e.error); Alert.alert(I18n.t(e.error));
} else { } else {
Alert.alert(I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_room') })); Alert.alert(I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_room') }));
} }

View File

@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, Switch, SafeAreaView, ScrollView, TextInput, StyleSheet, FlatList, Platform } from 'react-native'; import {
View, Text, Switch, SafeAreaView, ScrollView, TextInput, StyleSheet, FlatList, Platform
} from 'react-native';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
import LoggedView from './View'; import LoggedView from './View';
import { createChannelRequest } from '../actions/createChannel'; import { createChannelRequest as createChannelRequestAction } from '../actions/createChannel';
import { removeUser } from '../actions/selectedUsers'; import { removeUser as removeUserAction } from '../actions/selectedUsers';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
@ -73,8 +75,8 @@ const styles = StyleSheet.create({
createChannel: state.createChannel, createChannel: state.createChannel,
users: state.selectedUsers.users users: state.selectedUsers.users
}), dispatch => ({ }), dispatch => ({
create: data => dispatch(createChannelRequest(data)), create: data => dispatch(createChannelRequestAction(data)),
removeUser: user => dispatch(removeUser(user)) removeUser: user => dispatch(removeUserAction(user))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class CreateChannelView extends LoggedView { export default class CreateChannelView extends LoggedView {
@ -105,15 +107,19 @@ export default class CreateChannelView extends LoggedView {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.createChannel.error && prevProps.createChannel.error !== this.props.createChannel.error) { const { createChannel } = this.props;
if (createChannel.error && prevProps.createChannel.error !== createChannel.error) {
setTimeout(() => { setTimeout(() => {
const msg = this.props.createChannel.error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); const msg = createChannel.error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
showErrorAlert(msg); showErrorAlert(msg);
}, 300); }, 300);
} }
} }
onChangeText = (channelName) => { onChangeText = (channelName) => {
const { navigator } = this.props;
const rightButtons = []; const rightButtons = [];
if (channelName.trim().length > 0) { if (channelName.trim().length > 0) {
rightButtons.push({ rightButtons.push({
@ -122,7 +128,7 @@ export default class CreateChannelView extends LoggedView {
testID: 'create-channel-submit' testID: 'create-channel-submit'
}); });
} }
this.props.navigator.setButtons({ rightButtons }); navigator.setButtons({ rightButtons });
this.setState({ channelName }); this.setState({ channelName });
} }
@ -135,28 +141,30 @@ export default class CreateChannelView extends LoggedView {
} }
submit = () => { submit = () => {
if (!this.state.channelName.trim() || this.props.createChannel.isFetching) {
return;
}
const { const {
channelName, type, readOnly, broadcast channelName, type, readOnly, broadcast
} = this.state; } = this.state;
let { users } = this.props; const { users: usersProps, createChannel, create } = this.props;
if (!channelName.trim() || createChannel.isFetching) {
return;
}
// transform users object into array of usernames // transform users object into array of usernames
users = users.map(user => user.name); const users = usersProps.map(user => user.name);
// create channel // create channel
this.props.create({ create({
name: channelName, users, type, readOnly, broadcast name: channelName, users, type, readOnly, broadcast
}); });
} }
removeUser = (user) => { removeUser = (user) => {
if (this.props.users.length === 1) { const { users, removeUser } = this.props;
if (users.length === 1) {
return; return;
} }
this.props.removeUser(user); removeUser(user);
} }
renderSwitch = ({ renderSwitch = ({
@ -215,31 +223,42 @@ export default class CreateChannelView extends LoggedView {
renderFormSeparator = () => <View style={[sharedStyles.separator, styles.formSeparator]} /> renderFormSeparator = () => <View style={[sharedStyles.separator, styles.formSeparator]} />
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<UserItem const { baseUrl } = this.props;
name={item.fname}
username={item.name}
onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${ item.name }`}
baseUrl={this.props.baseUrl}
/>
)
renderInvitedList = () => ( return (
<FlatList <UserItem
data={this.props.users} name={item.fname}
extraData={this.props.users} username={item.name}
keyExtractor={item => item._id} onPress={() => this.removeUser(item)}
style={[styles.list, sharedStyles.separatorVertical]} testID={`create-channel-view-item-${ item.name }`}
renderItem={this.renderItem} baseUrl={baseUrl}
ItemSeparatorComponent={this.renderSeparator} />
enableEmptySections );
keyboardShouldPersistTaps='always' }
/>
) renderInvitedList = () => {
const { users } = this.props;
return (
<FlatList
data={users}
extraData={users}
keyExtractor={item => item._id}
style={[styles.list, sharedStyles.separatorVertical]}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
enableEmptySections
keyboardShouldPersistTaps='always'
/>
);
}
render() { render() {
const userCount = this.props.users.length; const { channelName } = this.state;
const { users, createChannel } = this.props;
const userCount = users.length;
return ( return (
<KeyboardView <KeyboardView
contentContainerStyle={[sharedStyles.container, styles.container]} contentContainerStyle={[sharedStyles.container, styles.container]}
@ -252,7 +271,7 @@ export default class CreateChannelView extends LoggedView {
ref={ref => this.channelNameRef = ref} ref={ref => this.channelNameRef = ref}
style={styles.input} style={styles.input}
label={I18n.t('Channel_Name')} label={I18n.t('Channel_Name')}
value={this.state.channelName} value={channelName}
onChangeText={this.onChangeText} onChangeText={this.onChangeText}
placeholder={I18n.t('Channel_Name')} placeholder={I18n.t('Channel_Name')}
returnKeyType='done' returnKeyType='done'
@ -273,7 +292,7 @@ export default class CreateChannelView extends LoggedView {
<Text style={styles.invitedCount}>{userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })}</Text> <Text style={styles.invitedCount}>{userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })}</Text>
</View> </View>
{this.renderInvitedList()} {this.renderInvitedList()}
<Loading visible={this.props.createChannel.isFetching} /> <Loading visible={createChannel.isFetching} />
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
</KeyboardView> </KeyboardView>

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, SafeAreaView, ScrollView } from 'react-native'; import {
Text, View, SafeAreaView, ScrollView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import LoggedView from './View'; import LoggedView from './View';
import { forgotPasswordInit, forgotPasswordRequest } from '../actions/login'; import { forgotPasswordInit as forgotPasswordInitAction, forgotPasswordRequest as forgotPasswordRequestAction } from '../actions/login';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import Button from '../containers/Button'; import Button from '../containers/Button';
@ -17,8 +19,8 @@ import I18n from '../i18n';
@connect(state => ({ @connect(state => ({
login: state.login login: state.login
}), dispatch => ({ }), dispatch => ({
forgotPasswordInit: () => dispatch(forgotPasswordInit()), forgotPasswordInit: () => dispatch(forgotPasswordInitAction()),
forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email)) forgotPasswordRequest: email => dispatch(forgotPasswordRequestAction(email))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class ForgotPasswordView extends LoggedView { export default class ForgotPasswordView extends LoggedView {
@ -39,13 +41,14 @@ export default class ForgotPasswordView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
this.props.forgotPasswordInit(); const { forgotPasswordInit } = this.props;
forgotPasswordInit();
} }
componentDidUpdate() { componentDidUpdate() {
const { login } = this.props; const { login, navigator } = this.props;
if (login.success) { if (login.success) {
this.props.navigator.pop(); navigator.pop();
setTimeout(() => { setTimeout(() => {
showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert')); showErrorAlert(I18n.t('Forgot_password_If_this_email_is_registered'), I18n.t('Alert'));
}); });
@ -64,13 +67,17 @@ export default class ForgotPasswordView extends LoggedView {
resetPassword = () => { resetPassword = () => {
const { email, invalidEmail } = this.state; const { email, invalidEmail } = this.state;
const { forgotPasswordRequest } = this.props;
if (invalidEmail || !email) { if (invalidEmail || !email) {
return; return;
} }
this.props.forgotPasswordRequest(email); forgotPasswordRequest(email);
} }
render() { render() {
const { invalidEmail } = this.state;
const { login } = this.props;
return ( return (
<KeyboardView <KeyboardView
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
@ -80,7 +87,7 @@ export default class ForgotPasswordView extends LoggedView {
<SafeAreaView style={styles.container} testID='forgot-password-view'> <SafeAreaView style={styles.container} testID='forgot-password-view'>
<View> <View>
<TextInput <TextInput
inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}} inputStyle={invalidEmail ? { borderColor: 'red' } : {}}
label={I18n.t('Email')} label={I18n.t('Email')}
placeholder={I18n.t('Email')} placeholder={I18n.t('Email')}
keyboardType='email-address' keyboardType='email-address'
@ -99,8 +106,8 @@ export default class ForgotPasswordView extends LoggedView {
/> />
</View> </View>
{this.props.login.failure ? <Text style={styles.error}>{this.props.login.error.reason}</Text> : null} {login.failure ? <Text style={styles.error}>{login.error.reason}</Text> : null}
<Loading visible={this.props.login.isFetching} /> <Loading visible={login.isFetching} />
</View> </View>
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>

View File

@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet, SafeAreaView } from 'react-native'; import {
Text, View, ScrollView, TouchableOpacity, LayoutAnimation, Image, StyleSheet, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/FontAwesome'; import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { open, close } from '../actions/login'; import { open as openAction, close as closeAction } from '../actions/login';
import LoggedView from './View'; import LoggedView from './View';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
@ -57,8 +59,8 @@ const styles = StyleSheet.create({
Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter, Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter,
services: state.login.services services: state.login.services
}), dispatch => ({ }), dispatch => ({
open: () => dispatch(open()), open: () => dispatch(openAction()),
close: () => dispatch(close()) close: () => dispatch(closeAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class LoginSignupView extends LoggedView { export default class LoginSignupView extends LoggedView {
@ -85,23 +87,27 @@ export default class LoginSignupView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
this.props.open(); const { open } = this.props;
open();
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.services !== nextProps.services) { const { services } = this.props;
if (services !== nextProps.services) {
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.close(); const { close } = this.props;
close();
} }
onPressFacebook = () => { onPressFacebook = () => {
const { appId } = this.props.services.facebook; const { services, server } = this.props;
const { appId } = services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth'; const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${ this.props.server }/_oauth/facebook?close`; const redirect_uri = `${ server }/_oauth/facebook?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ appId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`; const params = `?client_id=${ appId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`;
@ -109,9 +115,10 @@ export default class LoginSignupView extends LoggedView {
} }
onPressGithub = () => { onPressGithub = () => {
const { clientId } = this.props.services.github; const { services, server } = this.props;
const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`; const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
const redirect_uri = `${ this.props.server }/_oauth/github?close`; const redirect_uri = `${ server }/_oauth/github?close`;
const scope = 'user:email'; const scope = 'user:email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`;
@ -119,9 +126,10 @@ export default class LoginSignupView extends LoggedView {
} }
onPressGitlab = () => { onPressGitlab = () => {
const { clientId } = this.props.services.gitlab; const { services, server } = this.props;
const { clientId } = services.gitlab;
const endpoint = 'https://gitlab.com/oauth/authorize'; const endpoint = 'https://gitlab.com/oauth/authorize';
const redirect_uri = `${ this.props.server }/_oauth/gitlab?close`; const redirect_uri = `${ server }/_oauth/gitlab?close`;
const scope = 'read_user'; const scope = 'read_user';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
@ -129,9 +137,10 @@ export default class LoginSignupView extends LoggedView {
} }
onPressGoogle = () => { onPressGoogle = () => {
const { clientId } = this.props.services.google; const { services, server } = this.props;
const { clientId } = services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth'; const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${ this.props.server }/_oauth/google?close`; const redirect_uri = `${ server }/_oauth/google?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
@ -139,9 +148,10 @@ export default class LoginSignupView extends LoggedView {
} }
onPressLinkedin = () => { onPressLinkedin = () => {
const { clientId } = this.props.services.linkedin; const { services, server } = this.props;
const { clientId } = services.linkedin;
const endpoint = 'https://www.linkedin.com/uas/oauth2/authorization'; const endpoint = 'https://www.linkedin.com/uas/oauth2/authorization';
const redirect_uri = `${ this.props.server }/_oauth/linkedin?close`; const redirect_uri = `${ server }/_oauth/linkedin?close`;
const scope = 'r_emailaddress'; const scope = 'r_emailaddress';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
@ -149,17 +159,19 @@ export default class LoginSignupView extends LoggedView {
} }
onPressMeteor = () => { onPressMeteor = () => {
const { clientId } = this.props.services['meteor-developer']; const { services, server } = this.props;
const { clientId } = services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize'; const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${ this.props.server }/_oauth/meteor-developer`; const redirect_uri = `${ server }/_oauth/meteor-developer`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`); this.openOAuth(`${ endpoint }${ params }`);
} }
onPressTwitter = () => { onPressTwitter = () => {
const { server } = this.props;
const state = this.getOAuthState(); const state = this.getOAuthState();
const url = `${ this.props.server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`; const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
this.openOAuth(url); this.openOAuth(url);
} }
@ -169,7 +181,8 @@ export default class LoginSignupView extends LoggedView {
} }
openOAuth = (oAuthUrl) => { openOAuth = (oAuthUrl) => {
this.props.navigator.showModal({ const { navigator } = this.props;
navigator.showModal({
screen: 'OAuthView', screen: 'OAuthView',
title: 'OAuth', title: 'OAuth',
passProps: { passProps: {
@ -179,93 +192,113 @@ export default class LoginSignupView extends LoggedView {
} }
login = () => { login = () => {
this.props.navigator.push({ const { navigator, server } = this.props;
navigator.push({
screen: 'LoginView', screen: 'LoginView',
title: this.props.server, title: server,
backButtonTitle: '' backButtonTitle: ''
}); });
} }
register = () => { register = () => {
this.props.navigator.push({ const { navigator, server } = this.props;
navigator.push({
screen: 'RegisterView', screen: 'RegisterView',
title: this.props.server, title: server,
backButtonTitle: '' backButtonTitle: ''
}); });
} }
renderServices = () => { renderServices = () => {
const { services } = this.props; const {
services, Accounts_OAuth_Facebook, Accounts_OAuth_Github, Accounts_OAuth_Gitlab, Accounts_OAuth_Google, Accounts_OAuth_Linkedin, Accounts_OAuth_Meteor, Accounts_OAuth_Twitter
} = this.props;
if (!Object.keys(services).length) { if (!Object.keys(services).length) {
return null; return null;
} }
return ( return (
<View style={styles.servicesContainer}> <View style={styles.servicesContainer}>
<Text style={styles.servicesTitle}> <Text style={styles.servicesTitle}>
{I18n.t('Or_continue_using_social_accounts')} {I18n.t('Or_continue_using_social_accounts')}
</Text> </Text>
<View style={sharedStyles.loginOAuthButtons} key='services'> <View style={sharedStyles.loginOAuthButtons} key='services'>
{this.props.Accounts_OAuth_Facebook && this.props.services.facebook ? {Accounts_OAuth_Facebook && services.facebook
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]} <TouchableOpacity
onPress={this.onPressFacebook} style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
> onPress={this.onPressFacebook}
<Icon name='facebook' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='facebook' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Github && this.props.services.github ? {Accounts_OAuth_Github && services.github
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.githubButton]} <TouchableOpacity
onPress={this.onPressGithub} style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
> onPress={this.onPressGithub}
<Icon name='github' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='github' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ? {Accounts_OAuth_Gitlab && services.gitlab
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]} <TouchableOpacity
onPress={this.onPressGitlab} style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
> onPress={this.onPressGitlab}
<Icon name='gitlab' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='gitlab' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Google && this.props.services.google ? {Accounts_OAuth_Google && services.google
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.googleButton]} <TouchableOpacity
onPress={this.onPressGoogle} style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
> onPress={this.onPressGoogle}
<Icon name='google' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='google' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ? {Accounts_OAuth_Linkedin && services.linkedin
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]} <TouchableOpacity
onPress={this.onPressLinkedin} style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
> onPress={this.onPressLinkedin}
<Icon name='linkedin' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='linkedin' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ? {Accounts_OAuth_Meteor && services['meteor-developer']
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]} <TouchableOpacity
onPress={this.onPressMeteor} style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
> onPress={this.onPressMeteor}
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' /> >
</TouchableOpacity> <MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter ? {Accounts_OAuth_Twitter && services.twitter
<TouchableOpacity ? (
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]} <TouchableOpacity
onPress={this.onPressTwitter} style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
> onPress={this.onPressTwitter}
<Icon name='twitter' size={20} color='#ffffff' /> >
</TouchableOpacity> <Icon name='twitter' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null : null
} }
</View> </View>
@ -274,6 +307,8 @@ export default class LoginSignupView extends LoggedView {
} }
render() { render() {
const { isFetching } = this.props;
return ( return (
<ScrollView <ScrollView
style={[sharedStyles.container, sharedStyles.containerScrollView]} style={[sharedStyles.container, sharedStyles.containerScrollView]}
@ -298,7 +333,7 @@ export default class LoginSignupView extends LoggedView {
/> />
{this.renderServices()} {this.renderServices()}
</View> </View>
<Loading visible={this.props.isFetching} /> <Loading visible={isFetching} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
); );

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Keyboard, Text, ScrollView, View, SafeAreaView } from 'react-native'; import {
Keyboard, Text, ScrollView, View, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Answers } from 'react-native-fabric'; import { Answers } from 'react-native-fabric';
@ -50,6 +52,8 @@ export default class LoginView extends LoggedView {
submit = async() => { submit = async() => {
const { username, password, code } = this.state; const { username, password, code } = this.state;
const { loginSubmit } = this.props;
if (username.trim() === '' || password.trim() === '') { if (username.trim() === '' || password.trim() === '') {
showToast(I18n.t('Email_or_password_field_is_empty')); showToast(I18n.t('Email_or_password_field_is_empty'));
return; return;
@ -57,7 +61,7 @@ export default class LoginView extends LoggedView {
Keyboard.dismiss(); Keyboard.dismiss();
try { try {
await this.props.loginSubmit({ username, password, code }); await loginSubmit({ username, password, code });
Answers.logLogin('Email', true); Answers.logLogin('Email', true);
} catch (error) { } catch (error) {
console.warn('LoginView submit', error); console.warn('LoginView submit', error);
@ -65,15 +69,17 @@ export default class LoginView extends LoggedView {
} }
register = () => { register = () => {
this.props.navigator.push({ const { navigator, server } = this.props;
navigator.push({
screen: 'RegisterView', screen: 'RegisterView',
title: this.props.server, title: server,
backButtonTitle: '' backButtonTitle: ''
}); });
} }
forgotPassword = () => { forgotPassword = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'ForgotPasswordView', screen: 'ForgotPasswordView',
title: I18n.t('Forgot_Password'), title: I18n.t('Forgot_Password'),
backButtonTitle: '' backButtonTitle: ''
@ -81,7 +87,8 @@ export default class LoginView extends LoggedView {
} }
renderTOTP = () => { renderTOTP = () => {
if (/totp/ig.test(this.props.error)) { const { error } = this.props;
if (/totp/ig.test(error)) {
return ( return (
<TextInput <TextInput
inputRef={ref => this.codeInput = ref} inputRef={ref => this.codeInput = ref}
@ -99,6 +106,10 @@ export default class LoginView extends LoggedView {
} }
render() { render() {
const {
Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, failure, reason, isFetching
} = this.props;
return ( return (
<KeyboardView <KeyboardView
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
@ -110,7 +121,7 @@ export default class LoginView extends LoggedView {
<Text style={[styles.loginText, styles.loginTitle]}>Login</Text> <Text style={[styles.loginText, styles.loginTitle]}>Login</Text>
<TextInput <TextInput
label={I18n.t('Username')} label={I18n.t('Username')}
placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || I18n.t('Username')} placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Username')}
keyboardType='email-address' keyboardType='email-address'
returnKeyType='next' returnKeyType='next'
iconLeft='at' iconLeft='at'
@ -122,7 +133,7 @@ export default class LoginView extends LoggedView {
<TextInput <TextInput
inputRef={(e) => { this.password = e; }} inputRef={(e) => { this.password = e; }}
label={I18n.t('Password')} label={I18n.t('Password')}
placeholder={this.props.Accounts_PasswordPlaceholder || I18n.t('Password')} placeholder={Accounts_PasswordPlaceholder || I18n.t('Password')}
returnKeyType='done' returnKeyType='done'
iconLeft='key-variant' iconLeft='key-variant'
secureTextEntry secureTextEntry
@ -156,8 +167,8 @@ export default class LoginView extends LoggedView {
</Text> </Text>
</View> </View>
{this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null} {failure ? <Text style={styles.error}>{reason}</Text> : null}
<Loading visible={this.props.isFetching} /> <Loading visible={isFetching} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text, SafeAreaView } from 'react-native'; import {
FlatList, View, Text, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openMentionedMessages as openMentionedMessagesAction, closeMentionedMessages as closeMentionedMessagesAction } from '../../actions/mentionedMessages';
import LoggedView from '../View'; import LoggedView from '../View';
import { openMentionedMessages, closeMentionedMessages } from '../../actions/mentionedMessages';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
@ -19,8 +21,8 @@ import I18n from '../../i18n';
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} }
}), dispatch => ({ }), dispatch => ({
openMentionedMessages: (rid, limit) => dispatch(openMentionedMessages(rid, limit)), openMentionedMessages: (rid, limit) => dispatch(openMentionedMessagesAction(rid, limit)),
closeMentionedMessages: () => dispatch(closeMentionedMessages()) closeMentionedMessages: () => dispatch(closeMentionedMessagesAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class MentionedMessagesView extends LoggedView { export default class MentionedMessagesView extends LoggedView {
@ -47,17 +49,20 @@ export default class MentionedMessagesView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.ready && nextProps.ready !== this.props.ready) { const { ready } = this.props;
if (nextProps.ready && nextProps.ready !== ready) {
this.setState({ loading: false, loadingMore: false }); this.setState({ loading: false, loadingMore: false });
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.closeMentionedMessages(); const { closeMentionedMessages } = this.props;
closeMentionedMessages();
} }
load = () => { load = () => {
this.props.openMentionedMessages(this.props.rid, this.limit); const { openMentionedMessages, rid } = this.props;
openMentionedMessages(rid, this.limit);
} }
moreData = () => { moreData = () => {
@ -79,15 +84,18 @@ export default class MentionedMessagesView extends LoggedView {
</View> </View>
) )
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item} return (
style={styles.message} <Message
reactions={item.reactions} item={item}
user={this.props.user} style={styles.message}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' reactions={item.reactions}
/> user={user}
) customTimeFormat='MMMM Do YYYY, h:mm:ss a'
/>
);
}
render() { render() {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, StyleSheet, SafeAreaView, FlatList, Text, Platform, Image } from 'react-native'; import {
View, StyleSheet, SafeAreaView, FlatList, Text, Platform, Image
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import database from '../lib/realm'; import database from '../lib/realm';
@ -75,9 +77,10 @@ export default class NewMessageView extends LoggedView {
} }
async onNavigatorEvent(event) { async onNavigatorEvent(event) {
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'cancel') { if (event.id === 'cancel') {
this.props.navigator.dismissModal(); navigator.dismissModal();
} }
} }
} }
@ -87,12 +90,14 @@ export default class NewMessageView extends LoggedView {
} }
onPressItem = (item) => { onPressItem = (item) => {
this.props.navigator.dismissModal(); const { navigator, onPressItem } = this.props;
navigator.dismissModal();
setTimeout(() => { setTimeout(() => {
this.props.onPressItem(item); onPressItem(item);
}, 600); }, 600);
} }
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => { updateState = debounce(() => {
this.forceUpdate(); this.forceUpdate();
}, 1000); }, 1000);
@ -105,7 +110,8 @@ export default class NewMessageView extends LoggedView {
} }
createChannel = () => { createChannel = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'SelectedUsersView', screen: 'SelectedUsersView',
title: I18n.t('Select_Users'), title: I18n.t('Select_Users'),
backButtonTitle: '', backButtonTitle: '',
@ -130,14 +136,17 @@ export default class NewMessageView extends LoggedView {
renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />;
renderItem = ({ item, index }) => { renderItem = ({ item, index }) => {
const { search } = this.state;
const { baseUrl } = this.props;
let style = {}; let style = {};
if (index === 0) { if (index === 0) {
style = { ...sharedStyles.separatorTop }; style = { ...sharedStyles.separatorTop };
} }
if (this.state.search.length > 0 && index === this.state.search.length - 1) { if (search.length > 0 && index === search.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
if (this.state.search.length === 0 && index === this.data.length - 1) { if (search.length === 0 && index === this.data.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
return ( return (
@ -145,24 +154,27 @@ export default class NewMessageView extends LoggedView {
name={item.search ? item.name : item.fname} name={item.search ? item.name : item.fname}
username={item.search ? item.username : item.name} username={item.search ? item.username : item.name}
onPress={() => this.onPressItem(item)} onPress={() => this.onPressItem(item)}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
testID={`new-message-view-item-${ item.name }`} testID={`new-message-view-item-${ item.name }`}
style={style} style={style}
/> />
); );
} }
renderList = () => ( renderList = () => {
<FlatList const { search } = this.state;
data={this.state.search.length > 0 ? this.state.search : this.data} return (
extraData={this.state} <FlatList
keyExtractor={item => item._id} data={search.length > 0 ? search : this.data}
ListHeaderComponent={this.renderHeader} extraData={this.state}
renderItem={this.renderItem} keyExtractor={item => item._id}
ItemSeparatorComponent={this.renderSeparator} ListHeaderComponent={this.renderHeader}
keyboardShouldPersistTaps='always' renderItem={this.renderItem}
/> ItemSeparatorComponent={this.renderSeparator}
) keyboardShouldPersistTaps='always'
/>
);
}
render = () => ( render = () => (
<SafeAreaView style={styles.safeAreaView} testID='new-message-view'> <SafeAreaView style={styles.safeAreaView} testID='new-message-view'>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, ScrollView, Keyboard, SafeAreaView, Image, Alert, StyleSheet, TouchableOpacity } from 'react-native'; import {
Text, ScrollView, Keyboard, SafeAreaView, Image, Alert, StyleSheet, TouchableOpacity
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
@ -74,9 +76,9 @@ export default class NewServerView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
const { server } = this.props; const { server, connectServer } = this.props;
if (server) { if (server) {
this.props.connectServer(server); connectServer(server);
this.setState({ text: server }); this.setState({ text: server });
} else { } else {
setTimeout(() => { setTimeout(() => {
@ -86,7 +88,8 @@ export default class NewServerView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.failure && nextProps.failure !== this.props.failure) { const { failure } = this.props;
if (nextProps.failure && nextProps.failure !== failure) {
Alert.alert(I18n.t('Oops'), I18n.t('The_URL_is_invalid')); Alert.alert(I18n.t('Oops'), I18n.t('The_URL_is_invalid'));
} }
} }
@ -96,17 +99,20 @@ export default class NewServerView extends LoggedView {
} }
submit = () => { submit = () => {
if (this.state.text) { const { text } = this.state;
const { connectServer } = this.props;
if (text) {
Keyboard.dismiss(); Keyboard.dismiss();
this.props.connectServer(this.completeUrl(this.state.text)); connectServer(this.completeUrl(text));
} }
} }
completeUrl = (url) => { completeUrl = (url) => {
url = url && url.trim(); url = url && url.trim();
if (/^(\w|[0-9-_]){3,}$/.test(url) && if (/^(\w|[0-9-_]){3,}$/.test(url)
/^(htt(ps?)?)|(loca((l)?|(lh)?|(lho)?|(lhos)?|(lhost:?\d*)?)$)/.test(url) === false) { && /^(htt(ps?)?)|(loca((l)?|(lh)?|(lho)?|(lhos)?|(lhost:?\d*)?)$)/.test(url) === false) {
url = `${ url }.rocket.chat`; url = `${ url }.rocket.chat`;
} }
@ -122,6 +128,8 @@ export default class NewServerView extends LoggedView {
} }
renderBack = () => { renderBack = () => {
const { navigator } = this.props;
let top = 15; let top = 15;
if (DeviceInfo.getBrand() === 'Apple') { if (DeviceInfo.getBrand() === 'Apple') {
top = DeviceInfo.isNotch() ? 45 : 30; top = DeviceInfo.isNotch() ? 45 : 30;
@ -130,7 +138,7 @@ export default class NewServerView extends LoggedView {
return ( return (
<TouchableOpacity <TouchableOpacity
style={[styles.backButton, { top }]} style={[styles.backButton, { top }]}
onPress={() => this.props.navigator.pop()} onPress={() => navigator.pop()}
> >
<Icon <Icon
name='ios-arrow-back' name='ios-arrow-back'

View File

@ -52,9 +52,10 @@ export default class OAuthView extends React.PureComponent {
} }
render() { render() {
const { oAuthUrl, navigator } = this.props;
return ( return (
<WebView <WebView
source={{ uri: this.props.oAuthUrl }} source={{ uri: oAuthUrl }}
userAgent={userAgent} userAgent={userAgent}
onNavigationStateChange={(webViewState) => { onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url); const url = decodeURIComponent(webViewState.url);
@ -62,7 +63,7 @@ export default class OAuthView extends React.PureComponent {
const parts = url.split('#'); const parts = url.split('#');
const credentials = JSON.parse(parts[1]); const credentials = JSON.parse(parts[1]);
this.login({ oauth: { ...credentials } }); this.login({ oauth: { ...credentials } });
this.props.navigator.dismissModal(); navigator.dismissModal();
} }
}} }}
/> />

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, TouchableWithoutFeedback, Image } from 'react-native'; import {
View, Text, TouchableWithoutFeedback, Image
} from 'react-native';
import styles from './styles'; import styles from './styles';

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { View, Text, Image, SafeAreaView, TouchableOpacity } from 'react-native'; import {
View, Text, Image, SafeAreaView, TouchableOpacity
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -56,11 +58,13 @@ export default class OnboardingView extends LoggedView {
} }
close = () => { close = () => {
this.props.navigator.dismissModal(); const { navigator } = this.props;
navigator.dismissModal();
} }
connectServer = () => { connectServer = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'NewServerView', screen: 'NewServerView',
backButtonTitle: '', backButtonTitle: '',
navigatorStyle: { navigatorStyle: {
@ -70,7 +74,8 @@ export default class OnboardingView extends LoggedView {
} }
joinCommunity = () => { joinCommunity = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'NewServerView', screen: 'NewServerView',
backButtonTitle: '', backButtonTitle: '',
passProps: { passProps: {
@ -87,7 +92,9 @@ export default class OnboardingView extends LoggedView {
} }
renderClose = () => { renderClose = () => {
if (this.props.previousServer) { const { previousServer } = this.props;
if (previousServer) {
let top = 15; let top = 15;
if (DeviceInfo.getBrand() === 'Apple') { if (DeviceInfo.getBrand() === 'Apple') {
top = DeviceInfo.isNotch() ? 45 : 30; top = DeviceInfo.isNotch() ? 45 : 30;
@ -96,6 +103,7 @@ export default class OnboardingView extends LoggedView {
<TouchableOpacity <TouchableOpacity
style={[styles.closeModal, { top }]} style={[styles.closeModal, { top }]}
onPress={this.close} onPress={this.close}
testID='onboarding-close'
> >
<Icon <Icon
name='close' name='close'

View File

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text, SafeAreaView } from 'react-native'; import {
FlatList, View, Text, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import { openPinnedMessages as openPinnedMessagesAction, closePinnedMessages as closePinnedMessagesAction } from '../../actions/pinnedMessages';
import { togglePinRequest as togglePinRequestAction } from '../../actions/messages';
import LoggedView from '../View'; import LoggedView from '../View';
import { openPinnedMessages, closePinnedMessages } from '../../actions/pinnedMessages';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Message from '../../containers/message';
import { togglePinRequest } from '../../actions/messages';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -25,9 +27,9 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} }
}), dispatch => ({ }), dispatch => ({
openPinnedMessages: (rid, limit) => dispatch(openPinnedMessages(rid, limit)), openPinnedMessages: (rid, limit) => dispatch(openPinnedMessagesAction(rid, limit)),
closePinnedMessages: () => dispatch(closePinnedMessages()), closePinnedMessages: () => dispatch(closePinnedMessagesAction()),
togglePinRequest: message => dispatch(togglePinRequest(message)) togglePinRequest: message => dispatch(togglePinRequestAction(message))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class PinnedMessagesView extends LoggedView { export default class PinnedMessagesView extends LoggedView {
@ -56,13 +58,15 @@ export default class PinnedMessagesView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.ready && nextProps.ready !== this.props.ready) { const { ready } = this.props;
if (nextProps.ready && nextProps.ready !== ready) {
this.setState({ loading: false, loadingMore: false }); this.setState({ loading: false, loadingMore: false });
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.closePinnedMessages(); const { closePinnedMessages } = this.props;
closePinnedMessages();
} }
onLongPress = (message) => { onLongPress = (message) => {
@ -73,9 +77,12 @@ export default class PinnedMessagesView extends LoggedView {
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { message } = this.state;
const { togglePinRequest } = this.props;
switch (actionIndex) { switch (actionIndex) {
case PIN_INDEX: case PIN_INDEX:
this.props.togglePinRequest(this.state.message); togglePinRequest(message);
break; break;
default: default:
break; break;
@ -83,7 +90,8 @@ export default class PinnedMessagesView extends LoggedView {
} }
load = () => { load = () => {
this.props.openPinnedMessages(this.props.rid, this.limit); const { openPinnedMessages, rid } = this.props;
openPinnedMessages(rid, this.limit);
} }
moreData = () => { moreData = () => {
@ -105,16 +113,19 @@ export default class PinnedMessagesView extends LoggedView {
</View> </View>
) )
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item} return (
style={styles.message} <Message
reactions={item.reactions} item={item}
user={this.props.user} style={styles.message}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' reactions={item.reactions}
onLongPress={this.onLongPress} user={user}
/> customTimeFormat='MMMM Do YYYY, h:mm:ss a'
) onLongPress={this.onLongPress}
/>
);
}
render() { render() {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;

View File

@ -20,9 +20,11 @@ export default class PrivacyPolicyView extends LoggedView {
} }
render() { render() {
const { privacyPolicy } = this.props;
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<WebView originWhitelist={['*']} source={{ html: this.props.privacyPolicy, baseUrl: '' }} /> <WebView originWhitelist={['*']} source={{ html: privacyPolicy, baseUrl: '' }} />
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, ScrollView, SafeAreaView, Keyboard, Dimensions } from 'react-native'; import {
View, ScrollView, SafeAreaView, Keyboard, Dimensions
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Dialog from 'react-native-dialog'; import Dialog from 'react-native-dialog';
import SHA256 from 'js-sha256'; import SHA256 from 'js-sha256';
@ -61,7 +63,8 @@ export default class ProfileView extends LoggedView {
} }
componentWillMount() { componentWillMount() {
this.props.navigator.setButtons({ const { navigator } = this.props;
navigator.setButtons({
leftButtons: [{ leftButtons: [{
id: 'settings', id: 'settings',
icon: { uri: 'settings', scale: Dimensions.get('window').scale } icon: { uri: 'settings', scale: Dimensions.get('window').scale }
@ -70,9 +73,11 @@ export default class ProfileView extends LoggedView {
} }
async componentDidMount() { async componentDidMount() {
const { navigator } = this.props;
this.init(); this.init();
this.props.navigator.setDrawerEnabled({ navigator.setDrawerEnabled({
side: 'left', side: 'left',
enabled: true enabled: true
}); });
@ -86,15 +91,18 @@ export default class ProfileView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.user !== nextProps.user) { const { user } = this.props;
if (user !== nextProps.user) {
this.init(nextProps.user); this.init(nextProps.user);
} }
} }
onNavigatorEvent(event) { onNavigatorEvent(event) {
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'settings') { if (event.id === 'settings') {
this.props.navigator.toggleDrawer({ navigator.toggleDrawer({
side: 'left' side: 'left'
}); });
} }
@ -106,9 +114,11 @@ export default class ProfileView extends LoggedView {
} }
init = (user) => { init = (user) => {
const { user: userProps } = this.props;
const { const {
name, username, emails, customFields name, username, emails, customFields
} = user || this.props.user; } = user || userProps;
this.setState({ this.setState({
name, name,
username, username,
@ -137,12 +147,12 @@ export default class ProfileView extends LoggedView {
}); });
} }
return !(user.name === name && return !(user.name === name
user.username === username && && user.username === username
!newPassword && && !newPassword
(user.emails && user.emails[0].address === email) && && (user.emails && user.emails[0].address === email)
!avatar.data && && !avatar.data
!customFieldsChanged && !customFieldsChanged
); );
} }
@ -278,43 +288,50 @@ export default class ProfileView extends LoggedView {
</Touch> </Touch>
) )
renderAvatarButtons = () => ( renderAvatarButtons = () => {
<View style={styles.avatarButtons}> const { avatarUrl, avatarSuggestions } = this.state;
{this.renderAvatarButton({ const { user, baseUrl } = this.props;
child: <Avatar text={this.props.user.username} size={50} baseUrl={this.props.baseUrl} forceInitials />,
onPress: () => this.resetAvatar(), return (
key: 'profile-view-reset-avatar' <View style={styles.avatarButtons}>
})} {this.renderAvatarButton({
{this.renderAvatarButton({ child: <Avatar text={user.username} size={50} baseUrl={baseUrl} forceInitials />,
child: <Icon name='file-upload' size={30} />, onPress: () => this.resetAvatar(),
onPress: () => this.pickImage(), key: 'profile-view-reset-avatar'
key: 'profile-view-upload-avatar' })}
})} {this.renderAvatarButton({
{this.renderAvatarButton({ child: <Icon name='file-upload' size={30} />,
child: <Icon name='link' size={30} />, onPress: () => this.pickImage(),
onPress: () => this.setAvatar({ url: this.state.avatarUrl, data: this.state.avatarUrl, service: 'url' }), key: 'profile-view-upload-avatar'
disabled: !this.state.avatarUrl, })}
key: 'profile-view-avatar-url-button' {this.renderAvatarButton({
})} child: <Icon name='link' size={30} />,
{Object.keys(this.state.avatarSuggestions).map((service) => { onPress: () => this.setAvatar({ url: avatarUrl, data: avatarUrl, service: 'url' }),
const { url, blob, contentType } = this.state.avatarSuggestions[service]; disabled: !avatarUrl,
return this.renderAvatarButton({ key: 'profile-view-avatar-url-button'
key: `profile-view-avatar-${ service }`, })}
child: <Avatar avatar={url} size={50} baseUrl={this.props.baseUrl} />, {Object.keys(avatarSuggestions).map((service) => {
onPress: () => this.setAvatar({ const { url, blob, contentType } = avatarSuggestions[service];
url, data: blob, service, contentType return this.renderAvatarButton({
}) key: `profile-view-avatar-${ service }`,
}); child: <Avatar avatar={url} size={50} baseUrl={baseUrl} />,
})} onPress: () => this.setAvatar({
</View> url, data: blob, service, contentType
); })
});
})}
</View>
);
}
renderCustomFields = () => { renderCustomFields = () => {
const { customFields } = this.state; const { customFields } = this.state;
if (!this.props.Accounts_CustomFields) { const { Accounts_CustomFields } = this.props;
if (!Accounts_CustomFields) {
return null; return null;
} }
const parsedCustomFields = JSON.parse(this.props.Accounts_CustomFields); const parsedCustomFields = JSON.parse(Accounts_CustomFields);
return Object.keys(parsedCustomFields).map((key, index, array) => { return Object.keys(parsedCustomFields).map((key, index, array) => {
if (parsedCustomFields[key].type === 'select') { if (parsedCustomFields[key].type === 'select') {
const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option })); const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option }));
@ -325,7 +342,7 @@ export default class ProfileView extends LoggedView {
onValueChange={(value) => { onValueChange={(value) => {
const newValue = {}; const newValue = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...this.state.customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
value={customFields[key]} value={customFields[key]}
> >
@ -350,7 +367,7 @@ export default class ProfileView extends LoggedView {
onChangeText={(value) => { onChangeText={(value) => {
const newValue = {}; const newValue = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...this.state.customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
onSubmitEditing={() => { onSubmitEditing={() => {
if (array.length - 1 > index) { if (array.length - 1 > index) {
@ -365,8 +382,10 @@ export default class ProfileView extends LoggedView {
render() { render() {
const { const {
name, username, email, newPassword, avatarUrl, customFields name, username, email, newPassword, avatarUrl, customFields, avatar, saving, showPasswordAlert
} = this.state; } = this.state;
const { baseUrl } = this.props;
return ( return (
<KeyboardView <KeyboardView
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
@ -381,9 +400,9 @@ export default class ProfileView extends LoggedView {
<View style={styles.avatarContainer} testID='profile-view-avatar'> <View style={styles.avatarContainer} testID='profile-view-avatar'>
<Avatar <Avatar
text={username} text={username}
avatar={this.state.avatar && this.state.avatar.url} avatar={avatar && avatar.url}
size={100} size={100}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
/> />
</View> </View>
<RCTextInput <RCTextInput
@ -448,8 +467,8 @@ export default class ProfileView extends LoggedView {
testID='profile-view-submit' testID='profile-view-submit'
/> />
</View> </View>
<Loading visible={this.state.saving} /> <Loading visible={saving} />
<Dialog.Container visible={this.state.showPasswordAlert}> <Dialog.Container visible={showPasswordAlert}>
<Dialog.Title> <Dialog.Title>
{I18n.t('Please_enter_your_password')} {I18n.t('Please_enter_your_password')}
</Dialog.Title> </Dialog.Title>

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Keyboard, Text, View, ScrollView, SafeAreaView } from 'react-native'; import {
Keyboard, Text, View, ScrollView, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { registerSubmit, setUsernameSubmit } from '../actions/login'; import { registerSubmit as registerSubmitAction, setUsernameSubmit as setUsernameSubmitAction } from '../actions/login';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import Button from '../containers/Button'; import Button from '../containers/Button';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
@ -22,8 +24,8 @@ import I18n from '../i18n';
Accounts_RepeatPasswordPlaceholder: state.settings.Accounts_RepeatPasswordPlaceholder, Accounts_RepeatPasswordPlaceholder: state.settings.Accounts_RepeatPasswordPlaceholder,
login: state.login login: state.login
}), dispatch => ({ }), dispatch => ({
registerSubmit: params => dispatch(registerSubmit(params)), registerSubmit: params => dispatch(registerSubmitAction(params)),
setUsernameSubmit: params => dispatch(setUsernameSubmit(params)) setUsernameSubmit: params => dispatch(setUsernameSubmitAction(params))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RegisterView extends LoggedView { export default class RegisterView extends LoggedView {
@ -51,28 +53,31 @@ export default class RegisterView extends LoggedView {
}; };
} }
valid() { valid = () => {
const { const {
name, email, password, confirmPassword name, email, password, confirmPassword
} = this.state; } = this.state;
return name.trim() && email.trim() && return name.trim() && email.trim()
password && confirmPassword && password === confirmPassword; && password && confirmPassword && password === confirmPassword;
} }
invalidEmail() { invalidEmail = () => {
return this.props.login.failure && /Email/.test(this.props.login.error.reason) ? this.props.login.error : {}; const { login } = this.props;
return login.failure && /Email/.test(login.error && login.error.reason) ? login.error : {};
} }
submit = () => { submit = () => {
const { const {
name, email, password, code name, email, password, code
} = this.state; } = this.state;
const { registerSubmit } = this.props;
if (!this.valid()) { if (!this.valid()) {
showToast(I18n.t('Some_field_is_invalid_or_empty')); showToast(I18n.t('Some_field_is_invalid_or_empty'));
return; return;
} }
this.props.registerSubmit({ registerSubmit({
name, email, pass: password, code name, email, pass: password, code
}); });
Keyboard.dismiss(); Keyboard.dismiss();
@ -80,17 +85,20 @@ export default class RegisterView extends LoggedView {
usernameSubmit = () => { usernameSubmit = () => {
const { username } = this.state; const { username } = this.state;
const { setUsernameSubmit } = this.props;
if (!username) { if (!username) {
showToast(I18n.t('Username_is_empty')); showToast(I18n.t('Username_is_empty'));
return; return;
} }
this.props.setUsernameSubmit({ username }); setUsernameSubmit({ username });
Keyboard.dismiss(); Keyboard.dismiss();
} }
termsService = () => { termsService = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'TermsServiceView', screen: 'TermsServiceView',
title: I18n.t('Terms_of_Service'), title: I18n.t('Terms_of_Service'),
backButtonTitle: '' backButtonTitle: ''
@ -98,7 +106,8 @@ export default class RegisterView extends LoggedView {
} }
privacyPolicy = () => { privacyPolicy = () => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'PrivacyPolicyView', screen: 'PrivacyPolicyView',
title: I18n.t('Privacy_Policy'), title: I18n.t('Privacy_Policy'),
backButtonTitle: '' backButtonTitle: ''
@ -106,15 +115,20 @@ export default class RegisterView extends LoggedView {
} }
_renderRegister() { _renderRegister() {
if (this.props.login.token) { const { password, confirmPassword } = this.state;
const {
login, Accounts_NamePlaceholder, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder, Accounts_RepeatPasswordPlaceholder
} = this.props;
if (login.token) {
return null; return null;
} }
return ( return (
<View> <View>
<TextInput <TextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
label={this.props.Accounts_NamePlaceholder || I18n.t('Name')} label={Accounts_NamePlaceholder || I18n.t('Name')}
placeholder={this.props.Accounts_NamePlaceholder || I18n.t('Name')} placeholder={Accounts_NamePlaceholder || I18n.t('Name')}
returnKeyType='next' returnKeyType='next'
iconLeft='account' iconLeft='account'
onChangeText={name => this.setState({ name })} onChangeText={name => this.setState({ name })}
@ -123,8 +137,8 @@ export default class RegisterView extends LoggedView {
/> />
<TextInput <TextInput
inputRef={(e) => { this.email = e; }} inputRef={(e) => { this.email = e; }}
label={this.props.Accounts_EmailOrUsernamePlaceholder || I18n.t('Email')} label={Accounts_EmailOrUsernamePlaceholder || I18n.t('Email')}
placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || I18n.t('Email')} placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Email')}
returnKeyType='next' returnKeyType='next'
keyboardType='email-address' keyboardType='email-address'
iconLeft='email' iconLeft='email'
@ -135,28 +149,28 @@ export default class RegisterView extends LoggedView {
/> />
<TextInput <TextInput
inputRef={(e) => { this.password = e; }} inputRef={(e) => { this.password = e; }}
label={this.props.Accounts_PasswordPlaceholder || I18n.t('Password')} label={Accounts_PasswordPlaceholder || I18n.t('Password')}
placeholder={this.props.Accounts_PasswordPlaceholder || I18n.t('Password')} placeholder={Accounts_PasswordPlaceholder || I18n.t('Password')}
returnKeyType='next' returnKeyType='next'
iconLeft='key-variant' iconLeft='key-variant'
secureTextEntry secureTextEntry
onChangeText={password => this.setState({ password })} onChangeText={value => this.setState({ password: value })}
onSubmitEditing={() => { this.confirmPassword.focus(); }} onSubmitEditing={() => { this.confirmPassword.focus(); }}
testID='register-view-password' testID='register-view-password'
/> />
<TextInput <TextInput
inputRef={(e) => { this.confirmPassword = e; }} inputRef={(e) => { this.confirmPassword = e; }}
inputStyle={ inputStyle={
this.state.password && password
this.state.confirmPassword && && confirmPassword
this.state.confirmPassword !== this.state.password ? { borderColor: 'red' } : {} && confirmPassword !== password ? { borderColor: 'red' } : {}
} }
label={this.props.Accounts_RepeatPasswordPlaceholder || I18n.t('Repeat_Password')} label={Accounts_RepeatPasswordPlaceholder || I18n.t('Repeat_Password')}
placeholder={this.props.Accounts_RepeatPasswordPlaceholder || I18n.t('Repeat_Password')} placeholder={Accounts_RepeatPasswordPlaceholder || I18n.t('Repeat_Password')}
returnKeyType='done' returnKeyType='done'
iconLeft='key-variant' iconLeft='key-variant'
secureTextEntry secureTextEntry
onChangeText={confirmPassword => this.setState({ confirmPassword })} onChangeText={value => this.setState({ confirmPassword: value })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
testID='register-view-repeat-password' testID='register-view-repeat-password'
/> />
@ -180,15 +194,17 @@ export default class RegisterView extends LoggedView {
} }
_renderUsername() { _renderUsername() {
if (!this.props.login.token) { const { login, Accounts_UsernamePlaceholder } = this.props;
if (!login.token) {
return null; return null;
} }
return ( return (
<View> <View>
<TextInput <TextInput
inputRef={(e) => { this.username = e; }} inputRef={(e) => { this.username = e; }}
label={this.props.Accounts_UsernamePlaceholder || I18n.t('Username')} label={Accounts_UsernamePlaceholder || I18n.t('Username')}
placeholder={this.props.Accounts_UsernamePlaceholder || I18n.t('Username')} placeholder={Accounts_UsernamePlaceholder || I18n.t('Username')}
returnKeyType='done' returnKeyType='done'
iconLeft='at' iconLeft='at'
onChangeText={username => this.setState({ username })} onChangeText={username => this.setState({ username })}
@ -209,6 +225,7 @@ export default class RegisterView extends LoggedView {
} }
render() { render() {
const { login } = this.props;
return ( return (
<KeyboardView contentContainerStyle={styles.container}> <KeyboardView contentContainerStyle={styles.container}>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
@ -216,13 +233,15 @@ export default class RegisterView extends LoggedView {
<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text> <Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
{this._renderRegister()} {this._renderRegister()}
{this._renderUsername()} {this._renderUsername()}
{this.props.login.failure ? {login.failure
<Text style={styles.error} testID='register-view-error'> ? (
{this.props.login.error.reason} <Text style={styles.error} testID='register-view-error'>
</Text> {login.error.reason}
</Text>
)
: null : null
} }
<Loading visible={this.props.login.isFetching} /> <Loading visible={login.isFetching} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>

View File

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, SectionList, Text, Alert, SafeAreaView } from 'react-native'; import {
View, SectionList, Text, Alert, SafeAreaView
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { leaveRoom as leaveRoomAction } from '../../actions/room';
import LoggedView from '../View'; import LoggedView from '../View';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
@ -13,7 +16,6 @@ import Status from '../../containers/status';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import { leaveRoom } from '../../actions/room';
import log from '../../utils/log'; import log from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -26,7 +28,7 @@ const renderSeparator = () => <View style={styles.separator} />;
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({ }), dispatch => ({
leaveRoom: rid => dispatch(leaveRoom(rid)) leaveRoom: rid => dispatch(leaveRoomAction(rid))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomActionsView extends LoggedView { export default class RoomActionsView extends LoggedView {
@ -63,8 +65,10 @@ export default class RoomActionsView extends LoggedView {
} }
onPressTouchable = (item) => { onPressTouchable = (item) => {
const { navigator } = this.props;
if (item.route) { if (item.route) {
this.props.navigator.push({ navigator.push({
screen: item.route, screen: item.route,
title: item.name, title: item.name,
passProps: item.params, passProps: item.params,
@ -81,8 +85,10 @@ export default class RoomActionsView extends LoggedView {
rid, t rid, t
} = this.room; } = this.room;
const { allMembers } = this.state; const { allMembers } = this.state;
const { username } = this.props;
// TODO: same test joined // TODO: same test joined
const userInRoom = !!allMembers.find(m => m.username === this.props.username); const userInRoom = !!allMembers.find(m => m.username === username);
const permissions = RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid); const permissions = RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid);
if (userInRoom && permissions['add-user-to-joined-room']) { if (userInRoom && permissions['add-user-to-joined-room']) {
@ -96,8 +102,10 @@ export default class RoomActionsView extends LoggedView {
} }
return false; return false;
} }
get canViewMembers() { get canViewMembers() {
const { rid, t, broadcast } = this.state.room; const { room } = this.state;
const { rid, t, broadcast } = room;
if (broadcast) { if (broadcast) {
const viewBroadcastMemberListPermission = 'view-broadcast-member-list'; const viewBroadcastMemberListPermission = 'view-broadcast-member-list';
const permissions = RocketChat.hasPermission([viewBroadcastMemberListPermission], rid); const permissions = RocketChat.hasPermission([viewBroadcastMemberListPermission], rid);
@ -107,6 +115,7 @@ export default class RoomActionsView extends LoggedView {
} }
return (t === 'c' || t === 'p'); return (t === 'c' || t === 'p');
} }
get sections() { get sections() {
const { const {
rid, t, blocker, notifications rid, t, blocker, notifications
@ -218,9 +227,9 @@ export default class RoomActionsView extends LoggedView {
actions.push({ actions.push({
icon: 'ios-people', icon: 'ios-people',
name: I18n.t('Members'), name: I18n.t('Members'),
description: (onlineMembers.length === 1 ? description: (onlineMembers.length === 1
I18n.t('1_online_member') : ? I18n.t('1_online_member')
I18n.t('N_online_members', { n: onlineMembers.length })), : I18n.t('N_online_members', { n: onlineMembers.length })),
route: 'RoomMembersView', route: 'RoomMembersView',
params: { rid, members: onlineMembers }, params: { rid, members: onlineMembers },
testID: 'room-actions-members' testID: 'room-actions-members'
@ -257,7 +266,8 @@ export default class RoomActionsView extends LoggedView {
} }
updateRoomMembers = async() => { updateRoomMembers = async() => {
const { t } = this.state.room; const { room } = this.state;
const { rid, t } = room;
if (!this.canViewMembers) { if (!this.canViewMembers) {
return {}; return {};
@ -267,8 +277,8 @@ export default class RoomActionsView extends LoggedView {
let onlineMembers = []; let onlineMembers = [];
let allMembers = []; let allMembers = [];
try { try {
const onlineMembersCall = RocketChat.getRoomMembers(this.state.room.rid, false); const onlineMembersCall = RocketChat.getRoomMembers(rid, false);
const allMembersCall = RocketChat.getRoomMembers(this.state.room.rid, true); const allMembersCall = RocketChat.getRoomMembers(rid, true);
const [onlineMembersResult, allMembersResult] = await Promise.all([onlineMembersCall, allMembersCall]); const [onlineMembersResult, allMembersResult] = await Promise.all([onlineMembersCall, allMembersCall]);
onlineMembers = onlineMembersResult.records; onlineMembers = onlineMembersResult.records;
allMembers = allMembersResult.records; allMembers = allMembersResult.records;
@ -280,11 +290,15 @@ export default class RoomActionsView extends LoggedView {
} }
updateRoomMember = async() => { updateRoomMember = async() => {
if (this.state.room.t !== 'd') { const { room } = this.state;
const { rid, t } = room;
const { userId } = this.props;
if (t !== 'd') {
return {}; return {};
} }
try { try {
const member = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId); const member = await RocketChat.getRoomMember(rid, userId);
return { member }; return { member };
} catch (e) { } catch (e) {
log('RoomActions updateRoomMember', e); log('RoomActions updateRoomMember', e);
@ -297,7 +311,8 @@ export default class RoomActionsView extends LoggedView {
} }
toggleBlockUser = async() => { toggleBlockUser = async() => {
const { rid, blocker } = this.state.room; const { room } = this.state;
const { rid, blocker } = room;
const { member } = this.state; const { member } = this.state;
try { try {
RocketChat.toggleBlockUser(rid, member._id, !blocker); RocketChat.toggleBlockUser(rid, member._id, !blocker);
@ -308,6 +323,8 @@ export default class RoomActionsView extends LoggedView {
leaveChannel = () => { leaveChannel = () => {
const { room } = this.state; const { room } = this.state;
const { leaveRoom } = this.props;
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: room.t === 'd' ? room.fname : room.name }), I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: room.t === 'd' ? room.fname : room.name }),
@ -319,7 +336,7 @@ export default class RoomActionsView extends LoggedView {
{ {
text: I18n.t('Yes_action_it', { action: I18n.t('leave') }), text: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
style: 'destructive', style: 'destructive',
onPress: () => this.props.leaveRoom(room.rid) onPress: () => leaveRoom(room.rid)
} }
] ]
); );
@ -337,6 +354,8 @@ export default class RoomActionsView extends LoggedView {
renderRoomInfo = ({ item }) => { renderRoomInfo = ({ item }) => {
const { room, member } = this.state; const { room, member } = this.state;
const { name, t, topic } = room; const { name, t, topic } = room;
const { baseUrl } = this.props;
return ( return (
this.renderTouchableItem([ this.renderTouchableItem([
<Avatar <Avatar
@ -345,17 +364,19 @@ export default class RoomActionsView extends LoggedView {
size={50} size={50}
style={styles.avatar} style={styles.avatar}
type={t} type={t}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
> >
{t === 'd' ? <Status style={sharedStyles.status} id={member._id} /> : null } {t === 'd' ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>, </Avatar>,
<View key='name' style={styles.roomTitleContainer}> <View key='name' style={styles.roomTitleContainer}>
{room.t === 'd' ? {room.t === 'd'
<Text style={styles.roomTitle}>{room.fname}</Text> : ? <Text style={styles.roomTitle}>{room.fname}</Text>
<View style={styles.roomTitleRow}> : (
<RoomTypeIcon type={room.t} /> <View style={styles.roomTitleRow}>
<Text style={styles.roomTitle}>{room.name}</Text> <RoomTypeIcon type={room.t} />
</View> <Text style={styles.roomTitle}>{room.name}</Text>
</View>
)
} }
<Text style={styles.roomDescription} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text> <Text style={styles.roomDescription} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text>
</View>, </View>,

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text, SafeAreaView } from 'react-native'; import {
FlatList, View, Text, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openRoomFiles as openRoomFilesAction, closeRoomFiles as closeRoomFilesAction } from '../../actions/roomFiles';
import LoggedView from '../View'; import LoggedView from '../View';
import { openRoomFiles, closeRoomFiles } from '../../actions/roomFiles';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
@ -19,8 +21,8 @@ import I18n from '../../i18n';
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} }
}), dispatch => ({ }), dispatch => ({
openRoomFiles: (rid, limit) => dispatch(openRoomFiles(rid, limit)), openRoomFiles: (rid, limit) => dispatch(openRoomFilesAction(rid, limit)),
closeRoomFiles: () => dispatch(closeRoomFiles()) closeRoomFiles: () => dispatch(closeRoomFilesAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomFilesView extends LoggedView { export default class RoomFilesView extends LoggedView {
@ -47,17 +49,20 @@ export default class RoomFilesView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.ready && nextProps.ready !== this.props.ready) { const { ready } = this.props;
if (nextProps.ready && nextProps.ready !== ready) {
this.setState({ loading: false, loadingMore: false }); this.setState({ loading: false, loadingMore: false });
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.closeRoomFiles(); const { closeRoomFiles } = this.props;
closeRoomFiles();
} }
load = () => { load = () => {
this.props.openRoomFiles(this.props.rid, this.limit); const { openRoomFiles, rid } = this.props;
openRoomFiles(rid, this.limit);
} }
moreData = () => { moreData = () => {
@ -79,15 +84,19 @@ export default class RoomFilesView extends LoggedView {
</View> </View>
) )
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item}
style={styles.message} return (
reactions={item.reactions} <Message
user={this.props.user} item={item}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' style={styles.message}
/> reactions={item.reactions}
) user={user}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
/>
);
}
render() { render() {
const { messages, ready } = this.props; const { messages, ready } = this.props;

View File

@ -3,7 +3,7 @@ import { View, Text, Switch } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../Styles';
export default class SwitchContainer extends React.PureComponent { export default class SwitchContainer extends React.PureComponent {
static propTypes = { static propTypes = {

View File

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, ScrollView, TouchableOpacity, SafeAreaView, Keyboard, Alert } from 'react-native'; import {
Text, View, ScrollView, TouchableOpacity, SafeAreaView, Keyboard, Alert
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { eraseRoom as eraseRoomAction } from '../../actions/room';
import LoggedView from '../View'; import LoggedView from '../View';
import KeyboardView from '../../presentation/KeyboardView'; import KeyboardView from '../../presentation/KeyboardView';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
@ -11,7 +14,6 @@ import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { showErrorAlert, showToast } from '../../utils/info'; import { showErrorAlert, showToast } from '../../utils/info';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import { eraseRoom } from '../../actions/room';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
import Loading from '../../containers/Loading'; import Loading from '../../containers/Loading';
import SwitchContainer from './SwitchContainer'; import SwitchContainer from './SwitchContainer';
@ -35,7 +37,7 @@ const PERMISSIONS_ARRAY = [
]; ];
@connect(null, dispatch => ({ @connect(null, dispatch => ({
eraseRoom: rid => dispatch(eraseRoom(rid)) eraseRoom: rid => dispatch(eraseRoomAction(rid))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomInfoEditView extends LoggedView { export default class RoomInfoEditView extends LoggedView {
@ -66,10 +68,11 @@ export default class RoomInfoEditView extends LoggedView {
async componentDidMount() { async componentDidMount() {
const { room } = this.state;
await this.updateRoom(); await this.updateRoom();
this.init(); this.init();
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, this.state.room.rid); this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid);
} }
componentWillUnmount() { componentWillUnmount() {
@ -82,9 +85,10 @@ export default class RoomInfoEditView extends LoggedView {
} }
init = () => { init = () => {
const { room } = this.state;
const { const {
name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired
} = this.state.room; } = room;
// fake password just to user knows about it // fake password just to user knows about it
this.randomValue = random(15); this.randomValue = random(15);
this.setState({ this.setState({
@ -114,14 +118,14 @@ export default class RoomInfoEditView extends LoggedView {
const { const {
room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
} = this.state; } = this.state;
return !(room.name === name && return !(room.name === name
room.description === description && && room.description === description
room.topic === topic && && room.topic === topic
room.announcement === announcement && && room.announcement === announcement
this.randomValue === joinCode && && this.randomValue === joinCode
room.t === 'p' === t && && room.t === 'p' === t
room.ro === ro && && room.ro === ro
room.reactWhenReadOnly === reactWhenReadOnly && room.reactWhenReadOnly === reactWhenReadOnly
); );
} }
@ -199,6 +203,9 @@ export default class RoomInfoEditView extends LoggedView {
} }
delete = () => { delete = () => {
const { room } = this.state;
const { eraseRoom } = this.props;
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
I18n.t('Delete_Room_Warning'), I18n.t('Delete_Room_Warning'),
@ -210,7 +217,7 @@ export default class RoomInfoEditView extends LoggedView {
{ {
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }), text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
style: 'destructive', style: 'destructive',
onPress: () => this.props.eraseRoom(this.state.room.rid) onPress: () => eraseRoom(room.rid)
} }
], ],
{ cancelable: false } { cancelable: false }
@ -218,7 +225,9 @@ export default class RoomInfoEditView extends LoggedView {
} }
toggleArchive = () => { toggleArchive = () => {
const { archived } = this.state.room; const { room } = this.state;
const { rid, archived } = room;
const action = I18n.t(`${ archived ? 'un' : '' }archive`); const action = I18n.t(`${ archived ? 'un' : '' }archive`);
Alert.alert( Alert.alert(
I18n.t('Are_you_sure_question_mark'), I18n.t('Are_you_sure_question_mark'),
@ -233,7 +242,7 @@ export default class RoomInfoEditView extends LoggedView {
style: 'destructive', style: 'destructive',
onPress: () => { onPress: () => {
try { try {
RocketChat.toggleArchiveRoom(this.state.room.rid, !archived); RocketChat.toggleArchiveRoom(rid, !archived);
} catch (e) { } catch (e) {
log('toggleArchive', e); log('toggleArchive', e);
} }
@ -244,9 +253,12 @@ export default class RoomInfoEditView extends LoggedView {
); );
} }
hasDeletePermission = () => ( hasDeletePermission = () => {
this.state.room.t === 'p' ? this.permissions[PERMISSION_DELETE_P] : this.permissions[PERMISSION_DELETE_C] const { room } = this.state;
); return (
room.t === 'p' ? this.permissions[PERMISSION_DELETE_P] : this.permissions[PERMISSION_DELETE_C]
);
}
hasArchivePermission = () => ( hasArchivePermission = () => (
this.permissions[PERMISSION_ARCHIVE] || this.permissions[PERMISSION_UNARCHIVE] this.permissions[PERMISSION_ARCHIVE] || this.permissions[PERMISSION_UNARCHIVE]
@ -254,7 +266,7 @@ export default class RoomInfoEditView extends LoggedView {
render() { render() {
const { const {
name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode, saving
} = this.state; } = this.state;
return ( return (
<KeyboardView <KeyboardView
@ -328,21 +340,23 @@ export default class RoomInfoEditView extends LoggedView {
disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast} disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
testID='room-info-edit-view-ro' testID='room-info-edit-view-ro'
/> />
{ro && !room.broadcast ? {ro && !room.broadcast
<SwitchContainer ? (
value={reactWhenReadOnly} <SwitchContainer
leftLabelPrimary={I18n.t('No_Reactions')} value={reactWhenReadOnly}
leftLabelSecondary={I18n.t('Reactions_are_disabled')} leftLabelPrimary={I18n.t('No_Reactions')}
rightLabelPrimary={I18n.t('Allow_Reactions')} leftLabelSecondary={I18n.t('Reactions_are_disabled')}
rightLabelSecondary={I18n.t('Reactions_are_enabled')} rightLabelPrimary={I18n.t('Allow_Reactions')}
onValueChange={value => this.setState({ reactWhenReadOnly: value })} rightLabelSecondary={I18n.t('Reactions_are_enabled')}
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]} onValueChange={value => this.setState({ reactWhenReadOnly: value })}
testID='room-info-edit-view-react-when-ro' disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
/> testID='room-info-edit-view-react-when-ro'
/>
)
: null : null
} }
{room.broadcast ? {room.broadcast
[ ? [
<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>, <Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
<View style={styles.divider} /> <View style={styles.divider} />
] ]
@ -394,7 +408,7 @@ export default class RoomInfoEditView extends LoggedView {
> >
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text> <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text>
</TouchableOpacity> </TouchableOpacity>
<Loading visible={this.state.saving} /> <Loading visible={saving} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, ScrollView, SafeAreaView } from 'react-native'; import {
View, Text, ScrollView, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
@ -20,14 +22,14 @@ import { iconsMap } from '../../Icons';
const PERMISSION_EDIT_ROOM = 'edit-room'; const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase()); const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' ? const getRoomTitle = room => (room.t === 'd'
<Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text> : ? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text>
[ : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} key='room-info-type' /> <RoomTypeIcon type={room.t} key='room-info-type' />
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text> <Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
</View> </View>
] )
); );
@connect(state => ({ @connect(state => ({
@ -35,7 +37,7 @@ const getRoomTitle = room => (room.t === 'd' ?
userId: state.login.user && state.login.user.id, userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers, activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles allRoles: state.roles
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomInfoView extends LoggedView { export default class RoomInfoView extends LoggedView {
@ -46,7 +48,7 @@ export default class RoomInfoView extends LoggedView {
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
activeUsers: PropTypes.object, activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string, Message_TimeFormat: PropTypes.string,
roles: PropTypes.object allRoles: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -64,19 +66,61 @@ export default class RoomInfoView extends LoggedView {
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
} }
async componentDidMount() { componentDidMount() {
await this.updateRoom(); this.updateRoom();
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
}
componentWillUnmount() {
this.rooms.removeAllListeners();
this.sub.unsubscribe();
}
onNavigatorEvent(event) {
const { rid, navigator } = this.props;
if (event.type === 'NavBarButtonPress') {
if (event.id === 'edit') {
navigator.push({
screen: 'RoomInfoEditView',
title: I18n.t('Room_Info_Edit'),
backButtonTitle: '',
passProps: {
rid
}
});
}
}
}
getFullUserData = async(username) => {
try {
const result = await RocketChat.subscribe('fullUserData', username);
this.sub = result;
} catch (e) {
log('getFullUserData', e);
}
}
isDirect = () => {
const { room: { t } } = this.state;
return t === 'd';
}
updateRoom = async() => {
const { userId, activeUsers, navigator } = this.props;
const [room] = this.rooms;
this.setState({ room });
// get user of room // get user of room
if (this.state.room) { if (room) {
if (this.state.room.t === 'd') { if (room.t === 'd') {
try { try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId); const roomUser = await RocketChat.getRoomMember(room.rid, userId);
this.setState({ roomUser }); this.setState({ roomUser });
const username = this.state.room.name; const username = room.name;
const activeUser = this.props.activeUsers[roomUser._id]; const activeUser = activeUsers[roomUser._id];
if (!activeUser || !activeUser.utcOffset) { if (!activeUser || !activeUser.utcOffset) {
// get full user data looking for utcOffset // get full user data looking for utcOffset
// will be catched by .on('users) and saved on activeUsers reducer // will be catched by .on('users) and saved on activeUsers reducer
@ -94,9 +138,14 @@ export default class RoomInfoView extends LoggedView {
log('RoomInfoView.componentDidMount', e); log('RoomInfoView.componentDidMount', e);
} }
} else { } else {
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid); const isVisible = await navigator.screenIsCurrentlyVisible();
if (!isVisible) {
return;
}
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM]) { if (permissions[PERMISSION_EDIT_ROOM]) {
this.props.navigator.setButtons({ navigator.setButtons({
rightButtons: [{ rightButtons: [{
id: 'edit', id: 'edit',
icon: iconsMap.create, icon: iconsMap.create,
@ -108,42 +157,6 @@ export default class RoomInfoView extends LoggedView {
} }
} }
componentWillUnmount() {
this.rooms.removeAllListeners();
this.sub.unsubscribe();
}
onNavigatorEvent(event) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'edit') {
this.props.navigator.push({
screen: 'RoomInfoEditView',
title: I18n.t('Room_Info_Edit'),
backButtonTitle: '',
passProps: {
rid: this.props.rid
}
});
}
}
}
getFullUserData = async(username) => {
try {
const result = await RocketChat.subscribe('fullUserData', username);
this.sub = result;
} catch (e) {
log('getFullUserData', e);
}
}
isDirect = () => this.state.room.t === 'd';
updateRoom = async() => {
const [room] = this.rooms;
this.setState({ room });
}
renderItem = (key, room) => ( renderItem = (key, room) => (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text> <Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text>
@ -155,24 +168,33 @@ export default class RoomInfoView extends LoggedView {
</View> </View>
); );
renderRoles = () => ( renderRoles = () => {
this.state.roles.length > 0 ? const { roles } = this.state;
<View style={styles.item}> const { allRoles } = this.props;
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}> return (
{this.state.roles.map(role => ( roles.length > 0
<View style={styles.roleBadge} key={role}> ? (
<Text>{ this.props.roles[role] }</Text> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}>
{roles.map(role => (
<View style={styles.roleBadge} key={role}>
<Text>{ allRoles[role] }</Text>
</View>
))}
</View> </View>
))} </View>
</View> )
</View> : null
: null );
) }
renderTimezone = (userId) => { renderTimezone = (userId) => {
if (this.props.activeUsers[userId]) { const { activeUsers, Message_TimeFormat } = this.props;
const { utcOffset } = this.props.activeUsers[userId];
if (activeUsers[userId]) {
const { utcOffset } = activeUsers[userId];
if (!utcOffset) { if (!utcOffset) {
return null; return null;
@ -180,24 +202,28 @@ export default class RoomInfoView extends LoggedView {
return ( return (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text> <Text style={styles.itemLabel}>{I18n.t('Timezone')}</Text>
<Text style={styles.itemContent}>{moment().utcOffset(utcOffset).format(this.props.Message_TimeFormat)} (UTC { utcOffset })</Text> <Text style={styles.itemContent}>{moment().utcOffset(utcOffset).format(Message_TimeFormat)} (UTC { utcOffset })</Text>
</View> </View>
); );
} }
return null; return null;
} }
renderAvatar = (room, roomUser) => ( renderAvatar = (room, roomUser) => {
<Avatar const { baseUrl } = this.props;
text={room.name}
size={100} return (
style={styles.avatar} <Avatar
type={room.t} text={room.name}
baseUrl={this.props.baseUrl} size={100}
> style={styles.avatar}
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null} type={room.t}
</Avatar> baseUrl={baseUrl}
) >
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null}
</Avatar>
);
}
renderBroadcast = () => ( renderBroadcast = () => (
<View style={styles.item}> <View style={styles.item}>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Vibration, SafeAreaView } from 'react-native'; import {
FlatList, View, Vibration, SafeAreaView
} from 'react-native';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -37,6 +39,8 @@ export default class RoomMembersView extends LoggedView {
constructor(props) { constructor(props) {
super('MentionedMessagesView', props); super('MentionedMessagesView', props);
const { navigator } = this.props;
this.CANCEL_INDEX = 0; this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1; this.MUTE_INDEX = 1;
this.actionSheetOptions = ['']; this.actionSheetOptions = [''];
@ -52,7 +56,7 @@ export default class RoomMembersView extends LoggedView {
userLongPressed: {}, userLongPressed: {},
room: {} room: {}
}; };
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
} }
componentDidMount() { componentDidMount() {
@ -64,16 +68,19 @@ export default class RoomMembersView extends LoggedView {
} }
async onNavigatorEvent(event) { async onNavigatorEvent(event) {
const { rid, allUsers } = this.state;
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'toggleOnline') { if (event.id === 'toggleOnline') {
try { try {
const allUsers = !this.state.allUsers; const allUsersFilter = !allUsers;
const membersResult = await RocketChat.getRoomMembers(this.state.rid, allUsers); const membersResult = await RocketChat.getRoomMembers(rid, allUsersFilter);
const members = membersResult.records; const members = membersResult.records;
this.setState({ allUsers, members }); this.setState({ allUsers: allUsersFilter, members });
this.props.navigator.setButtons({ navigator.setButtons({
rightButtons: [{ rightButtons: [{
title: this.state.allUsers ? I18n.t('Online') : I18n.t('All'), title: allUsers ? I18n.t('Online') : I18n.t('All'),
id: 'toggleOnline', id: 'toggleOnline',
testID: 'room-members-view-toggle-status' testID: 'room-members-view-toggle-status'
}] }]
@ -86,9 +93,11 @@ export default class RoomMembersView extends LoggedView {
} }
onSearchChangeText = (text) => { onSearchChangeText = (text) => {
const { members } = this.state;
let membersFiltered = []; let membersFiltered = [];
if (text) { if (text) {
membersFiltered = this.state.members.filter(m => m.username.toLowerCase().match(text.toLowerCase())); membersFiltered = members.filter(m => m.username.toLowerCase().match(text.toLowerCase()));
} }
this.setState({ filtering: !!text, membersFiltered }); this.setState({ filtering: !!text, membersFiltered });
} }
@ -111,8 +120,10 @@ export default class RoomMembersView extends LoggedView {
if (!this.permissions['mute-user']) { if (!this.permissions['mute-user']) {
return; return;
} }
const { room } = this.state;
const { muted } = room;
this.actionSheetOptions = [I18n.t('Cancel')]; this.actionSheetOptions = [I18n.t('Cancel')];
const { muted } = this.state.room;
const userIsMuted = !!muted.find(m => m.value === user.username); const userIsMuted = !!muted.find(m => m.value === user.username);
user.muted = userIsMuted; user.muted = userIsMuted;
if (userIsMuted) { if (userIsMuted) {
@ -133,9 +144,10 @@ export default class RoomMembersView extends LoggedView {
} }
goRoom = ({ rid, name }) => { goRoom = ({ rid, name }) => {
this.props.navigator.popToRoot(); const { navigator } = this.props;
navigator.popToRoot();
setTimeout(() => { setTimeout(() => {
this.props.navigator.push({ navigator.push({
screen: 'RoomView', screen: 'RoomView',
title: name, title: name,
backButtonTitle: '', backButtonTitle: '',
@ -170,16 +182,20 @@ export default class RoomMembersView extends LoggedView {
renderSeparator = () => <View style={styles.separator} />; renderSeparator = () => <View style={styles.separator} />;
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<UserItem const { baseUrl } = this.props;
name={item.name}
username={item.username} return (
onPress={() => this.onPressUser(item)} <UserItem
onLongPress={() => this.onLongPressUser(item)} name={item.name}
baseUrl={this.props.baseUrl} username={item.username}
testID={`room-members-view-item-${ item.username }`} onPress={() => this.onPressUser(item)}
/> onLongPress={() => this.onLongPressUser(item)}
) baseUrl={baseUrl}
testID={`room-members-view-item-${ item.username }`}
/>
);
}
render() { render() {
const { filtering, members, membersFiltered } = this.state; const { filtering, members, membersFiltered } = this.state;

View File

@ -35,6 +35,7 @@ export class List extends React.Component {
room: PropTypes.string, room: PropTypes.string,
end: PropTypes.bool end: PropTypes.bool
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this.data = database this.data = database
@ -43,16 +44,22 @@ export class List extends React.Component {
.sorted('ts', true); .sorted('ts', true);
this.dataSource = ds.cloneWithRows(this.data); this.dataSource = ds.cloneWithRows(this.data);
} }
componentDidMount() { componentDidMount() {
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return this.props.end !== nextProps.end; const { end } = this.props;
return end !== nextProps.end;
} }
componentWillUnmount() { componentWillUnmount() {
this.data.removeAllListeners(); this.data.removeAllListeners();
this.updateState.stop(); this.updateState.stop();
} }
// eslint-disable-next-line react/sort-comp
updateState = throttle(() => { updateState = throttle(() => {
// this.setState({ // this.setState({
this.dataSource = this.dataSource.cloneWithRows(this.data); this.dataSource = this.dataSource.cloneWithRows(this.data);
@ -62,6 +69,8 @@ export class List extends React.Component {
}, 1000); }, 1000);
render() { render() {
const { renderFooter, onEndReached, renderRow } = this.props;
return ( return (
<ListView <ListView
enableEmptySections enableEmptySections
@ -69,11 +78,11 @@ export class List extends React.Component {
data={this.data} data={this.data}
keyExtractor={item => item._id} keyExtractor={item => item._id}
onEndReachedThreshold={100} onEndReachedThreshold={100}
renderFooter={this.props.renderFooter} renderFooter={renderFooter}
renderHeader={() => <Typing />} renderHeader={() => <Typing />}
onEndReached={() => this.props.onEndReached(this.data[this.data.length - 1])} onEndReached={() => onEndReached(this.data[this.data.length - 1])}
dataSource={this.dataSource} dataSource={this.dataSource}
renderRow={(item, previousItem) => this.props.renderRow(item, previousItem)} renderRow={(item, previousItem) => renderRow(item, previousItem)}
initialListSize={1} initialListSize={1}
pageSize={20} pageSize={20}
testID='room-view-messages' testID='room-view-messages'
@ -106,7 +115,9 @@ export class ListView extends OldList2 {
setNativeProps(props) { setNativeProps(props) {
this.refs.listView.setNativeProps(props); this.refs.listView.setNativeProps(props);
} }
static DataSource = DataSource; static DataSource = DataSource;
render() { render() {
const bodyComponents = []; const bodyComponents = [];
@ -132,9 +143,9 @@ export class ListView extends OldList2 {
continue; // eslint-disable-line continue; // eslint-disable-line
} }
const showUnreadSeparator = this.props.lastOpen && const showUnreadSeparator = this.props.lastOpen
moment(message.ts).isAfter(this.props.lastOpen) && && moment(message.ts).isAfter(this.props.lastOpen)
moment(previousMessage.ts).isBefore(this.props.lastOpen); && moment(previousMessage.ts).isBefore(this.props.lastOpen);
const showDateSeparator = !moment(message.ts).isSame(previousMessage.ts, 'day'); const showDateSeparator = !moment(message.ts).isSame(previousMessage.ts, 'day');
if (showUnreadSeparator || showDateSeparator) { if (showUnreadSeparator || showDateSeparator) {

View File

@ -4,8 +4,9 @@ import { View, Platform } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import EmojiPicker from '../../containers/EmojiPicker'; import EmojiPicker from '../../containers/EmojiPicker';
import { toggleReactionPicker } from '../../actions/messages'; import { toggleReactionPicker as toggleReactionPickerAction } from '../../actions/messages';
import styles from './styles'; import styles from './styles';
const margin = Platform.OS === 'android' ? 40 : 20; const margin = Platform.OS === 'android' ? 40 : 20;
@ -15,7 +16,7 @@ const tabEmojiStyle = { fontSize: 15 };
showReactionPicker: state.messages.showReactionPicker, showReactionPicker: state.messages.showReactionPicker,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({ }), dispatch => ({
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
})) }))
@responsive @responsive
export default class ReactionPicker extends React.Component { export default class ReactionPicker extends React.Component {
@ -28,40 +29,47 @@ export default class ReactionPicker extends React.Component {
}; };
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return nextProps.showReactionPicker !== this.props.showReactionPicker || this.props.window.width !== nextProps.window.width; const { showReactionPicker, window } = this.props;
return nextProps.showReactionPicker !== showReactionPicker || window.width !== nextProps.window.width;
} }
onEmojiSelected(emoji, shortname) { onEmojiSelected(emoji, shortname) {
// standard emojis: `emoji` is unicode and `shortname` is :joy: // standard emojis: `emoji` is unicode and `shortname` is :joy:
// custom emojis: only `emoji` is returned with shortname type (:joy:) // custom emojis: only `emoji` is returned with shortname type (:joy:)
// to set reactions, we need shortname type // to set reactions, we need shortname type
this.props.onEmojiSelected(shortname || emoji); const { onEmojiSelected } = this.props;
onEmojiSelected(shortname || emoji);
} }
render() { render() {
const { window: { width, height }, showReactionPicker, baseUrl } = this.props; const {
window: { width, height }, showReactionPicker, baseUrl, toggleReactionPicker
} = this.props;
return (showReactionPicker ? return (showReactionPicker
<Modal ? (
isVisible={this.props.showReactionPicker} <Modal
style={{ alignItems: 'center' }} isVisible={showReactionPicker}
onBackdropPress={() => this.props.toggleReactionPicker()} style={{ alignItems: 'center' }}
onBackButtonPress={() => this.props.toggleReactionPicker()} onBackdropPress={() => toggleReactionPicker()}
animationIn='fadeIn' onBackButtonPress={() => toggleReactionPicker()}
animationOut='fadeOut' animationIn='fadeIn'
> animationOut='fadeOut'
<View
style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]}
testID='reaction-picker'
> >
<EmojiPicker <View
tabEmojiStyle={tabEmojiStyle} style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]}
width={Math.min(width, height) - margin} testID='reaction-picker'
onEmojiSelected={(emoji, shortname) => this.onEmojiSelected(emoji, shortname)} >
baseUrl={baseUrl} <EmojiPicker
/> tabEmojiStyle={tabEmojiStyle}
</View> width={Math.min(width, height) - margin}
</Modal> : null onEmojiSelected={(emoji, shortname) => this.onEmojiSelected(emoji, shortname)}
baseUrl={baseUrl}
/>
</View>
</Modal>
)
: null
); );
} }
} }

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'; import {
View, Text, StyleSheet, TouchableOpacity, ScrollView
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
@ -64,7 +66,8 @@ export default class UploadProgress extends Component {
this.state = { this.state = {
uploads: [] uploads: []
}; };
this.uploads = database.objects('uploads').filtered('rid = $0', this.props.rid); const { rid } = this.props;
this.uploads = database.objects('uploads').filtered('rid = $0', rid);
this.uploads.addListener(this.updateUploads); this.uploads.addListener(this.updateUploads);
} }
@ -98,11 +101,13 @@ export default class UploadProgress extends Component {
} }
tryAgain = async(item) => { tryAgain = async(item) => {
const { rid } = this.props;
try { try {
database.write(() => { database.write(() => {
item.error = false; item.error = false;
}); });
await RocketChat.sendFileMessage(this.props.rid, JSON.parse(JSON.stringify(item))); await RocketChat.sendFileMessage(rid, JSON.parse(JSON.stringify(item)));
} catch (e) { } catch (e) {
log('UploadProgess.tryAgain', e); log('UploadProgess.tryAgain', e);
} }
@ -113,6 +118,8 @@ export default class UploadProgress extends Component {
} }
renderItemContent = (item) => { renderItemContent = (item) => {
const { window } = this.props;
if (!item.error) { if (!item.error) {
return ( return (
[ [
@ -123,7 +130,7 @@ export default class UploadProgress extends Component {
</Text> </Text>
<Icon name='close' size={20} color='#9EA2A8' onPress={() => this.cancelUpload(item)} /> <Icon name='close' size={20} color='#9EA2A8' onPress={() => this.cancelUpload(item)} />
</View>, </View>,
<View key='progress' style={[styles.progress, { width: (this.props.window.width * item.progress) / 100 }]} /> <View key='progress' style={[styles.progress, { width: (window.width * item.progress) / 100 }]} />
] ]
); );
} }

View File

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, LayoutAnimation, ActivityIndicator, SafeAreaView } from 'react-native'; import {
Text, View, LayoutAnimation, ActivityIndicator, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import { openRoom as openRoomAction, closeRoom as closeRoomAction, setLastOpen as setLastOpenAction } from '../../actions/room';
import { toggleReactionPicker as toggleReactionPickerAction, actionsShow as actionsShowAction } from '../../actions/messages';
import LoggedView from '../View'; import LoggedView from '../View';
import { List } from './ListView'; import { List } from './ListView';
import { openRoom, closeRoom, setLastOpen } from '../../actions/room';
import { toggleReactionPicker, actionsShow } from '../../actions/messages';
import database from '../../lib/realm'; import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message'; import Message from '../../containers/message';
@ -33,11 +35,11 @@ import { iconsMap } from '../../Icons';
showActions: state.messages.showActions, showActions: state.messages.showActions,
showErrorActions: state.messages.showErrorActions showErrorActions: state.messages.showErrorActions
}), dispatch => ({ }), dispatch => ({
openRoom: room => dispatch(openRoom(room)), openRoom: room => dispatch(openRoomAction(room)),
setLastOpen: date => dispatch(setLastOpen(date)), setLastOpen: date => dispatch(setLastOpenAction(date)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)), toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
close: () => dispatch(closeRoom()) closeRoom: () => dispatch(closeRoomAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomView extends LoggedView { export default class RoomView extends LoggedView {
@ -56,12 +58,12 @@ export default class RoomView extends LoggedView {
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
toggleReactionPicker: PropTypes.func.isRequired, toggleReactionPicker: PropTypes.func.isRequired,
actionsShow: PropTypes.func, actionsShow: PropTypes.func,
close: PropTypes.func closeRoom: PropTypes.func
}; };
constructor(props) { constructor(props) {
super('RoomView', props); super('RoomView', props);
this.rid = this.props.rid; this.rid = props.rid;
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
loaded: false, loaded: false,
@ -74,7 +76,9 @@ export default class RoomView extends LoggedView {
} }
componentWillMount() { componentWillMount() {
this.props.navigator.setButtons({ const { navigator } = this.props;
navigator.setButtons({
rightButtons: [{ rightButtons: [{
id: 'more', id: 'more',
testID: 'room-view-header-actions', testID: 'room-view-header-actions',
@ -88,9 +92,11 @@ export default class RoomView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
const { navigator } = this.props;
this.updateRoom(); this.updateRoom();
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
this.props.navigator.setDrawerEnabled({ navigator.setDrawerEnabled({
side: 'left', side: 'left',
enabled: false enabled: false
}); });
@ -98,12 +104,16 @@ export default class RoomView extends LoggedView {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
return !(equal(this.props, nextProps) && equal(this.state, nextState) && this.state.room.ro === nextState.room.ro); const { room } = this.state;
return !(equal(this.props, nextProps) && equal(this.state, nextState) && room.ro === nextState.room.ro);
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevState.room.f !== this.state.room.f) { const { room } = this.state;
this.props.navigator.setButtons({ const { navigator } = this.props;
if (prevState.room.f !== room.f) {
navigator.setButtons({
rightButtons: [{ rightButtons: [{
id: 'more', id: 'more',
testID: 'room-view-header-actions', testID: 'room-view-header-actions',
@ -111,32 +121,37 @@ export default class RoomView extends LoggedView {
}, { }, {
id: 'star', id: 'star',
testID: 'room-view-header-star', testID: 'room-view-header-star',
icon: this.state.room.f ? iconsMap.star : iconsMap.starOutline icon: room.f ? iconsMap.star : iconsMap.starOutline
}] }]
}); });
} }
} }
componentWillUnmount() { componentWillUnmount() {
const { closeRoom } = this.props;
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
this.onEndReached.stop(); this.onEndReached.stop();
this.props.close(); closeRoom();
} }
onNavigatorEvent(event) { onNavigatorEvent(event) {
const { room } = this.state;
const { rid, f } = room;
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'more') { if (event.id === 'more') {
this.props.navigator.push({ navigator.push({
screen: 'RoomActionsView', screen: 'RoomActionsView',
title: I18n.t('Actions'), title: I18n.t('Actions'),
backButtonTitle: '', backButtonTitle: '',
passProps: { passProps: {
rid: this.state.room.rid rid
} }
}); });
} else if (event.id === 'star') { } else if (event.id === 'star') {
try { try {
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f); RocketChat.toggleFavorite(rid, f);
} catch (e) { } catch (e) {
log('toggleFavorite', e); log('toggleFavorite', e);
} }
@ -151,8 +166,9 @@ export default class RoomView extends LoggedView {
} }
requestAnimationFrame(async() => { requestAnimationFrame(async() => {
const { room } = this.state;
try { try {
const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts }); const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: room.t, latest: lastRowData.ts });
this.setState({ end: result < 20 }); this.setState({ end: result < 20 });
} catch (e) { } catch (e) {
log('RoomView.onEndReached', e); log('RoomView.onEndReached', e);
@ -161,14 +177,16 @@ export default class RoomView extends LoggedView {
}) })
onMessageLongPress = (message) => { onMessageLongPress = (message) => {
this.props.actionsShow(message); const { actionsShow } = this.props;
actionsShow(message);
} }
onReactionPress = (shortname, messageId) => { onReactionPress = (shortname, messageId) => {
const { actionMessage, toggleReactionPicker } = this.props;
try { try {
if (!messageId) { if (!messageId) {
RocketChat.setReaction(shortname, this.props.actionMessage._id); RocketChat.setReaction(shortname, actionMessage._id);
return this.props.toggleReactionPicker(); return toggleReactionPicker();
} }
RocketChat.setReaction(shortname, messageId); RocketChat.setReaction(shortname, messageId);
} catch (e) { } catch (e) {
@ -177,53 +195,71 @@ export default class RoomView extends LoggedView {
}; };
updateRoom = async() => { updateRoom = async() => {
const { navigator, openRoom, setLastOpen } = this.props;
if (this.rooms.length > 0) { if (this.rooms.length > 0) {
const { room: prevRoom } = this.state; const { room: prevRoom } = this.state;
const room = JSON.parse(JSON.stringify(this.rooms[0])); const room = JSON.parse(JSON.stringify(this.rooms[0]));
this.setState({ room }); this.setState({ room });
if (!prevRoom.rid) { if (!prevRoom.rid) {
this.props.navigator.setTitle({ title: room.name }); navigator.setTitle({ title: room.name });
this.props.openRoom({ openRoom({
...room ...room
}); });
if (room.alert || room.unread || room.userMentions) { if (room.alert || room.unread || room.userMentions) {
this.props.setLastOpen(room.ls); setLastOpen(room.ls);
} else { } else {
this.props.setLastOpen(null); setLastOpen(null);
} }
} }
} else { } else {
this.props.openRoom({ rid: this.rid }); openRoom({ rid: this.rid });
this.setState({ joined: false }); this.setState({ joined: false });
} }
} }
sendMessage = (message) => { sendMessage = (message) => {
const { setLastOpen } = this.props;
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
RocketChat.sendMessage(this.rid, message).then(() => { RocketChat.sendMessage(this.rid, message).then(() => {
this.props.setLastOpen(null); setLastOpen(null);
}); });
}; };
joinRoom = async() => { joinRoom = async() => {
const { rid } = this.props;
try { try {
await RocketChat.joinRoom(this.props.rid); await RocketChat.joinRoom(rid);
this.setState({ joined: true }); this.setState({
joined: true
});
} catch (e) { } catch (e) {
log('joinRoom', e); log('joinRoom', e);
} }
}; };
isOwner = () => this.state.room && this.state.room.roles && Array.from(Object.keys(this.state.room.roles), i => this.state.room.roles[i].value).includes('owner'); isOwner = () => {
const { room } = this.state;
return room && room.roles && Array.from(Object.keys(room.roles), i => room.roles[i].value).includes('owner');
}
isMuted = () => this.state.room && this.state.room.muted && Array.from(Object.keys(this.state.room.muted), i => this.state.room.muted[i].value).includes(this.props.user.username); isMuted = () => {
const { room } = this.state;
const { user } = this.props;
return room && room.muted && Array.from(Object.keys(room.muted), i => room.muted[i].value).includes(user.username);
}
isReadOnly = () => this.state.room.ro && this.isMuted() && !this.isOwner(); isReadOnly = () => {
const { room } = this.state;
return room.ro && this.isMuted() && !this.isOwner();
}
isBlocked = () => { isBlocked = () => {
if (this.state.room) { const { room } = this.state;
const { t, blocked, blocker } = this.state.room;
if (room) {
const { t, blocked, blocker } = room;
if (t === 'd' && (blocked || blocker)) { if (t === 'd' && (blocked || blocker)) {
return true; return true;
} }
@ -231,24 +267,31 @@ export default class RoomView extends LoggedView {
return false; return false;
} }
renderItem = (item, previousItem) => ( renderItem = (item, previousItem) => {
<Message const { room } = this.state;
key={item._id} const { user } = this.props;
item={item}
status={item.status} return (
reactions={JSON.parse(JSON.stringify(item.reactions))} <Message
user={this.props.user} key={item._id}
archived={this.state.room.archived} item={item}
broadcast={this.state.room.broadcast} status={item.status}
previousItem={previousItem} reactions={JSON.parse(JSON.stringify(item.reactions))}
_updatedAt={item._updatedAt} user={user}
onReactionPress={this.onReactionPress} archived={room.archived}
onLongPress={this.onMessageLongPress} broadcast={room.broadcast}
/> previousItem={previousItem}
); _updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
/>
);
}
renderFooter = () => { renderFooter = () => {
if (!this.state.joined) { const { joined, room } = this.state;
if (!joined) {
return ( return (
<View style={styles.joinRoomContainer} key='room-view-join'> <View style={styles.joinRoomContainer} key='room-view-join'>
<Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text> <Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text>
@ -263,7 +306,7 @@ export default class RoomView extends LoggedView {
</View> </View>
); );
} }
if (this.state.room.archived || this.isReadOnly()) { if (room.archived || this.isReadOnly()) {
return ( return (
<View style={styles.readOnly}> <View style={styles.readOnly}>
<Text>{I18n.t('This_room_is_read_only')}</Text> <Text>{I18n.t('This_room_is_read_only')}</Text>
@ -281,21 +324,23 @@ export default class RoomView extends LoggedView {
}; };
renderHeader = () => { renderHeader = () => {
if (!this.state.end) { const { end } = this.state;
if (!end) {
return <ActivityIndicator style={[styles.loading, { transform: [{ scaleY: -1 }] }]} />; return <ActivityIndicator style={[styles.loading, { transform: [{ scaleY: -1 }] }]} />;
} }
return null; return null;
} }
renderList = () => { renderList = () => {
if (!this.state.loaded) { const { loaded, end } = this.state;
if (!loaded) {
return <ActivityIndicator style={styles.loading} />; return <ActivityIndicator style={styles.loading} />;
} }
return ( return (
[ [
<List <List
key='room-view-messages' key='room-view-messages'
end={this.state.end} end={end}
room={this.rid} room={this.rid}
renderFooter={this.renderHeader} renderFooter={this.renderHeader}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
@ -307,13 +352,17 @@ export default class RoomView extends LoggedView {
} }
render() { render() {
const { room } = this.state;
const { user, showActions, showErrorActions } = this.props;
return ( return (
<SafeAreaView style={styles.container} testID='room-view'> <SafeAreaView style={styles.container} testID='room-view'>
{this.renderList()} {this.renderList()}
{this.state.room._id && this.props.showActions ? {room._id && showActions
<MessageActions room={this.state.room} user={this.props.user} /> : ? <MessageActions room={room} user={user} />
null} : null
{this.props.showErrorActions ? <MessageErrorActions /> : null} }
{showErrorActions ? <MessageErrorActions /> : null}
<ReactionPicker onEmojiSelected={this.onReactionPress} /> <ReactionPicker onEmojiSelected={this.onReactionPress} />
<UploadProgress rid={this.rid} /> <UploadProgress rid={this.rid} />
</SafeAreaView> </SafeAreaView>

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Text, View, TouchableOpacity, Image, StyleSheet } from 'react-native'; import {
Text, View, TouchableOpacity, Image, StyleSheet
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Text, View, TouchableOpacity, Image, StyleSheet } from 'react-native'; import {
Text, View, TouchableOpacity, Image, StyleSheet
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import I18n from '../../../i18n'; import I18n from '../../../i18n';

View File

@ -3,12 +3,12 @@ import { View, TextInput } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { setSearch } from '../../../actions/rooms'; import { setSearch as setSearchAction } from '../../../actions/rooms';
import styles from './styles'; import styles from './styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
@connect(null, dispatch => ({ @connect(null, dispatch => ({
setSearch: searchText => dispatch(setSearch(searchText)) setSearch: searchText => dispatch(setSearchAction(searchText))
})) }))
export default class RoomsListSearchView extends React.Component { export default class RoomsListSearchView extends React.Component {
static propTypes = { static propTypes = {
@ -20,7 +20,8 @@ export default class RoomsListSearchView extends React.Component {
} }
onSearchChangeText(text) { onSearchChangeText(text) {
this.props.setSearch(text.trim()); const { setSearch } = this.props;
setSearch(text.trim());
} }
render() { render() {

View File

@ -1,12 +1,14 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View, Text, Animated, Easing, TouchableWithoutFeedback, TouchableOpacity, FlatList, Image, AsyncStorage } from 'react-native'; import {
View, Text, Animated, Easing, TouchableWithoutFeedback, TouchableOpacity, FlatList, Image, AsyncStorage
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
import { appStart as appStartAction } from '../../actions';
import styles from './styles'; import styles from './styles';
import { toggleServerDropdown } from '../../actions/rooms';
import { selectServerRequest } from '../../actions/server';
import { appStart } from '../../actions';
import database from '../../lib/realm'; import database from '../../lib/realm';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@ -19,9 +21,9 @@ const ANIMATION_DURATION = 200;
closeServerDropdown: state.rooms.closeServerDropdown, closeServerDropdown: state.rooms.closeServerDropdown,
server: state.server.server server: state.server.server
}), dispatch => ({ }), dispatch => ({
toggleServerDropdown: () => dispatch(toggleServerDropdown()), toggleServerDropdown: () => dispatch(toggleServerDropdownAction()),
selectServerRequest: server => dispatch(selectServerRequest(server)), selectServerRequest: server => dispatch(selectServerRequestAction(server)),
appStart: () => dispatch(appStart('outside')) appStart: () => dispatch(appStartAction('outside'))
})) }))
export default class ServerDropdown extends Component { export default class ServerDropdown extends Component {
static propTypes = { static propTypes = {
@ -56,7 +58,8 @@ export default class ServerDropdown extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.closeServerDropdown !== this.props.closeServerDropdown) { const { closeServerDropdown } = this.props;
if (prevProps.closeServerDropdown !== closeServerDropdown) {
this.close(); this.close();
} }
} }
@ -67,6 +70,7 @@ export default class ServerDropdown extends Component {
} }
close = () => { close = () => {
const { toggleServerDropdown } = this.props;
Animated.timing( Animated.timing(
this.animatedValue, this.animatedValue,
{ {
@ -75,16 +79,18 @@ export default class ServerDropdown extends Component {
easing: Easing.ease, easing: Easing.ease,
useNativeDriver: true useNativeDriver: true
} }
).start(() => this.props.toggleServerDropdown()); ).start(() => toggleServerDropdown());
} }
addServer = () => { addServer = () => {
const { navigator, server } = this.props;
this.close(); this.close();
setTimeout(() => { setTimeout(() => {
this.props.navigator.showModal({ navigator.showModal({
screen: 'OnboardingView', screen: 'OnboardingView',
passProps: { passProps: {
previousServer: this.props.server previousServer: server
}, },
navigatorStyle: { navigatorStyle: {
navBarHidden: true, navBarHidden: true,
@ -95,14 +101,18 @@ export default class ServerDropdown extends Component {
} }
select = async(server) => { select = async(server) => {
const {
server: serverProp, selectServerRequest, appStart, navigator
} = this.props;
this.close(); this.close();
if (this.props.server !== server) { if (serverProp !== server) {
this.props.selectServerRequest(server); selectServerRequest(server);
const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (!token) { if (!token) {
this.props.appStart(); appStart();
setTimeout(() => { setTimeout(() => {
this.props.navigator.push({ navigator.push({
screen: 'NewServerView', screen: 'NewServerView',
backButtonTitle: '', backButtonTitle: '',
passProps: { passProps: {
@ -119,32 +129,41 @@ export default class ServerDropdown extends Component {
renderSeparator = () => <View style={styles.serverSeparator} />; renderSeparator = () => <View style={styles.serverSeparator} />;
renderServer = ({ item }) => ( renderServer = ({ item }) => {
<Touch onPress={() => this.select(item.id)} style={styles.serverItem} testID={`rooms-list-header-server-${ item.id }`}> const { server } = this.props;
<View style={styles.serverItemContainer}>
{item.iconURL ? return (
<Image <Touch onPress={() => this.select(item.id)} style={styles.serverItem} testID={`rooms-list-header-server-${ item.id }`}>
source={{ uri: item.iconURL }} <View style={styles.serverItemContainer}>
defaultSource={{ uri: 'logo' }} {item.iconURL
style={styles.serverIcon} ? (
/> : <Image
<Image source={{ uri: item.iconURL }}
source={{ uri: 'logo' }} defaultSource={{ uri: 'logo' }}
style={styles.serverIcon} style={styles.serverIcon}
/> />
} )
<View style={styles.serverTextContainer}> : (
<Text style={styles.serverName}>{item.name || item.id}</Text> <Image
<Text style={styles.serverUrl}>{item.id}</Text> source={{ uri: 'logo' }}
style={styles.serverIcon}
/>
)
}
<View style={styles.serverTextContainer}>
<Text style={styles.serverName}>{item.name || item.id}</Text>
<Text style={styles.serverUrl}>{item.id}</Text>
</View>
{item.id === server ? <Image style={styles.checkIcon} source={{ uri: 'check' }} /> : null}
</View> </View>
{item.id === this.props.server ? <Image style={styles.checkIcon} source={{ uri: 'check' }} /> : null} </Touch>
</View> );
</Touch> }
)
render() { render() {
const { servers } = this.state;
const maxRows = 4; const maxRows = 4;
const initialTop = 41 + (Math.min(this.state.servers.length, maxRows) * ROW_HEIGHT); const initialTop = 41 + (Math.min(servers.length, maxRows) * ROW_HEIGHT);
const translateY = this.animatedValue.interpolate({ const translateY = this.animatedValue.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [-initialTop, 0] outputRange: [-initialTop, 0]
@ -171,7 +190,7 @@ export default class ServerDropdown extends Component {
</View> </View>
<FlatList <FlatList
style={{ maxHeight: maxRows * ROW_HEIGHT }} style={{ maxHeight: maxRows * ROW_HEIGHT }}
data={this.state.servers} data={servers}
keyExtractor={item => item.id} keyExtractor={item => item.id}
renderItem={this.renderServer} renderItem={this.renderServer}
ItemSeparatorComponent={this.renderSeparator} ItemSeparatorComponent={this.renderSeparator}

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View, Text, Animated, Easing, Image, TouchableWithoutFeedback } from 'react-native'; import {
View, Text, Animated, Easing, Image, TouchableWithoutFeedback
} from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -46,14 +48,17 @@ export default class Sort extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.closeSortDropdown !== this.props.closeSortDropdown) { const { closeSortDropdown } = this.props;
if (prevProps.closeSortDropdown !== closeSortDropdown) {
this.close(); this.close();
} }
} }
setSortPreference = async(param) => { setSortPreference = async(param) => {
const { setSortPreference } = this.props;
try { try {
this.props.setSortPreference(param); setSortPreference(param);
RocketChat.saveSortPreference(param); RocketChat.saveSortPreference(param);
} catch (e) { } catch (e) {
log('RoomsListView.setSortPreference', e); log('RoomsListView.setSortPreference', e);
@ -69,18 +74,22 @@ export default class Sort extends Component {
} }
toggleGroupByType = () => { toggleGroupByType = () => {
this.setSortPreference({ groupByType: !this.props.groupByType }); const { groupByType } = this.props;
this.setSortPreference({ groupByType: !groupByType });
} }
toggleGroupByFavorites = () => { toggleGroupByFavorites = () => {
this.setSortPreference({ showFavorites: !this.props.showFavorites }); const { showFavorites } = this.props;
this.setSortPreference({ showFavorites: !showFavorites });
} }
toggleUnread = () => { toggleUnread = () => {
this.setSortPreference({ showUnread: !this.props.showUnread }); const { showUnread } = this.props;
this.setSortPreference({ showUnread: !showUnread });
} }
close = () => { close = () => {
const { close } = this.props;
Animated.timing( Animated.timing(
this.animatedValue, this.animatedValue,
{ {
@ -89,7 +98,7 @@ export default class Sort extends Component {
easing: Easing.ease, easing: Easing.ease,
useNativeDriver: true useNativeDriver: true
}, },
).start(() => this.props.close()); ).start(() => close());
} }
render() { render() {
@ -104,6 +113,7 @@ export default class Sort extends Component {
const { const {
sortBy, groupByType, showFavorites, showUnread sortBy, groupByType, showFavorites, showUnread
} = this.props; } = this.props;
return ( return (
[ [
<TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}> <TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}>
@ -156,7 +166,7 @@ export default class Sort extends Component {
style={[styles.dropdownContainerHeader, styles.sortToggleContainerClose]} style={[styles.dropdownContainerHeader, styles.sortToggleContainerClose]}
> >
<View style={styles.sortItemContainer}> <View style={styles.sortItemContainer}>
<Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(this.props.sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> <Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text>
<Image style={styles.sortIcon} source={{ uri: 'group_type' }} /> <Image style={styles.sortIcon} source={{ uri: 'group_type' }} />
</View> </View>
</Touch> </Touch>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Platform, View, FlatList, BackHandler, ActivityIndicator, SafeAreaView, Text, Image, Dimensions, ScrollView, Keyboard } from 'react-native'; import {
Platform, View, FlatList, BackHandler, ActivityIndicator, SafeAreaView, Text, Image, Dimensions, ScrollView, Keyboard
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -15,7 +17,7 @@ import I18n from '../../i18n';
import SortDropdown from './SortDropdown'; import SortDropdown from './SortDropdown';
import ServerDropdown from './ServerDropdown'; import ServerDropdown from './ServerDropdown';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { toggleSortDropdown } from '../../actions/rooms'; import { toggleSortDropdown as toggleSortDropdownAction } from '../../actions/rooms';
const ROW_HEIGHT = 70; const ROW_HEIGHT = 70;
const SCROLL_OFFSET = 56; const SCROLL_OFFSET = 56;
@ -54,7 +56,7 @@ if (Platform.OS === 'android') {
showUnread: state.sortPreferences.showUnread, showUnread: state.sortPreferences.showUnread,
useRealName: state.settings.UI_Use_Real_Name useRealName: state.settings.UI_Use_Real_Name
}), dispatch => ({ }), dispatch => ({
toggleSortDropdown: () => dispatch(toggleSortDropdown()) toggleSortDropdown: () => dispatch(toggleSortDropdownAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomsListView extends LoggedView { export default class RoomsListView extends LoggedView {
@ -114,13 +116,15 @@ export default class RoomsListView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.server && this.props.loadingServer !== nextProps.loadingServer) { const { loadingServer, searchText } = this.props;
if (nextProps.server && loadingServer !== nextProps.loadingServer) {
if (nextProps.loadingServer) { if (nextProps.loadingServer) {
this.setState({ loading: true }); this.setState({ loading: true });
} else { } else {
this.getSubscriptions(); this.getSubscriptions();
} }
} else if (this.props.searchText !== nextProps.searchText) { } else if (searchText !== nextProps.searchText) {
this.search(nextProps.searchText); this.search(nextProps.searchText);
} }
} }
@ -130,11 +134,15 @@ export default class RoomsListView extends LoggedView {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const {
sortBy, groupByType, showFavorites, showUnread
} = this.props;
if (!( if (!(
(prevProps.sortBy === this.props.sortBy) && (prevProps.sortBy === sortBy)
(prevProps.groupByType === this.props.groupByType) && && (prevProps.groupByType === groupByType)
(prevProps.showFavorites === this.props.showFavorites) && && (prevProps.showFavorites === showFavorites)
(prevProps.showUnread === this.props.showUnread) && (prevProps.showUnread === showUnread)
)) { )) {
this.getSubscriptions(); this.getSubscriptions();
} }
@ -158,7 +166,7 @@ export default class RoomsListView extends LoggedView {
const { navigator } = this.props; const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'newMessage') { if (event.id === 'newMessage') {
this.props.navigator.showModal({ navigator.showModal({
screen: 'NewMessageView', screen: 'NewMessageView',
title: I18n.t('New_Message'), title: I18n.t('New_Message'),
passProps: { passProps: {
@ -183,8 +191,12 @@ export default class RoomsListView extends LoggedView {
} }
getSubscriptions = () => { getSubscriptions = () => {
if (this.props.server && this.hasActiveDB()) { const {
if (this.props.sortBy === 'alphabetical') { server, sortBy, showUnread, showFavorites, groupByType
} = this.props;
if (server && this.hasActiveDB()) {
if (sortBy === 'alphabetical') {
this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('name', false); this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('name', false);
} else { } else {
this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('roomUpdatedAt', true); this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('roomUpdatedAt', true);
@ -199,7 +211,7 @@ export default class RoomsListView extends LoggedView {
let livechat = []; let livechat = [];
// unread // unread
if (this.props.showUnread) { if (showUnread) {
this.unread = this.data.filtered('archived != true && open == true').sorted('name', false).filtered('(unread > 0 || alert == true)'); this.unread = this.data.filtered('archived != true && open == true').sorted('name', false).filtered('(unread > 0 || alert == true)');
unread = this.unread.slice(); unread = this.unread.slice();
setTimeout(() => { setTimeout(() => {
@ -209,7 +221,7 @@ export default class RoomsListView extends LoggedView {
this.removeListener(unread); this.removeListener(unread);
} }
// favorites // favorites
if (this.props.showFavorites) { if (showFavorites) {
this.favorites = this.data.filtered('f == true'); this.favorites = this.data.filtered('f == true');
favorites = this.favorites.slice(); favorites = this.favorites.slice();
setTimeout(() => { setTimeout(() => {
@ -219,7 +231,7 @@ export default class RoomsListView extends LoggedView {
this.removeListener(favorites); this.removeListener(favorites);
} }
// type // type
if (this.props.groupByType) { if (groupByType) {
// channels // channels
this.channels = this.data.filtered('t == $0', 'c'); this.channels = this.data.filtered('t == $0', 'c');
channels = this.channels.slice(); channels = this.channels.slice();
@ -241,7 +253,7 @@ export default class RoomsListView extends LoggedView {
this.removeListener(this.chats); this.removeListener(this.chats);
} else { } else {
// chats // chats
if (this.props.showUnread) { if (showUnread) {
this.chats = this.data.filtered('(unread == 0 && alert == false)'); this.chats = this.data.filtered('(unread == 0 && alert == false)');
} else { } else {
this.chats = this.data; this.chats = this.data;
@ -327,7 +339,8 @@ export default class RoomsListView extends LoggedView {
} }
goRoom = (rid, name) => { goRoom = (rid, name) => {
this.props.navigator.push({ const { navigator } = this.props;
navigator.push({
screen: 'RoomView', screen: 'RoomView',
title: name, title: name,
backButtonTitle: '', backButtonTitle: '',
@ -358,34 +371,41 @@ export default class RoomsListView extends LoggedView {
} }
toggleSort = () => { toggleSort = () => {
const { toggleSortDropdown } = this.props;
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
this.scroll.scrollTo({ x: 0, y: SCROLL_OFFSET, animated: true }); this.scroll.scrollTo({ x: 0, y: SCROLL_OFFSET, animated: true });
} else { } else {
this.scroll.scrollTo({ x: 0, y: 0, animated: true }); this.scroll.scrollTo({ x: 0, y: 0, animated: true });
} }
setTimeout(() => { setTimeout(() => {
this.props.toggleSortDropdown(); toggleSortDropdown();
}, 100); }, 100);
} }
renderHeader = () => { renderHeader = () => {
if (this.state.search.length > 0) { const { search } = this.state;
if (search.length > 0) {
return null; return null;
} }
return this.renderSort(); return this.renderSort();
} }
renderSort = () => ( renderSort = () => {
<Touch const { sortBy } = this.props;
onPress={this.toggleSort}
style={styles.dropdownContainerHeader} return (
> <Touch
<View style={styles.sortItemContainer}> onPress={this.toggleSort}
<Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(this.props.sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> style={styles.dropdownContainerHeader}
<Image style={styles.sortIcon} source={{ uri: 'group_type' }} /> >
</View> <View style={styles.sortItemContainer}>
</Touch> <Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text>
) <Image style={styles.sortIcon} source={{ uri: 'group_type' }} />
</View>
</Touch>
);
}
renderSearchBar = () => { renderSearchBar = () => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
@ -394,36 +414,41 @@ export default class RoomsListView extends LoggedView {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const id = item.rid.replace(this.props.userId, '').trim(); const { useRealName, userId, baseUrl } = this.props;
const { useRealName } = this.props; const id = item.rid.replace(userId, '').trim();
return (<RoomItem
alert={item.alert} return (
unread={item.unread} <RoomItem
userMentions={item.userMentions} alert={item.alert}
favorite={item.f} unread={item.unread}
lastMessage={item.lastMessage} userMentions={item.userMentions}
name={(useRealName && item.fname) || item.name} favorite={item.f}
_updatedAt={item.roomUpdatedAt} lastMessage={item.lastMessage}
key={item._id} name={(useRealName && item.fname) || item.name}
id={id} _updatedAt={item.roomUpdatedAt}
type={item.t} key={item._id}
baseUrl={this.props.baseUrl} id={id}
onPress={() => this._onPressItem(item)} type={item.t}
testID={`rooms-list-view-item-${ item.name }`} baseUrl={baseUrl}
height={ROW_HEIGHT} onPress={() => this._onPressItem(item)}
/>); testID={`rooms-list-view-item-${ item.name }`}
height={ROW_HEIGHT}
/>
);
} }
renderSeparator = () => <View style={styles.separator} />; renderSeparator = () => <View style={styles.separator} />;
renderSection = (data, header) => { renderSection = (data, header) => {
if (header === 'Unread' && !this.props.showUnread) { const { showUnread, showFavorites, groupByType } = this.props;
if (header === 'Unread' && !showUnread) {
return null; return null;
} else if (header === 'Favorites' && !this.props.showFavorites) { } else if (header === 'Favorites' && !showFavorites) {
return null; return null;
} else if (['Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !this.props.groupByType) { } else if (['Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !groupByType) {
return null; return null;
} else if (header === 'Chats' && this.props.groupByType) { } else if (header === 'Chats' && groupByType) {
return null; return null;
} }
if (data.length > 0) { if (data.length > 0) {
@ -486,7 +511,9 @@ export default class RoomsListView extends LoggedView {
} }
renderScroll = () => { renderScroll = () => {
if (this.state.loading) { const { loading } = this.state;
if (loading) {
return <ActivityIndicator style={styles.loading} />; return <ActivityIndicator style={styles.loading} />;
} }
@ -506,22 +533,25 @@ export default class RoomsListView extends LoggedView {
render = () => { render = () => {
const { const {
sortBy, groupByType, showFavorites, showUnread, showServerDropdown, showSortDropdown sortBy, groupByType, showFavorites, showUnread, showServerDropdown, showSortDropdown, navigator
} = this.props; } = this.props;
return ( return (
<SafeAreaView style={styles.container} testID='rooms-list-view'> <SafeAreaView style={styles.container} testID='rooms-list-view'>
{this.renderScroll()} {this.renderScroll()}
{showSortDropdown ? {showSortDropdown
<SortDropdown ? (
close={this.toggleSort} <SortDropdown
sortBy={sortBy} close={this.toggleSort}
groupByType={groupByType} sortBy={sortBy}
showFavorites={showFavorites} groupByType={groupByType}
showUnread={showUnread} showFavorites={showFavorites}
/> : showUnread={showUnread}
null} />
{showServerDropdown ? <ServerDropdown navigator={this.props.navigator} /> : null} )
: null
}
{showServerDropdown ? <ServerDropdown navigator={navigator} /> : null}
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@ -52,22 +52,26 @@ export default class SearchMessagesView extends LoggedView {
} }
onChangeSearch = debounce((search) => { onChangeSearch = debounce((search) => {
const { searching } = this.state;
this.searchText = search; this.searchText = search;
this.limit = 0; this.limit = 0;
if (!this.state.searching) { if (!searching) {
this.setState({ searching: true }); this.setState({ searching: true });
} }
this.search(); this.search();
}, 1000) }, 1000)
search = async() => { search = async() => {
const { rid } = this.props;
if (this._cancel) { if (this._cancel) {
this._cancel('cancel'); this._cancel('cancel');
} }
const cancel = new Promise((r, reject) => this._cancel = reject); const cancel = new Promise((r, reject) => this._cancel = reject);
let messages = []; let messages = [];
try { try {
const result = await Promise.race([RocketChat.messageSearch(this.searchText, this.props.rid, this.limit), cancel]); const result = await Promise.race([RocketChat.messageSearch(this.searchText, rid, this.limit), cancel]);
messages = result.message.docs.map(message => buildMessage(message)); messages = result.message.docs.map(message => buildMessage(message));
this.setState({ messages, searching: false, loadingMore: false }); this.setState({ messages, searching: false, loadingMore: false });
} catch (e) { } catch (e) {
@ -91,27 +95,30 @@ export default class SearchMessagesView extends LoggedView {
} }
} }
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item} return (
style={styles.message} <Message
reactions={item.reactions} item={item}
user={this.props.user} style={styles.message}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' reactions={item.reactions}
onReactionPress={async(emoji) => { user={user}
try { customTimeFormat='MMMM Do YYYY, h:mm:ss a'
await RocketChat.setReaction(emoji, item._id); onReactionPress={async(emoji) => {
this.search(); try {
this.forceUpdate(); await RocketChat.setReaction(emoji, item._id);
} catch (e) { this.search();
log('SearchMessagesView.onReactionPress', e); this.forceUpdate();
} } catch (e) {
}} log('SearchMessagesView.onReactionPress', e);
/> }
); }}
/>
);
}
render() { render() {
const { searching, loadingMore } = this.state; const { searching, loadingMore, messages } = this.state;
return ( return (
<SafeAreaView style={styles.container} testID='search-messages-view'> <SafeAreaView style={styles.container} testID='search-messages-view'>
<View style={styles.searchContainer}> <View style={styles.searchContainer}>
@ -126,7 +133,7 @@ export default class SearchMessagesView extends LoggedView {
<View style={styles.divider} /> <View style={styles.divider} />
</View> </View>
<FlatList <FlatList
data={this.state.messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
keyExtractor={item => item._id} keyExtractor={item => item._id}

View File

@ -1,9 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, StyleSheet, SafeAreaView, FlatList, LayoutAnimation, Platform } from 'react-native'; import {
View, StyleSheet, SafeAreaView, FlatList, LayoutAnimation, Platform
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { addUser, removeUser, reset, setLoading } from '../actions/selectedUsers'; import {
addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction
} from '../actions/selectedUsers';
import database from '../lib/realm'; import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
@ -33,10 +37,10 @@ const styles = StyleSheet.create({
users: state.selectedUsers.users, users: state.selectedUsers.users,
loading: state.selectedUsers.loading loading: state.selectedUsers.loading
}), dispatch => ({ }), dispatch => ({
addUser: user => dispatch(addUser(user)), addUser: user => dispatch(addUserAction(user)),
removeUser: user => dispatch(removeUser(user)), removeUser: user => dispatch(removeUserAction(user)),
reset: () => dispatch(reset()), reset: () => dispatch(resetAction()),
setLoadingInvite: loading => dispatch(setLoading(loading)) setLoadingInvite: loading => dispatch(setLoadingAction(loading))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class SelectedUsersView extends LoggedView { export default class SelectedUsersView extends LoggedView {
@ -64,19 +68,22 @@ export default class SelectedUsersView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
this.props.navigator.setDrawerEnabled({ const { navigator } = this.props;
navigator.setDrawerEnabled({
side: 'left', side: 'left',
enabled: false enabled: false
}); });
} }
async componentDidUpdate(prevProps) { async componentDidUpdate(prevProps) {
const isVisible = await this.props.navigator.screenIsCurrentlyVisible(); const { navigator, users } = this.props;
const isVisible = await navigator.screenIsCurrentlyVisible();
if (!isVisible) { if (!isVisible) {
return; return;
} }
if (prevProps.users.length !== this.props.users.length) { if (prevProps.users.length !== users.length) {
const { length } = this.props.users; const { length } = users;
const rightButtons = []; const rightButtons = [];
if (length > 0) { if (length > 0) {
rightButtons.push({ rightButtons.push({
@ -85,14 +92,15 @@ export default class SelectedUsersView extends LoggedView {
testID: 'selected-users-view-submit' testID: 'selected-users-view-submit'
}); });
} }
this.props.navigator.setButtons({ rightButtons }); navigator.setButtons({ rightButtons });
} }
} }
componentWillUnmount() { componentWillUnmount() {
const { reset } = this.props;
this.updateState.stop(); this.updateState.stop();
this.data.removeAllListeners(); this.data.removeAllListeners();
this.props.reset(); reset();
} }
async onNavigatorEvent(event) { async onNavigatorEvent(event) {
@ -100,15 +108,16 @@ export default class SelectedUsersView extends LoggedView {
if (event.id === 'create') { if (event.id === 'create') {
const { nextAction, setLoadingInvite, navigator } = this.props; const { nextAction, setLoadingInvite, navigator } = this.props;
if (nextAction === 'CREATE_CHANNEL') { if (nextAction === 'CREATE_CHANNEL') {
this.props.navigator.push({ navigator.push({
screen: 'CreateChannelView', screen: 'CreateChannelView',
title: I18n.t('Create_Channel'), title: I18n.t('Create_Channel'),
backButtonTitle: '' backButtonTitle: ''
}); });
} else { } else {
const { rid } = this.props;
try { try {
setLoadingInvite(true); setLoadingInvite(true);
await RocketChat.addUsersToRoom(this.props.rid); await RocketChat.addUsersToRoom(rid);
navigator.pop(); navigator.pop();
} catch (e) { } catch (e) {
log('RoomActions Add User', e); log('RoomActions Add User', e);
@ -124,6 +133,7 @@ export default class SelectedUsersView extends LoggedView {
this.search(text); this.search(text);
} }
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => { updateState = debounce(() => {
this.forceUpdate(); this.forceUpdate();
}, 1000); }, 1000);
@ -135,14 +145,19 @@ export default class SelectedUsersView extends LoggedView {
}); });
} }
isChecked = username => this.props.users.findIndex(el => el.name === username) !== -1; isChecked = (username) => {
const { users } = this.props;
return users.findIndex(el => el.name === username) !== -1;
}
toggleUser = (user) => { toggleUser = (user) => {
const { addUser, removeUser } = this.props;
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
if (!this.isChecked(user.name)) { if (!this.isChecked(user.name)) {
this.props.addUser(user); addUser(user);
} else { } else {
this.props.removeUser(user); removeUser(user);
} }
} }
@ -164,12 +179,14 @@ export default class SelectedUsersView extends LoggedView {
) )
renderSelected = () => { renderSelected = () => {
if (this.props.users.length === 0) { const { users } = this.props;
if (users.length === 0) {
return null; return null;
} }
return ( return (
<FlatList <FlatList
data={this.props.users} data={users}
keyExtractor={item => item._id} keyExtractor={item => item._id}
style={[styles.list, sharedStyles.separatorTop]} style={[styles.list, sharedStyles.separatorTop]}
contentContainerStyle={{ marginVertical: 5 }} contentContainerStyle={{ marginVertical: 5 }}
@ -181,30 +198,36 @@ export default class SelectedUsersView extends LoggedView {
); );
} }
renderSelectedItem = ({ item }) => ( renderSelectedItem = ({ item }) => {
<UserItem const { baseUrl } = this.props;
name={item.fname} return (
username={item.name} <UserItem
onPress={() => this._onPressSelectedItem(item)} name={item.fname}
testID={`selected-user-${ item.name }`} username={item.name}
baseUrl={this.props.baseUrl} onPress={() => this._onPressSelectedItem(item)}
style={{ paddingRight: 15 }} testID={`selected-user-${ item.name }`}
/> baseUrl={baseUrl}
) style={{ paddingRight: 15 }}
/>
);
}
renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />
renderItem = ({ item, index }) => { renderItem = ({ item, index }) => {
const { search } = this.state;
const { baseUrl } = this.props;
const name = item.search ? item.name : item.fname; const name = item.search ? item.name : item.fname;
const username = item.search ? item.username : item.name; const username = item.search ? item.username : item.name;
let style = {}; let style = {};
if (index === 0) { if (index === 0) {
style = { ...sharedStyles.separatorTop }; style = { ...sharedStyles.separatorTop };
} }
if (this.state.search.length > 0 && index === this.state.search.length - 1) { if (search.length > 0 && index === search.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
if (this.state.search.length === 0 && index === this.data.length - 1) { if (search.length === 0 && index === this.data.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
return ( return (
@ -214,29 +237,35 @@ export default class SelectedUsersView extends LoggedView {
onPress={() => this._onPressItem(item._id, item)} onPress={() => this._onPressItem(item._id, item)}
testID={`select-users-view-item-${ item.name }`} testID={`select-users-view-item-${ item.name }`}
icon={this.isChecked(username) ? 'check' : null} icon={this.isChecked(username) ? 'check' : null}
baseUrl={this.props.baseUrl} baseUrl={baseUrl}
style={style} style={style}
/> />
); );
} }
renderList = () => ( renderList = () => {
<FlatList const { search } = this.state;
data={this.state.search.length > 0 ? this.state.search : this.data} return (
extraData={this.props} <FlatList
keyExtractor={item => item._id} data={search.length > 0 ? search : this.data}
renderItem={this.renderItem} extraData={this.props}
ItemSeparatorComponent={this.renderSeparator} keyExtractor={item => item._id}
ListHeaderComponent={this.renderHeader} renderItem={this.renderItem}
enableEmptySections ItemSeparatorComponent={this.renderSeparator}
keyboardShouldPersistTaps='always' ListHeaderComponent={this.renderHeader}
/> enableEmptySections
) keyboardShouldPersistTaps='always'
/>
);
}
render = () => ( render = () => {
<SafeAreaView style={styles.safeAreaView} testID='select-users-view'> const { loading } = this.props;
{this.renderList()} return (
<Loading visible={this.props.loading} /> <SafeAreaView style={styles.safeAreaView} testID='select-users-view'>
</SafeAreaView> {this.renderList()}
) <Loading visible={loading} />
</SafeAreaView>
);
}
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, ScrollView, SafeAreaView, Dimensions } from 'react-native'; import {
View, ScrollView, SafeAreaView, Dimensions
} from 'react-native';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -15,12 +17,12 @@ import Button from '../../containers/Button';
import Loading from '../../containers/Loading'; import Loading from '../../containers/Loading';
import { showErrorAlert, showToast } from '../../utils/info'; import { showErrorAlert, showToast } from '../../utils/info';
import log from '../../utils/log'; import log from '../../utils/log';
import { setUser } from '../../actions/login'; import { setUser as setUserAction } from '../../actions/login';
@connect(state => ({ @connect(state => ({
userLanguage: state.login.user && state.login.user.language userLanguage: state.login.user && state.login.user.language
}), dispatch => ({ }), dispatch => ({
setUser: params => dispatch(setUser(params)) setUser: params => dispatch(setUserAction(params))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class SettingsView extends LoggedView { export default class SettingsView extends LoggedView {
@ -51,7 +53,8 @@ export default class SettingsView extends LoggedView {
} }
componentWillMount() { componentWillMount() {
this.props.navigator.setButtons({ const { navigator } = this.props;
navigator.setButtons({
leftButtons: [{ leftButtons: [{
id: 'settings', id: 'settings',
icon: { uri: 'settings', scale: Dimensions.get('window').scale } icon: { uri: 'settings', scale: Dimensions.get('window').scale }
@ -60,16 +63,18 @@ export default class SettingsView extends LoggedView {
} }
componentDidMount() { componentDidMount() {
this.props.navigator.setDrawerEnabled({ const { navigator } = this.props;
navigator.setDrawerEnabled({
side: 'left', side: 'left',
enabled: true enabled: true
}); });
} }
onNavigatorEvent(event) { onNavigatorEvent(event) {
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') { if (event.type === 'NavBarButtonPress') {
if (event.id === 'settings') { if (event.id === 'settings') {
this.props.navigator.toggleDrawer({ navigator.toggleDrawer({
side: 'left' side: 'left'
}); });
} }
@ -86,17 +91,16 @@ export default class SettingsView extends LoggedView {
} }
formIsChanged = () => { formIsChanged = () => {
const { userLanguage } = this.props;
const { language } = this.state; const { language } = this.state;
return !(this.props.userLanguage === language); return !(userLanguage === language);
} }
submit = async() => { submit = async() => {
this.setState({ saving: true }); this.setState({ saving: true });
const { const { language } = this.state;
language const { userLanguage, setUser, navigator } = this.props;
} = this.state;
const { userLanguage } = this.props;
if (!this.formIsChanged()) { if (!this.formIsChanged()) {
return; return;
@ -111,14 +115,14 @@ export default class SettingsView extends LoggedView {
try { try {
await RocketChat.saveUserPreferences(params); await RocketChat.saveUserPreferences(params);
this.props.setUser({ language: params.language }); setUser({ language: params.language });
this.setState({ saving: false }); this.setState({ saving: false });
setTimeout(() => { setTimeout(() => {
showToast(I18n.t('Preferences_saved')); showToast(I18n.t('Preferences_saved'));
if (params.language) { if (params.language) {
this.props.navigator.setTitle({ title: I18n.t('Settings') }); navigator.setTitle({ title: I18n.t('Settings') });
} }
}, 300); }, 300);
} catch (e) { } catch (e) {
@ -134,7 +138,9 @@ export default class SettingsView extends LoggedView {
} }
render() { render() {
const { language, languages, placeholder } = this.state; const {
language, languages, placeholder, saving
} = this.state;
return ( return (
<KeyboardView <KeyboardView
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
@ -171,7 +177,7 @@ export default class SettingsView extends LoggedView {
testID='settings-view-button' testID='settings-view-button'
/> />
</View> </View>
<Loading visible={this.state.saving} /> <Loading visible={saving} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text, SafeAreaView } from 'react-native'; import {
FlatList, View, Text, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages';
import LoggedView from '../View'; import LoggedView from '../View';
import { openSnippetedMessages, closeSnippetedMessages } from '../../actions/snippetedMessages';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
@ -19,8 +21,8 @@ import I18n from '../../i18n';
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} }
}), dispatch => ({ }), dispatch => ({
openSnippetedMessages: (rid, limit) => dispatch(openSnippetedMessages(rid, limit)), openSnippetedMessages: (rid, limit) => dispatch(openSnippetedMessagesAction(rid, limit)),
closeSnippetedMessages: () => dispatch(closeSnippetedMessages()) closeSnippetedMessages: () => dispatch(closeSnippetedMessagesAction())
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class SnippetedMessagesView extends LoggedView { export default class SnippetedMessagesView extends LoggedView {
@ -47,17 +49,20 @@ export default class SnippetedMessagesView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.ready && nextProps.ready !== this.props.ready) { const { ready } = this.props;
if (nextProps.ready && nextProps.ready !== ready) {
this.setState({ loading: false, loadingMore: false }); this.setState({ loading: false, loadingMore: false });
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.closeSnippetedMessages(); const { closeSnippetedMessages } = this.props;
closeSnippetedMessages();
} }
load() { load = () => {
this.props.openSnippetedMessages(this.props.rid, this.limit); const { rid, openSnippetedMessages } = this.props;
openSnippetedMessages(rid, this.limit);
} }
moreData = () => { moreData = () => {
@ -79,15 +84,18 @@ export default class SnippetedMessagesView extends LoggedView {
</View> </View>
) )
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item} return (
style={styles.message} <Message
reactions={item.reactions} item={item}
user={this.props.user} style={styles.message}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' reactions={item.reactions}
/> user={user}
); customTimeFormat='MMMM Do YYYY, h:mm:ss a'
/>
);
}
render() { render() {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;

View File

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, View, Text, SafeAreaView } from 'react-native'; import {
FlatList, View, Text, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet'; import ActionSheet from 'react-native-actionsheet';
import { openStarredMessages as openStarredMessagesAction, closeStarredMessages as closeStarredMessagesAction } from '../../actions/starredMessages';
import { toggleStarRequest as toggleStarRequestAction } from '../../actions/messages';
import LoggedView from '../View'; import LoggedView from '../View';
import { openStarredMessages, closeStarredMessages } from '../../actions/starredMessages';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Message from '../../containers/message';
import { toggleStarRequest } from '../../actions/messages';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -25,9 +27,9 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')];
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
} }
}), dispatch => ({ }), dispatch => ({
openStarredMessages: (rid, limit) => dispatch(openStarredMessages(rid, limit)), openStarredMessages: (rid, limit) => dispatch(openStarredMessagesAction(rid, limit)),
closeStarredMessages: () => dispatch(closeStarredMessages()), closeStarredMessages: () => dispatch(closeStarredMessagesAction()),
toggleStarRequest: message => dispatch(toggleStarRequest(message)) toggleStarRequest: message => dispatch(toggleStarRequestAction(message))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class StarredMessagesView extends LoggedView { export default class StarredMessagesView extends LoggedView {
@ -56,13 +58,15 @@ export default class StarredMessagesView extends LoggedView {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.ready && nextProps.ready !== this.props.ready) { const { ready } = this.props;
if (nextProps.ready && nextProps.ready !== ready) {
this.setState({ loading: false, loadingMore: false }); this.setState({ loading: false, loadingMore: false });
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.closeStarredMessages(); const { closeStarredMessages } = this.props;
closeStarredMessages();
} }
onLongPress = (message) => { onLongPress = (message) => {
@ -73,9 +77,12 @@ export default class StarredMessagesView extends LoggedView {
} }
handleActionPress = (actionIndex) => { handleActionPress = (actionIndex) => {
const { message } = this.state;
const { toggleStarRequest } = this.props;
switch (actionIndex) { switch (actionIndex) {
case STAR_INDEX: case STAR_INDEX:
this.props.toggleStarRequest(this.state.message); toggleStarRequest(message);
break; break;
default: default:
break; break;
@ -83,7 +90,8 @@ export default class StarredMessagesView extends LoggedView {
} }
load = () => { load = () => {
this.props.openStarredMessages(this.props.rid, this.limit); const { rid, openStarredMessages } = this.props;
openStarredMessages(rid, this.limit);
} }
moreData = () => { moreData = () => {
@ -105,16 +113,19 @@ export default class StarredMessagesView extends LoggedView {
</View> </View>
) )
renderItem = ({ item }) => ( renderItem = ({ item }) => {
<Message const { user } = this.props;
item={item} return (
style={styles.message} <Message
reactions={item.reactions} item={item}
user={this.props.user} style={styles.message}
customTimeFormat='MMMM Do YYYY, h:mm:ss a' reactions={item.reactions}
onLongPress={this.onLongPress} user={user}
/> customTimeFormat='MMMM Do YYYY, h:mm:ss a'
) onLongPress={this.onLongPress}
/>
);
}
render() { render() {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;

View File

@ -1,6 +1,8 @@
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_TEXT, COLOR_SEPARATOR } from '../constants/colors'; import {
COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_TEXT, COLOR_SEPARATOR
} from '../constants/colors';
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {

Some files were not shown because too many files have changed in this diff Show More