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 }]],
"env": {
"production": {

View File

@ -37,7 +37,7 @@ jobs:
e2e-test:
macos:
xcode: "9.0"
xcode: "10.0.0"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
@ -87,7 +87,8 @@ jobs:
- image: circleci/android:api-27-node8-alpha
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
TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh"
@ -170,7 +171,7 @@ jobs:
ios-build:
macos:
xcode: "9.0"
xcode: "10.0.0"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
@ -236,7 +237,7 @@ jobs:
ios-testflight:
macos:
xcode: "9.0"
xcode: "10.0.0"
steps:
- checkout

View File

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

View File

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

View File

@ -300,7 +300,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
<View>
<View
accessibilityLabel="rocket.cat, last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -532,7 +531,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View>
<View
accessibilityLabel="rocket.cat, last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -768,7 +766,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View>
<View
accessibilityLabel="rocket.cat, 1 alert, last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -1026,7 +1023,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</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"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -1288,7 +1284,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</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"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -1546,7 +1541,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</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"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -1804,7 +1798,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</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"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -2062,7 +2055,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</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"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -2320,7 +2312,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View>
<View
accessibilityLabel="W, last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -2552,7 +2543,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View>
<View
accessibilityLabel="WW, last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
@ -2784,7 +2774,6 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
</View>
<View
accessibilityLabel=", last message Nov 10"
accessibilityTraits="selected"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}

View File

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

View File

@ -6,13 +6,11 @@ import android.os.Bundle;
import com.AlexanderZaytsev.RNI18n.RNI18nPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.balthazargronon.RCTZeroconf.ZeroconfReactPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.crashlytics.android.Crashlytics;
import com.dylanvann.fastimage.FastImageViewPackage;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.horcrux.svg.SvgPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.reactnativenavigation.NavigationApplication;
import com.remobile.toast.RCTToastPackage;
@ -62,10 +60,8 @@ public class MainApplication extends NavigationApplication implements INotificat
new RNDeviceInfo(),
new RNGestureHandlerPackage(),
new PickerPackage(),
new SvgPackage(),
new VectorIconsPackage(),
new RNFetchBlobPackage(),
new ZeroconfReactPackage(),
new RealmReactPackage(),
new ReactVideoPackage(),
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')
include ':react-native-video'
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'
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'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-navigation'

View File

@ -3,12 +3,17 @@ class NavigationActionsClass {
this.navigator = navigator;
}
push = params => this.navigator && this.navigator.push(params);
pop = params => this.navigator && this.navigator.pop(params);
popToRoot = params => this.navigator && this.navigator.popToRoot(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);
push = params => this.navigator && this.navigator.push(params)
pop = params => this.navigator && this.navigator.pop(params)
popToRoot = params => this.navigator && this.navigator.popToRoot(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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import React from 'react';
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 Touch from '../../utils/touch';
@ -92,9 +94,9 @@ export default class Button extends React.PureComponent {
]}
>
{
loading ?
<ActivityIndicator color={colors[`text_color_${ type }`]} /> :
<Text style={[styles.text, styles[`text_color_${ type }`]]}>{title}</Text>
loading
? <ActivityIndicator color={colors[`text_color_${ type }`]} />
: <Text style={[styles.text, styles[`text_color_${ type }`]]}>{title}</Text>
}
</View>
</Touch>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,20 @@
import React from 'react';
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 ActionSheet from 'react-native-actionsheet';
import * as moment from 'moment';
import {
deleteRequest,
editInit,
toggleStarRequest,
togglePinRequest,
actionsHide,
toggleReactionPicker,
replyInit
actionsHide as actionsHideAction,
deleteRequest as deleteRequestAction,
editInit as editInitAction,
replyInit as replyInitAction,
togglePinRequest as togglePinRequestAction,
toggleReactionPicker as toggleReactionPickerAction,
toggleStarRequest as toggleStarRequestAction
} from '../actions/messages';
import { showToast } from '../utils/info';
import RocketChat from '../lib/rocketchat';
@ -29,13 +31,13 @@ import I18n from '../i18n';
Message_AllowStarring: state.settings.Message_AllowStarring
}),
dispatch => ({
actionsHide: () => dispatch(actionsHide()),
deleteRequest: message => dispatch(deleteRequest(message)),
editInit: message => dispatch(editInit(message)),
toggleStarRequest: message => dispatch(toggleStarRequest(message)),
togglePinRequest: message => dispatch(togglePinRequest(message)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
replyInit: (message, mention) => dispatch(replyInit(message, mention))
actionsHide: () => dispatch(actionsHideAction()),
deleteRequest: message => dispatch(deleteRequestAction(message)),
editInit: message => dispatch(editInitAction(message)),
toggleStarRequest: message => dispatch(toggleStarRequestAction(message)),
togglePinRequest: message => dispatch(togglePinRequestAction(message)),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
replyInit: (message, mention) => dispatch(replyInitAction(message, mention))
})
)
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.setPermissions();
const { Message_AllowStarring, Message_AllowPinning } = this.props;
// Cancel
this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0;
@ -98,13 +102,13 @@ export default class MessageActions extends React.Component {
}
// Star
if (this.props.Message_AllowStarring) {
if (Message_AllowStarring) {
this.options.push(I18n.t(props.actionMessage.starred ? 'Unstar' : 'Star'));
this.STAR_INDEX = this.options.length - 1;
}
// Pin
if (this.props.Message_AllowPinning) {
if (Message_AllowPinning) {
this.options.push(I18n.t(props.actionMessage.pinned ? 'Unpin' : 'Pin'));
this.PIN_INDEX = this.options.length - 1;
}
@ -129,8 +133,9 @@ export default class MessageActions extends React.Component {
}
setPermissions() {
const { room } = this.props;
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.hasDeletePermission = result[permissions[1]];
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;
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) => {
if (this.isRoomReadOnly()) {
return false;
}
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))) {
return false;
}
const blockEditInMinutes = this.props.Message_AllowEditing_BlockEditInMinutes;
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (props.actionMessage.ts != null) {
@ -179,14 +191,14 @@ export default class MessageActions extends React.Component {
return false;
}
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)) {
return false;
}
if (this.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = this.props.Message_AllowDeleting_BlockDeleteInMinutes;
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (props.actionMessage.ts != null) {
@ -201,7 +213,8 @@ export default class MessageActions extends React.Component {
return true;
}
handleDelete() {
handleDelete = () => {
const { deleteRequest, actionMessage } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
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' }),
style: 'destructive',
onPress: () => this.props.deleteRequest(this.props.actionMessage)
onPress: () => deleteRequest(actionMessage)
}
],
{ cancelable: false }
);
}
handleEdit() {
const { _id, msg, rid } = this.props.actionMessage;
this.props.editInit({ _id, msg, rid });
handleEdit = () => {
const { actionMessage, editInit } = this.props;
const { _id, msg, rid } = actionMessage;
editInit({ _id, msg, rid });
}
handleCopy = async() => {
await Clipboard.setString(this.props.actionMessage.msg);
const { actionMessage } = this.props;
await Clipboard.setString(actionMessage.msg);
showToast(I18n.t('Copied_to_clipboard'));
}
handleShare = async() => {
const { actionMessage } = this.props;
Share.share({
message: this.props.actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '')
message: actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '')
});
};
handleStar() {
this.props.toggleStarRequest(this.props.actionMessage);
handleStar = () => {
const { actionMessage, toggleStarRequest } = this.props;
toggleStarRequest(actionMessage);
}
async handlePermalink() {
const permalink = await this.getPermalink(this.props.actionMessage);
handlePermalink = async() => {
const { actionMessage } = this.props;
const permalink = await this.getPermalink(actionMessage);
Clipboard.setString(permalink);
showToast(I18n.t('Permalink_copied_to_clipboard'));
}
handlePin() {
this.props.togglePinRequest(this.props.actionMessage);
handlePin = () => {
const { actionMessage, togglePinRequest } = this.props;
togglePinRequest(actionMessage);
}
handleReply() {
this.props.replyInit(this.props.actionMessage, true);
handleReply = () => {
const { actionMessage, replyInit } = this.props;
replyInit(actionMessage, true);
}
handleQuote() {
this.props.replyInit(this.props.actionMessage, false);
handleQuote = () => {
const { actionMessage, replyInit } = this.props;
replyInit(actionMessage, false);
}
handleReaction() {
this.props.toggleReactionPicker(this.props.actionMessage);
handleReaction = () => {
const { actionMessage, toggleReactionPicker } = this.props;
toggleReactionPicker(actionMessage);
}
handleActionPress = (actionIndex) => {
const { actionsHide } = this.props;
switch (actionIndex) {
case this.REPLY_INDEX:
this.handleReply();
@ -297,7 +320,7 @@ export default class MessageActions extends React.Component {
default:
break;
}
this.props.actionsHide();
actionsHide();
}
render() {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 Modal from 'react-native-modal';
import { responsive } from 'react-native-responsive-ui';
@ -85,9 +87,9 @@ export default class UploadModal extends Component {
return (
<Modal
isVisible={isVisible}
style={{ alignItems: 'center' }} // TODO: need this?
onBackdropPress={() => this.props.close()}
onBackButtonPress={() => this.props.close()}
style={{ alignItems: 'center' }}
onBackdropPress={() => close()}
onBackButtonPress={() => close()}
animationIn='fadeIn'
animationOut='fadeOut'
useNativeDriver

View File

@ -1,15 +1,21 @@
import React from 'react';
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 { connect } from 'react-redux';
import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
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 { editRequest, editCancel, replyCancel } from '../../actions/messages';
import styles from './styles';
import MyIcon from '../icons';
import database from '../../lib/realm';
@ -48,10 +54,10 @@ const imagePickerConfig = {
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
username: state.login.user && state.login.user.username
}), dispatch => ({
editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)),
typing: status => dispatch(userTyping(status)),
closeReply: () => dispatch(replyCancel())
editCancel: () => dispatch(editCancelAction()),
editRequest: message => dispatch(editRequestAction(message)),
typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction())
}))
export default class MessageBox extends React.PureComponent {
static propTypes = {
@ -86,13 +92,15 @@ export default class MessageBox extends React.PureComponent {
this.rooms = [];
this.emojis = [];
this.customEmojis = [];
this._onEmojiSelected = this._onEmojiSelected.bind(this);
this.onEmojiSelected = this.onEmojiSelected.bind(this);
}
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.component.focus();
} else if (this.props.replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
this.component.focus();
} else if (!nextProps.message) {
this.setState({ text: '' });
@ -100,8 +108,10 @@ export default class MessageBox extends React.PureComponent {
}
onChangeText(text) {
const { typing } = this.props;
this.setState({ text });
this.props.typing(text.length > 0);
typing(text.length > 0);
requestAnimationFrame(() => {
const { start, end } = this.component._lastNativeSelection;
@ -126,45 +136,95 @@ export default class MessageBox extends React.PureComponent {
this.closeEmoji();
}
get leftButtons() {
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 !this.state.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'
/>);
onPressMention(item) {
const { trackingType } = this.state;
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 = 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 });
}
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() {
const { text } = this.state;
const icons = [];
if (this.state.text) {
if (text) {
icons.push(<MyIcon
style={[styles.actionButtons, { color: '#1D74F5' }]}
name='send'
key='sendIcon'
accessibilityLabel={I18n.t('Send message')}
accessibilityTraits='button'
onPress={() => this.submit(this.state.text)}
onPress={() => this.submit(text)}
testID='messagebox-send-message'
/>);
return icons;
@ -198,123 +258,7 @@ export default class MessageBox extends React.PureComponent {
}
}
toggleFilesActions = () => {
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) {
getFixedMentions(keyword) {
if ('all'.indexOf(keyword) !== -1) {
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');
if (keyword) {
this.users = this.users.filtered('username CONTAINS[c] $0', keyword);
}
this._getFixedMentions(keyword);
this.getFixedMentions(keyword);
this.setState({ mentions: this.users.slice() });
const usernames = [];
@ -359,12 +303,12 @@ export default class MessageBox extends React.PureComponent {
} finally {
delete this.oldPromise;
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice();
this._getFixedMentions(keyword);
this.getFixedMentions(keyword);
this.setState({ mentions: this.users });
}
}
async _getRooms(keyword = '') {
async getRooms(keyword = '') {
this.roomsCache = this.roomsCache || [];
this.rooms = database.objects('subscriptions')
.filtered('t != $0', 'd');
@ -406,7 +350,7 @@ export default class MessageBox extends React.PureComponent {
}
}
_getEmojis(keyword) {
getEmojis(keyword) {
if (keyword) {
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4);
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4);
@ -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() {
this.setState({
mentions: [],
@ -426,76 +513,26 @@ export default class MessageBox extends React.PureComponent {
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 => (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this._onPressMention(item)}
onPress={() => this.onPressMention(item)}
>
<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>
</TouchableOpacity>
)
renderMentionEmoji = (item) => {
const { baseUrl } = this.props;
if (item.name) {
return (
<CustomEmoji
key='mention-item-avatar'
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={this.props.baseUrl}
baseUrl={baseUrl}
/>
);
}
@ -508,18 +545,22 @@ export default class MessageBox extends React.PureComponent {
</Text>
);
}
renderMentionItem = (item) => {
const { trackingType } = this.state;
const { baseUrl } = this.props;
if (item.username === 'all' || item.username === 'here') {
return this.renderFixedMentionItem(item);
}
return (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this._onPressMention(item)}
testID={`mention-item-${ this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
onPress={() => this.onPressMention(item)}
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),
<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}
size={30}
type={item.username ? 'd' : 'c'}
baseUrl={this.props.baseUrl}
baseUrl={baseUrl}
/>,
<Text key='mention-item-name'>{ item.username || item.name }</Text>
]
@ -538,6 +579,7 @@ export default class MessageBox extends React.PureComponent {
</TouchableOpacity>
);
}
renderMentions = () => {
const { mentions, trackingType } = this.state;
if (!trackingType) {
@ -567,7 +609,9 @@ export default class MessageBox extends React.PureComponent {
};
renderFilesActions = () => {
if (!this.state.showFilesAction) {
const { showFilesAction } = this.state;
if (!showFilesAction) {
return null;
}
return (
@ -581,7 +625,10 @@ export default class MessageBox extends React.PureComponent {
}
renderContent() {
if (this.state.recording) {
const { recording, text } = this.state;
const { editing } = this.props;
if (recording) {
return (<Recording onFinish={this.finishAudioMessage} />);
}
return (
@ -590,7 +637,7 @@ export default class MessageBox extends React.PureComponent {
<View style={styles.composer} key='messagebox'>
{this.renderReplyPreview()}
<View
style={[styles.textArea, this.props.editing && styles.editing]}
style={[styles.textArea, editing && styles.editing]}
testID='messagebox'
>
{this.leftButtons}
@ -601,8 +648,8 @@ export default class MessageBox extends React.PureComponent {
keyboardType='twitter'
blurOnSubmit={false}
placeholder={I18n.t('New_Message')}
onChangeText={text => this.onChangeText(text)}
value={this.state.text}
onChangeText={t => this.onChangeText(t)}
value={text}
underlineColorAndroid='transparent'
defaultValue=''
multiline
@ -617,15 +664,16 @@ export default class MessageBox extends React.PureComponent {
}
render() {
const { showEmojiKeyboard, file } = this.state;
return (
[
<KeyboardAccessoryView
key='input'
renderContent={() => this.renderContent()}
kbInputRef={this.component}
kbComponent={this.state.showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
onKeyboardResigned={() => this.onKeyboardResigned()}
onItemSelected={this._onEmojiSelected}
onItemSelected={this.onEmojiSelected}
trackInteractive
// revealKeyboardInteractive
requiresSameParentToManageScrollView
@ -634,8 +682,8 @@ export default class MessageBox extends React.PureComponent {
this.renderFilesActions(),
<UploadModal
key='upload-modal'
isVisible={(this.state.file && this.state.file.isVisible)}
file={this.state.file}
isVisible={(file && file.isVisible)}
file={file}
close={() => this.setState({ file: {} })}
submit={this.sendImageMessage}
/>

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import { errorActionsHide } from '../actions/messages';
import { errorActionsHide as errorActionsHideAction } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import protectedFunction from '../lib/methods/helpers/protectedFunction';
@ -14,7 +14,7 @@ import I18n from '../i18n';
actionMessage: state.messages.actionMessage
}),
dispatch => ({
errorActionsHide: () => dispatch(errorActionsHide())
errorActionsHide: () => dispatch(errorActionsHideAction())
})
)
export default class MessageErrorActions extends React.Component {
@ -23,6 +23,7 @@ export default class MessageErrorActions extends React.Component {
actionMessage: PropTypes.object
};
// eslint-disable-next-line react/sort-comp
constructor(props) {
super(props);
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(() => {
const { actionMessage } = this.props;
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);
});
})
handleActionPress = (actionIndex) => {
const { errorActionsHide } = this.props;
switch (actionIndex) {
case this.RESEND_INDEX:
this.handleResend();
@ -55,7 +61,7 @@ export default class MessageErrorActions extends React.Component {
default:
break;
}
this.props.errorActionsHide();
errorActionsHide();
}
render() {

View File

@ -1,5 +1,7 @@
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 I18n from '../i18n';

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import React from 'react';
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 I18n from '../i18n';
@ -22,18 +24,24 @@ const styles = StyleSheet.create({
}))
export default class Typing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.usersTyping.join() !== nextProps.usersTyping.join();
const { usersTyping } = this.props;
return usersTyping.join() !== nextProps.usersTyping.join();
}
componentWillUpdate() {
LayoutAnimation.easeInEaseOut();
}
onPress = () => {
Keyboard.dismiss();
}
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') }` : '';
}
render() {
const { usersTyping } = this;

View File

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

View File

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

View File

@ -40,7 +40,7 @@ export default class extends React.PureComponent {
}
render() {
const { isPressed } = this.state;
const { modalVisible, isPressed } = this.state;
const { baseUrl, file, user } = this.props;
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}
description={file.description}
image={img}
isVisible={this.state.modalVisible}
isVisible={modalVisible}
onClose={() => this.setState({ modalVisible: false })}
/>
]

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 PropTypes from 'prop-types';
import Modal from 'react-native-modal';
@ -45,6 +47,7 @@ export default class PhotoModal extends React.PureComponent {
onClose: PropTypes.func.isRequired,
window: PropTypes.object
}
render() {
const {
image, isVisible, onClose, title, description, window: { width, height }

View File

@ -1,5 +1,7 @@
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 Modal from 'react-native-modal';
import Icon from 'react-native-vector-icons/MaterialIcons';
@ -66,10 +68,12 @@ export default class ReactionsModal extends React.PureComponent {
PropTypes.object
])
}
renderItem = (item) => {
const { user, customEmojis, baseUrl } = this.props;
const count = item.usernames.length;
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) {
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
} else {
@ -82,8 +86,8 @@ export default class ReactionsModal extends React.PureComponent {
content={item.emoji}
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
customEmojis={this.props.customEmojis}
baseUrl={this.props.baseUrl}
customEmojis={customEmojis}
baseUrl={baseUrl}
/>
</View>
<View style={styles.peopleItemContainer}>

View File

@ -41,7 +41,7 @@ export default class User extends React.PureComponent {
render() {
const {
username, alias, ts, temp
username, alias, ts, temp, timeFormat
} = this.props;
const extraStyle = {};
@ -50,7 +50,7 @@ export default class User extends React.PureComponent {
}
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 (
<View style={styles.usernameView}>

View File

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

View File

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

View File

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

View File

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

View File

@ -27,15 +27,17 @@ export default async function() {
const filteredSettings = this._prepareSettings(this._filterSettings(data));
InteractionManager.runAfterInteractions(() =>
database.write(() =>
filteredSettings.forEach((setting) => {
InteractionManager.runAfterInteractions(
() => database.write(
() => filteredSettings.forEach((setting) => {
database.create('settings', { ...setting, _updatedAt: new Date() }, true);
if (setting._id === 'Site_Name') {
updateServer.call(this, { name: setting.valueAsString });
}
})));
})
)
);
reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings)));
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;
try {
// 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);
db.write(() => {
subscription.open = true;

View File

@ -1,7 +1,7 @@
import Random from 'react-native-meteor/lib/Random';
import messagesStatus from '../../constants/messagesStatus';
import buildMessage from '../methods/helpers/buildMessage';
import buildMessage from './helpers/buildMessage';
import { post } from './helpers/rest';
import database from '../realm';
import reduxStore from '../createStore';
@ -46,7 +46,7 @@ function sendMessageByDDP(message) {
export async function _sendMessageCall(message) {
try {
// 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;
} catch (e) {
database.write(() => {

View File

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

View File

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

View File

@ -10,7 +10,9 @@ import database from './realm';
import log from '../utils/log';
// 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 { setActiveUser } from '../actions/activeUsers';
import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages';
@ -598,8 +600,8 @@ const RocketChat = {
getPermissions,
getCustomEmoji,
parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[defaultSettings[item._id].type] || item.valueAsString || item.valueAsNumber ||
item.valueAsBoolean || item.value;
ret[item._id] = item[defaultSettings[item._id].type] || item.valueAsString || item.valueAsNumber
|| item.valueAsBoolean || item.value;
return ret;
}, {}),
_prepareSettings(settings) {

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 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 { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';

View File

@ -1,5 +1,7 @@
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 { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';

View File

@ -1,5 +1,7 @@
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 { appStart } from '../actions';

View File

@ -1,6 +1,8 @@
import { AsyncStorage } from 'react-native';
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 { appStart } from '../actions';

View File

@ -1,5 +1,7 @@
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 { BACKGROUND } from 'redux-enhancer-react-native-appstate';
@ -161,7 +163,7 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
yield goRoomsListAndDelete(rid);
} catch (e) {
if (e.error === 'error-you-are-last-owner') {
Alert.alert(e.error);
Alert.alert(I18n.t(e.error));
} else {
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 { connect } from 'react-redux';
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 LoggedView from './View';
import { createChannelRequest } from '../actions/createChannel';
import { removeUser } from '../actions/selectedUsers';
import { createChannelRequest as createChannelRequestAction } from '../actions/createChannel';
import { removeUser as removeUserAction } from '../actions/selectedUsers';
import sharedStyles from './Styles';
import KeyboardView from '../presentation/KeyboardView';
import scrollPersistTaps from '../utils/scrollPersistTaps';
@ -73,8 +75,8 @@ const styles = StyleSheet.create({
createChannel: state.createChannel,
users: state.selectedUsers.users
}), dispatch => ({
create: data => dispatch(createChannelRequest(data)),
removeUser: user => dispatch(removeUser(user))
create: data => dispatch(createChannelRequestAction(data)),
removeUser: user => dispatch(removeUserAction(user))
}))
/** @extends React.Component */
export default class CreateChannelView extends LoggedView {
@ -105,15 +107,19 @@ export default class CreateChannelView extends LoggedView {
}
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(() => {
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);
}, 300);
}
}
onChangeText = (channelName) => {
const { navigator } = this.props;
const rightButtons = [];
if (channelName.trim().length > 0) {
rightButtons.push({
@ -122,7 +128,7 @@ export default class CreateChannelView extends LoggedView {
testID: 'create-channel-submit'
});
}
this.props.navigator.setButtons({ rightButtons });
navigator.setButtons({ rightButtons });
this.setState({ channelName });
}
@ -135,28 +141,30 @@ export default class CreateChannelView extends LoggedView {
}
submit = () => {
if (!this.state.channelName.trim() || this.props.createChannel.isFetching) {
return;
}
const {
channelName, type, readOnly, broadcast
} = 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
users = users.map(user => user.name);
const users = usersProps.map(user => user.name);
// create channel
this.props.create({
create({
name: channelName, users, type, readOnly, broadcast
});
}
removeUser = (user) => {
if (this.props.users.length === 1) {
const { users, removeUser } = this.props;
if (users.length === 1) {
return;
}
this.props.removeUser(user);
removeUser(user);
}
renderSwitch = ({
@ -215,31 +223,42 @@ export default class CreateChannelView extends LoggedView {
renderFormSeparator = () => <View style={[sharedStyles.separator, styles.formSeparator]} />
renderItem = ({ item }) => (
<UserItem
name={item.fname}
username={item.name}
onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${ item.name }`}
baseUrl={this.props.baseUrl}
/>
)
renderItem = ({ item }) => {
const { baseUrl } = this.props;
renderInvitedList = () => (
<FlatList
data={this.props.users}
extraData={this.props.users}
keyExtractor={item => item._id}
style={[styles.list, sharedStyles.separatorVertical]}
renderItem={this.renderItem}
ItemSeparatorComponent={this.renderSeparator}
enableEmptySections
keyboardShouldPersistTaps='always'
/>
)
return (
<UserItem
name={item.fname}
username={item.name}
onPress={() => this.removeUser(item)}
testID={`create-channel-view-item-${ item.name }`}
baseUrl={baseUrl}
/>
);
}
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() {
const userCount = this.props.users.length;
const { channelName } = this.state;
const { users, createChannel } = this.props;
const userCount = users.length;
return (
<KeyboardView
contentContainerStyle={[sharedStyles.container, styles.container]}
@ -252,7 +271,7 @@ export default class CreateChannelView extends LoggedView {
ref={ref => this.channelNameRef = ref}
style={styles.input}
label={I18n.t('Channel_Name')}
value={this.state.channelName}
value={channelName}
onChangeText={this.onChangeText}
placeholder={I18n.t('Channel_Name')}
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>
</View>
{this.renderInvitedList()}
<Loading visible={this.props.createChannel.isFetching} />
<Loading visible={createChannel.isFetching} />
</ScrollView>
</SafeAreaView>
</KeyboardView>

View File

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

View File

@ -1,12 +1,14 @@
import React from 'react';
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 Icon from 'react-native-vector-icons/FontAwesome';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
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 sharedStyles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps';
@ -57,8 +59,8 @@ const styles = StyleSheet.create({
Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter,
services: state.login.services
}), dispatch => ({
open: () => dispatch(open()),
close: () => dispatch(close())
open: () => dispatch(openAction()),
close: () => dispatch(closeAction())
}))
/** @extends React.Component */
export default class LoginSignupView extends LoggedView {
@ -85,23 +87,27 @@ export default class LoginSignupView extends LoggedView {
}
componentDidMount() {
this.props.open();
const { open } = this.props;
open();
}
componentWillReceiveProps(nextProps) {
if (this.props.services !== nextProps.services) {
const { services } = this.props;
if (services !== nextProps.services) {
LayoutAnimation.easeInEaseOut();
}
}
componentWillUnmount() {
this.props.close();
const { close } = this.props;
close();
}
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 redirect_uri = `${ this.props.server }/_oauth/facebook?close`;
const redirect_uri = `${ server }/_oauth/facebook?close`;
const scope = 'email';
const state = this.getOAuthState();
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 = () => {
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 redirect_uri = `${ this.props.server }/_oauth/github?close`;
const redirect_uri = `${ server }/_oauth/github?close`;
const scope = 'user:email';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`;
@ -119,9 +126,10 @@ export default class LoginSignupView extends LoggedView {
}
onPressGitlab = () => {
const { clientId } = this.props.services.gitlab;
const { services, server } = this.props;
const { clientId } = services.gitlab;
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 state = this.getOAuthState();
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 = () => {
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 redirect_uri = `${ this.props.server }/_oauth/google?close`;
const redirect_uri = `${ server }/_oauth/google?close`;
const scope = 'email';
const state = this.getOAuthState();
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 = () => {
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 redirect_uri = `${ this.props.server }/_oauth/linkedin?close`;
const redirect_uri = `${ server }/_oauth/linkedin?close`;
const scope = 'r_emailaddress';
const state = this.getOAuthState();
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 = () => {
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 redirect_uri = `${ this.props.server }/_oauth/meteor-developer`;
const redirect_uri = `${ server }/_oauth/meteor-developer`;
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressTwitter = () => {
const { server } = this.props;
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);
}
@ -169,7 +181,8 @@ export default class LoginSignupView extends LoggedView {
}
openOAuth = (oAuthUrl) => {
this.props.navigator.showModal({
const { navigator } = this.props;
navigator.showModal({
screen: 'OAuthView',
title: 'OAuth',
passProps: {
@ -179,93 +192,113 @@ export default class LoginSignupView extends LoggedView {
}
login = () => {
this.props.navigator.push({
const { navigator, server } = this.props;
navigator.push({
screen: 'LoginView',
title: this.props.server,
title: server,
backButtonTitle: ''
});
}
register = () => {
this.props.navigator.push({
const { navigator, server } = this.props;
navigator.push({
screen: 'RegisterView',
title: this.props.server,
title: server,
backButtonTitle: ''
});
}
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) {
return null;
}
return (
<View style={styles.servicesContainer}>
<Text style={styles.servicesTitle}>
{I18n.t('Or_continue_using_social_accounts')}
</Text>
<View style={sharedStyles.loginOAuthButtons} key='services'>
{this.props.Accounts_OAuth_Facebook && this.props.services.facebook ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
onPress={this.onPressFacebook}
>
<Icon name='facebook' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Facebook && services.facebook
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
onPress={this.onPressFacebook}
>
<Icon name='facebook' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Github && this.props.services.github ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
onPress={this.onPressGithub}
>
<Icon name='github' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Github && services.github
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
onPress={this.onPressGithub}
>
<Icon name='github' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
onPress={this.onPressGitlab}
>
<Icon name='gitlab' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Gitlab && services.gitlab
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
onPress={this.onPressGitlab}
>
<Icon name='gitlab' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Google && this.props.services.google ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
onPress={this.onPressGoogle}
>
<Icon name='google' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Google && services.google
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
onPress={this.onPressGoogle}
>
<Icon name='google' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
onPress={this.onPressLinkedin}
>
<Icon name='linkedin' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Linkedin && services.linkedin
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
onPress={this.onPressLinkedin}
>
<Icon name='linkedin' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
onPress={this.onPressMeteor}
>
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Meteor && services['meteor-developer']
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
onPress={this.onPressMeteor}
>
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
</TouchableOpacity>
)
: null
}
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter ?
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
onPress={this.onPressTwitter}
>
<Icon name='twitter' size={20} color='#ffffff' />
</TouchableOpacity>
{Accounts_OAuth_Twitter && services.twitter
? (
<TouchableOpacity
style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
onPress={this.onPressTwitter}
>
<Icon name='twitter' size={20} color='#ffffff' />
</TouchableOpacity>
)
: null
}
</View>
@ -274,6 +307,8 @@ export default class LoginSignupView extends LoggedView {
}
render() {
const { isFetching } = this.props;
return (
<ScrollView
style={[sharedStyles.container, sharedStyles.containerScrollView]}
@ -298,7 +333,7 @@ export default class LoginSignupView extends LoggedView {
/>
{this.renderServices()}
</View>
<Loading visible={this.props.isFetching} />
<Loading visible={isFetching} />
</SafeAreaView>
</ScrollView>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import React from 'react';
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
import React from 'react';
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 { openRoomFiles as openRoomFilesAction, closeRoomFiles as closeRoomFilesAction } from '../../actions/roomFiles';
import LoggedView from '../View';
import { openRoomFiles, closeRoomFiles } from '../../actions/roomFiles';
import styles from './styles';
import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator';
@ -19,8 +21,8 @@ import I18n from '../../i18n';
token: state.login.user && state.login.user.token
}
}), dispatch => ({
openRoomFiles: (rid, limit) => dispatch(openRoomFiles(rid, limit)),
closeRoomFiles: () => dispatch(closeRoomFiles())
openRoomFiles: (rid, limit) => dispatch(openRoomFilesAction(rid, limit)),
closeRoomFiles: () => dispatch(closeRoomFilesAction())
}))
/** @extends React.Component */
export default class RoomFilesView extends LoggedView {
@ -47,17 +49,20 @@ export default class RoomFilesView extends LoggedView {
}
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 });
}
}
componentWillUnmount() {
this.props.closeRoomFiles();
const { closeRoomFiles } = this.props;
closeRoomFiles();
}
load = () => {
this.props.openRoomFiles(this.props.rid, this.limit);
const { openRoomFiles, rid } = this.props;
openRoomFiles(rid, this.limit);
}
moreData = () => {
@ -79,15 +84,19 @@ export default class RoomFilesView extends LoggedView {
</View>
)
renderItem = ({ item }) => (
<Message
item={item}
style={styles.message}
reactions={item.reactions}
user={this.props.user}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
/>
)
renderItem = ({ item }) => {
const { user } = this.props;
return (
<Message
item={item}
style={styles.message}
reactions={item.reactions}
user={user}
customTimeFormat='MMMM Do YYYY, h:mm:ss a'
/>
);
}
render() {
const { messages, ready } = this.props;

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import React from 'react';
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 moment from 'moment';
@ -20,14 +22,14 @@ import { iconsMap } from '../../Icons';
const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' ?
<Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text> :
[
const getRoomTitle = room => (room.t === 'd'
? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text>
: (
<View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} key='room-info-type' />
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
</View>
]
)
);
@connect(state => ({
@ -35,7 +37,7 @@ const getRoomTitle = room => (room.t === 'd' ?
userId: state.login.user && state.login.user.id,
activeUsers: state.activeUsers,
Message_TimeFormat: state.settings.Message_TimeFormat,
roles: state.roles
allRoles: state.roles
}))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView {
@ -46,7 +48,7 @@ export default class RoomInfoView extends LoggedView {
baseUrl: PropTypes.string,
activeUsers: PropTypes.object,
Message_TimeFormat: PropTypes.string,
roles: PropTypes.object
allRoles: PropTypes.object
}
constructor(props) {
@ -64,19 +66,61 @@ export default class RoomInfoView extends LoggedView {
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
async componentDidMount() {
await this.updateRoom();
componentDidMount() {
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
if (this.state.room) {
if (this.state.room.t === 'd') {
if (room) {
if (room.t === 'd') {
try {
const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.userId);
const roomUser = await RocketChat.getRoomMember(room.rid, userId);
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) {
// get full user data looking for utcOffset
// 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);
}
} 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]) {
this.props.navigator.setButtons({
navigator.setButtons({
rightButtons: [{
id: 'edit',
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) => (
<View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t(camelize(key))}</Text>
@ -155,24 +168,33 @@ export default class RoomInfoView extends LoggedView {
</View>
);
renderRoles = () => (
this.state.roles.length > 0 ?
<View style={styles.item}>
<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}>
{this.state.roles.map(role => (
<View style={styles.roleBadge} key={role}>
<Text>{ this.props.roles[role] }</Text>
renderRoles = () => {
const { roles } = this.state;
const { allRoles } = this.props;
return (
roles.length > 0
? (
<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>
: null
)
</View>
)
: null
);
}
renderTimezone = (userId) => {
if (this.props.activeUsers[userId]) {
const { utcOffset } = this.props.activeUsers[userId];
const { activeUsers, Message_TimeFormat } = this.props;
if (activeUsers[userId]) {
const { utcOffset } = activeUsers[userId];
if (!utcOffset) {
return null;
@ -180,24 +202,28 @@ export default class RoomInfoView extends LoggedView {
return (
<View style={styles.item}>
<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>
);
}
return null;
}
renderAvatar = (room, roomUser) => (
<Avatar
text={room.name}
size={100}
style={styles.avatar}
type={room.t}
baseUrl={this.props.baseUrl}
>
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null}
</Avatar>
)
renderAvatar = (room, roomUser) => {
const { baseUrl } = this.props;
return (
<Avatar
text={room.name}
size={100}
style={styles.avatar}
type={room.t}
baseUrl={baseUrl}
>
{room.t === 'd' ? <Status style={[sharedStyles.status, styles.status]} id={roomUser._id} /> : null}
</Avatar>
);
}
renderBroadcast = () => (
<View style={styles.item}>

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 Icon from 'react-native-vector-icons/MaterialIcons';
import { responsive } from 'react-native-responsive-ui';
@ -64,7 +66,8 @@ export default class UploadProgress extends Component {
this.state = {
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);
}
@ -98,11 +101,13 @@ export default class UploadProgress extends Component {
}
tryAgain = async(item) => {
const { rid } = this.props;
try {
database.write(() => {
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) {
log('UploadProgess.tryAgain', e);
}
@ -113,6 +118,8 @@ export default class UploadProgress extends Component {
}
renderItemContent = (item) => {
const { window } = this.props;
if (!item.error) {
return (
[
@ -123,7 +130,7 @@ export default class UploadProgress extends Component {
</Text>
<Icon name='close' size={20} color='#9EA2A8' onPress={() => this.cancelUpload(item)} />
</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 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 equal from 'deep-equal';
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 { List } from './ListView';
import { openRoom, closeRoom, setLastOpen } from '../../actions/room';
import { toggleReactionPicker, actionsShow } from '../../actions/messages';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message';
@ -33,11 +35,11 @@ import { iconsMap } from '../../Icons';
showActions: state.messages.showActions,
showErrorActions: state.messages.showErrorActions
}), dispatch => ({
openRoom: room => dispatch(openRoom(room)),
setLastOpen: date => dispatch(setLastOpen(date)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
close: () => dispatch(closeRoom())
openRoom: room => dispatch(openRoomAction(room)),
setLastOpen: date => dispatch(setLastOpenAction(date)),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
closeRoom: () => dispatch(closeRoomAction())
}))
/** @extends React.Component */
export default class RoomView extends LoggedView {
@ -56,12 +58,12 @@ export default class RoomView extends LoggedView {
actionMessage: PropTypes.object,
toggleReactionPicker: PropTypes.func.isRequired,
actionsShow: PropTypes.func,
close: PropTypes.func
closeRoom: PropTypes.func
};
constructor(props) {
super('RoomView', props);
this.rid = this.props.rid;
this.rid = props.rid;
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
loaded: false,
@ -74,7 +76,9 @@ export default class RoomView extends LoggedView {
}
componentWillMount() {
this.props.navigator.setButtons({
const { navigator } = this.props;
navigator.setButtons({
rightButtons: [{
id: 'more',
testID: 'room-view-header-actions',
@ -88,9 +92,11 @@ export default class RoomView extends LoggedView {
}
componentDidMount() {
const { navigator } = this.props;
this.updateRoom();
this.rooms.addListener(this.updateRoom);
this.props.navigator.setDrawerEnabled({
navigator.setDrawerEnabled({
side: 'left',
enabled: false
});
@ -98,12 +104,16 @@ export default class RoomView extends LoggedView {
}
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) {
if (prevState.room.f !== this.state.room.f) {
this.props.navigator.setButtons({
const { room } = this.state;
const { navigator } = this.props;
if (prevState.room.f !== room.f) {
navigator.setButtons({
rightButtons: [{
id: 'more',
testID: 'room-view-header-actions',
@ -111,32 +121,37 @@ export default class RoomView extends LoggedView {
}, {
id: 'star',
testID: 'room-view-header-star',
icon: this.state.room.f ? iconsMap.star : iconsMap.starOutline
icon: room.f ? iconsMap.star : iconsMap.starOutline
}]
});
}
}
componentWillUnmount() {
const { closeRoom } = this.props;
this.rooms.removeAllListeners();
this.onEndReached.stop();
this.props.close();
closeRoom();
}
onNavigatorEvent(event) {
const { room } = this.state;
const { rid, f } = room;
const { navigator } = this.props;
if (event.type === 'NavBarButtonPress') {
if (event.id === 'more') {
this.props.navigator.push({
navigator.push({
screen: 'RoomActionsView',
title: I18n.t('Actions'),
backButtonTitle: '',
passProps: {
rid: this.state.room.rid
rid
}
});
} else if (event.id === 'star') {
try {
RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
RocketChat.toggleFavorite(rid, f);
} catch (e) {
log('toggleFavorite', e);
}
@ -151,8 +166,9 @@ export default class RoomView extends LoggedView {
}
requestAnimationFrame(async() => {
const { room } = this.state;
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 });
} catch (e) {
log('RoomView.onEndReached', e);
@ -161,14 +177,16 @@ export default class RoomView extends LoggedView {
})
onMessageLongPress = (message) => {
this.props.actionsShow(message);
const { actionsShow } = this.props;
actionsShow(message);
}
onReactionPress = (shortname, messageId) => {
const { actionMessage, toggleReactionPicker } = this.props;
try {
if (!messageId) {
RocketChat.setReaction(shortname, this.props.actionMessage._id);
return this.props.toggleReactionPicker();
RocketChat.setReaction(shortname, actionMessage._id);
return toggleReactionPicker();
}
RocketChat.setReaction(shortname, messageId);
} catch (e) {
@ -177,53 +195,71 @@ export default class RoomView extends LoggedView {
};
updateRoom = async() => {
const { navigator, openRoom, setLastOpen } = this.props;
if (this.rooms.length > 0) {
const { room: prevRoom } = this.state;
const room = JSON.parse(JSON.stringify(this.rooms[0]));
this.setState({ room });
if (!prevRoom.rid) {
this.props.navigator.setTitle({ title: room.name });
this.props.openRoom({
navigator.setTitle({ title: room.name });
openRoom({
...room
});
if (room.alert || room.unread || room.userMentions) {
this.props.setLastOpen(room.ls);
setLastOpen(room.ls);
} else {
this.props.setLastOpen(null);
setLastOpen(null);
}
}
} else {
this.props.openRoom({ rid: this.rid });
openRoom({ rid: this.rid });
this.setState({ joined: false });
}
}
sendMessage = (message) => {
const { setLastOpen } = this.props;
LayoutAnimation.easeInEaseOut();
RocketChat.sendMessage(this.rid, message).then(() => {
this.props.setLastOpen(null);
setLastOpen(null);
});
};
joinRoom = async() => {
const { rid } = this.props;
try {
await RocketChat.joinRoom(this.props.rid);
this.setState({ joined: true });
await RocketChat.joinRoom(rid);
this.setState({
joined: true
});
} catch (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 = () => {
if (this.state.room) {
const { t, blocked, blocker } = this.state.room;
const { room } = this.state;
if (room) {
const { t, blocked, blocker } = room;
if (t === 'd' && (blocked || blocker)) {
return true;
}
@ -231,24 +267,31 @@ export default class RoomView extends LoggedView {
return false;
}
renderItem = (item, previousItem) => (
<Message
key={item._id}
item={item}
status={item.status}
reactions={JSON.parse(JSON.stringify(item.reactions))}
user={this.props.user}
archived={this.state.room.archived}
broadcast={this.state.room.broadcast}
previousItem={previousItem}
_updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
/>
);
renderItem = (item, previousItem) => {
const { room } = this.state;
const { user } = this.props;
return (
<Message
key={item._id}
item={item}
status={item.status}
reactions={JSON.parse(JSON.stringify(item.reactions))}
user={user}
archived={room.archived}
broadcast={room.broadcast}
previousItem={previousItem}
_updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
/>
);
}
renderFooter = () => {
if (!this.state.joined) {
const { joined, room } = this.state;
if (!joined) {
return (
<View style={styles.joinRoomContainer} key='room-view-join'>
<Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text>
@ -263,7 +306,7 @@ export default class RoomView extends LoggedView {
</View>
);
}
if (this.state.room.archived || this.isReadOnly()) {
if (room.archived || this.isReadOnly()) {
return (
<View style={styles.readOnly}>
<Text>{I18n.t('This_room_is_read_only')}</Text>
@ -281,21 +324,23 @@ export default class RoomView extends LoggedView {
};
renderHeader = () => {
if (!this.state.end) {
const { end } = this.state;
if (!end) {
return <ActivityIndicator style={[styles.loading, { transform: [{ scaleY: -1 }] }]} />;
}
return null;
}
renderList = () => {
if (!this.state.loaded) {
const { loaded, end } = this.state;
if (!loaded) {
return <ActivityIndicator style={styles.loading} />;
}
return (
[
<List
key='room-view-messages'
end={this.state.end}
end={end}
room={this.rid}
renderFooter={this.renderHeader}
onEndReached={this.onEndReached}
@ -307,13 +352,17 @@ export default class RoomView extends LoggedView {
}
render() {
const { room } = this.state;
const { user, showActions, showErrorActions } = this.props;
return (
<SafeAreaView style={styles.container} testID='room-view'>
{this.renderList()}
{this.state.room._id && this.props.showActions ?
<MessageActions room={this.state.room} user={this.props.user} /> :
null}
{this.props.showErrorActions ? <MessageErrorActions /> : null}
{room._id && showActions
? <MessageActions room={room} user={user} />
: null
}
{showErrorActions ? <MessageErrorActions /> : null}
<ReactionPicker onEmojiSelected={this.onReactionPress} />
<UploadProgress rid={this.rid} />
</SafeAreaView>

View File

@ -1,5 +1,7 @@
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';
const styles = StyleSheet.create({

View File

@ -1,5 +1,7 @@
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 I18n from '../../../i18n';

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 { connect } from 'react-redux';
@ -46,14 +48,17 @@ export default class Sort extends Component {
}
componentDidUpdate(prevProps) {
if (prevProps.closeSortDropdown !== this.props.closeSortDropdown) {
const { closeSortDropdown } = this.props;
if (prevProps.closeSortDropdown !== closeSortDropdown) {
this.close();
}
}
setSortPreference = async(param) => {
const { setSortPreference } = this.props;
try {
this.props.setSortPreference(param);
setSortPreference(param);
RocketChat.saveSortPreference(param);
} catch (e) {
log('RoomsListView.setSortPreference', e);
@ -69,18 +74,22 @@ export default class Sort extends Component {
}
toggleGroupByType = () => {
this.setSortPreference({ groupByType: !this.props.groupByType });
const { groupByType } = this.props;
this.setSortPreference({ groupByType: !groupByType });
}
toggleGroupByFavorites = () => {
this.setSortPreference({ showFavorites: !this.props.showFavorites });
const { showFavorites } = this.props;
this.setSortPreference({ showFavorites: !showFavorites });
}
toggleUnread = () => {
this.setSortPreference({ showUnread: !this.props.showUnread });
const { showUnread } = this.props;
this.setSortPreference({ showUnread: !showUnread });
}
close = () => {
const { close } = this.props;
Animated.timing(
this.animatedValue,
{
@ -89,7 +98,7 @@ export default class Sort extends Component {
easing: Easing.ease,
useNativeDriver: true
},
).start(() => this.props.close());
).start(() => close());
}
render() {
@ -104,6 +113,7 @@ export default class Sort extends Component {
const {
sortBy, groupByType, showFavorites, showUnread
} = this.props;
return (
[
<TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}>
@ -156,7 +166,7 @@ export default class Sort extends Component {
style={[styles.dropdownContainerHeader, styles.sortToggleContainerClose]}
>
<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' }} />
</View>
</Touch>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
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({
container: {

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