diff --git a/.circleci/changelog.sh b/.circleci/changelog.sh
new file mode 100644
index 000000000..deb042836
--- /dev/null
+++ b/.circleci/changelog.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+git log --format="%cd" -n 14 --date=short | sort -u -r | while read DATE ; do
+ echo $DATE
+ GIT_PAGER=cat git log --no-merges --format="- %s" --since="$DATE 00:00:00" --until="$DATE 24:00:00"
+ echo
+done
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 51eb5edc9..37f74a1b7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -137,6 +137,7 @@ jobs:
- run:
name: Install NPM modules
command: |
+ rm -rf node_modules
npm install
npm install react-native
@@ -185,7 +186,7 @@ jobs:
name: Fastlane Tesflight Upload
command: |
cd ios
- fastlane pilot upload
+ fastlane pilot upload --changelog "$(sh ../.circleci/changelog.sh)"
workflows:
version: 2
diff --git a/.eslintrc b/.eslintrc.js
similarity index 99%
rename from .eslintrc
rename to .eslintrc.js
index a55acafff..f7cb61726 100644
--- a/.eslintrc
+++ b/.eslintrc.js
@@ -1,4 +1,4 @@
-{
+module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"parserOptions": {
@@ -120,4 +120,4 @@
"globals": {
"__DEV__": true
}
-}
+};
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f1f65eeaf..eba340df1 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -144,6 +144,7 @@ android {
}
dependencies {
+ compile project(':react-native-video')
compile project(':react-native-push-notification')
compile project(':react-native-svg')
compile project(':react-native-image-picker')
diff --git a/android/app/src/main/java/com/rocketchatrn/MainApplication.java b/android/app/src/main/java/com/rocketchatrn/MainApplication.java
index e81c036ec..d4ef8c951 100644
--- a/android/app/src/main/java/com/rocketchatrn/MainApplication.java
+++ b/android/app/src/main/java/com/rocketchatrn/MainApplication.java
@@ -14,6 +14,7 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
+import com.brentvatne.react.ReactVideoPackage;
import java.util.Arrays;
import java.util.List;
@@ -35,7 +36,8 @@ public class MainApplication extends Application implements ReactApplication {
new RNFetchBlobPackage(),
new ZeroconfReactPackage(),
new RealmReactPackage(),
- new ReactNativePushNotificationPackage()
+ new ReactNativePushNotificationPackage(),
+ new ReactVideoPackage()
);
}
};
diff --git a/android/settings.gradle b/android/settings.gradle
index 17aab02a2..192ad0401 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,6 +1,6 @@
rootProject.name = 'RocketChatRN'
-include ':react-native-push-notification'
-project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/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-image-picker'
diff --git a/app/actions/login.js b/app/actions/login.js
index c603f42bd..4c53c301e 100644
--- a/app/actions/login.js
+++ b/app/actions/login.js
@@ -76,8 +76,7 @@ export function loginFailure(err) {
export function setToken(user = {}) {
return {
type: types.LOGIN.SET_TOKEN,
- token: user.token,
- user
+ ...user
};
}
diff --git a/app/containers/Banner.js b/app/containers/Banner.js
index 48660deca..5cc765c39 100644
--- a/app/containers/Banner.js
+++ b/app/containers/Banner.js
@@ -30,22 +30,9 @@ export default class Banner extends React.PureComponent {
authenticating: PropTypes.bool,
offline: PropTypes.bool
}
- componentWillMount() {
- this.setState({
- slow: false
- });
- this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
- }
- componentWillUnmount() {
- clearTimeout(this.timer);
- }
render() {
const { connecting, authenticating, offline } = this.props;
- if (!this.state.slow) {
- return null;
- }
-
if (offline) {
return (
diff --git a/app/containers/Routes.js b/app/containers/Routes.js
index e1e57e950..09fb6eee9 100644
--- a/app/containers/Routes.js
+++ b/app/containers/Routes.js
@@ -12,7 +12,8 @@ import * as NavigationService from './routes/NavigationService';
@connect(
state => ({
login: state.login,
- app: state.app
+ app: state.app,
+ background: state.app.background
}),
dispatch => bindActionCreators({
appInit
@@ -26,7 +27,7 @@ export default class Routes extends React.Component {
}
componentWillMount() {
- this.props.appInit();
+ return !this.props.app.ready && this.props.appInit();
}
componentDidUpdate() {
@@ -40,7 +41,7 @@ export default class Routes extends React.Component {
return ();
}
- if ((login.token && !login.failure && !login.isRegistering) || app.ready) {
+ if (login.token && !login.failure && !login.isRegistering) {
return ( this.navigator = nav} />);
}
diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js
new file mode 100644
index 000000000..fca05d995
--- /dev/null
+++ b/app/containers/message/Audio.js
@@ -0,0 +1,161 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View, StyleSheet, TouchableOpacity, Text, Easing } from 'react-native';
+import Video from 'react-native-video';
+import Icon from 'react-native-vector-icons/MaterialIcons';
+import Slider from 'react-native-slider';
+import Markdown from './Markdown';
+
+const styles = StyleSheet.create({
+ audioContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 50,
+ margin: 5,
+ backgroundColor: '#eee',
+ borderRadius: 6
+ },
+ playPauseButton: {
+ width: 50,
+ alignItems: 'center',
+ backgroundColor: 'transparent',
+ borderRightColor: '#ccc',
+ borderRightWidth: 1
+ },
+ playPauseIcon: {
+ color: '#ccc',
+ backgroundColor: 'transparent'
+ },
+ progressContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ height: '100%',
+ marginHorizontal: 10
+ },
+ label: {
+ color: '#888',
+ fontSize: 10
+ },
+ currentTime: {
+ position: 'absolute',
+ left: 0,
+ bottom: 2
+ },
+ duration: {
+ position: 'absolute',
+ right: 0,
+ bottom: 2
+ }
+});
+
+const formatTime = (t = 0, duration = 0) => {
+ const time = Math.min(
+ Math.max(t, 0),
+ duration
+ );
+ const formattedMinutes = Math.floor(time / 60).toFixed(0).padStart(2, 0);
+ const formattedSeconds = Math.floor(time % 60).toFixed(0).padStart(2, 0);
+ return `${ formattedMinutes }:${ formattedSeconds }`;
+};
+
+export default class Audio extends React.PureComponent {
+ static propTypes = {
+ file: PropTypes.object.isRequired,
+ baseUrl: PropTypes.string.isRequired,
+ user: PropTypes.object.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+ this.onLoad = this.onLoad.bind(this);
+ this.onProgress = this.onProgress.bind(this);
+ this.onEnd = this.onEnd.bind(this);
+ const { baseUrl, file, user } = props;
+ this.state = {
+ currentTime: 0,
+ duration: 0,
+ paused: true,
+ uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
+ };
+ }
+
+ onLoad(data) {
+ this.setState({ duration: data.duration });
+ }
+
+ onProgress(data) {
+ if (data.currentTime < this.state.duration) {
+ this.setState({ currentTime: data.currentTime });
+ }
+ }
+
+ onEnd() {
+ this.setState({ paused: true, currentTime: 0 });
+ requestAnimationFrame(() => {
+ this.player.seek(0);
+ });
+ }
+
+ getCurrentTime() {
+ return formatTime(this.state.currentTime, this.state.duration);
+ }
+
+ getDuration() {
+ return formatTime(this.state.duration);
+ }
+
+ togglePlayPause() {
+ this.setState({ paused: !this.state.paused });
+ }
+
+ render() {
+ const { uri, paused } = this.state;
+ const { description } = this.props.file;
+ return (
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/containers/message/Card.js b/app/containers/message/Card.js
deleted file mode 100644
index cef86b8ed..000000000
--- a/app/containers/message/Card.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Meteor from 'react-native-meteor';
-import { connect } from 'react-redux';
-import { CachedImage } from 'react-native-img-cache';
-import { Text, TouchableOpacity, View } from 'react-native';
-import {
- Card,
- CardImage,
- // CardTitle,
- CardContent
- // CardAction
-} from 'react-native-card-view';
-import RocketChat from '../../lib/rocketchat';
-
-import PhotoModal from './PhotoModal';
-
-
-@connect(state => ({
- base: state.settings.Site_Url,
- canShowList: state.login.token.length || state.login.user.token
-}))
-export default class Cards extends React.PureComponent {
- static propTypes = {
- data: PropTypes.object.isRequired,
- base: PropTypes.string
- }
-
- constructor() {
- super();
- const user = Meteor.user();
- this.state = {
- modalVisible: false
- };
- RocketChat.getUserToken().then((token) => {
- this.setState({ img: `${ this.props.base }${ this.props.data.image_url }?rc_uid=${ user._id }&rc_token=${ token }` });
- });
- }
-
- getDescription() {
- if (this.props.data.description) {
- return {this.props.data.description};
- }
- }
-
- getImage() {
- return (
-
- this._onPressButton()}>
-
-
-
-
-
- {this.props.data.title}
- {this.getDescription()}
-
-
-
- this.setState({ modalVisible: false })}
- />
-
- );
- }
-
- getOther() {
- return (
- {this.props.data.title}
- );
- }
-
- _onPressButton() {
- this.setState({
- modalVisible: true
- });
- }
-
- render() {
- return this.state.img ? this.getImage() : this.getOther();
- }
-}
diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js
new file mode 100644
index 000000000..c7b3bdbc6
--- /dev/null
+++ b/app/containers/message/Image.js
@@ -0,0 +1,97 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { CachedImage } from 'react-native-img-cache';
+import { Text, TouchableOpacity, View, StyleSheet } from 'react-native';
+import PhotoModal from './PhotoModal';
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ flexDirection: 'column',
+ height: 320,
+ borderColor: '#ccc',
+ borderWidth: 1,
+ borderRadius: 6
+ },
+ imageContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ image: {
+ width: 256,
+ height: 256,
+ resizeMode: 'cover'
+ },
+ labelContainer: {
+ height: 62,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ imageName: {
+ fontSize: 12,
+ alignSelf: 'center',
+ fontStyle: 'italic'
+ },
+ message: {
+ alignSelf: 'center',
+ fontWeight: 'bold'
+ }
+});
+
+export default class extends React.PureComponent {
+ static propTypes = {
+ file: PropTypes.object.isRequired,
+ baseUrl: PropTypes.string.isRequired,
+ user: PropTypes.object.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+ const { baseUrl, file, user } = props;
+ this.state = {
+ modalVisible: false,
+ img: `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
+ };
+ }
+
+ getDescription() {
+ if (this.props.file.description) {
+ return {this.props.file.description};
+ }
+ }
+
+ _onPressButton() {
+ this.setState({
+ modalVisible: true
+ });
+ }
+
+ render() {
+ return (
+
+ this._onPressButton()}
+ style={styles.button}
+ >
+
+
+
+
+ {this.props.file.title}
+ {this.getDescription()}
+
+
+ this.setState({ modalVisible: false })}
+ />
+
+ );
+ }
+}
diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js
new file mode 100644
index 000000000..58739eecb
--- /dev/null
+++ b/app/containers/message/Markdown.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line
+import { emojify } from 'react-emojione';
+
+const Markdown = ({ msg }) => {
+ if (!msg) {
+ return null;
+ }
+ msg = emojify(msg, { output: 'unicode' });
+ return {msg};
+};
+
+Markdown.propTypes = {
+ msg: PropTypes.string.isRequired
+};
+
+export default Markdown;
diff --git a/app/containers/message/QuoteMark.js b/app/containers/message/QuoteMark.js
new file mode 100644
index 000000000..2c010c6e2
--- /dev/null
+++ b/app/containers/message/QuoteMark.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import PropTypes from 'prop-types';
+
+const styles = StyleSheet.create({
+ quoteSign: {
+ borderWidth: 2,
+ borderRadius: 4,
+ height: '100%',
+ marginRight: 5
+ }
+});
+
+const QuoteMark = ({ color }) => ;
+
+QuoteMark.propTypes = {
+ color: PropTypes.string
+};
+
+export default QuoteMark;
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
new file mode 100644
index 000000000..621c409e9
--- /dev/null
+++ b/app/containers/message/Reply.js
@@ -0,0 +1,155 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+
+import Markdown from './Markdown';
+import QuoteMark from './QuoteMark';
+import Avatar from '../Avatar';
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 2,
+ alignSelf: 'flex-end'
+ },
+ quoteSign: {
+ borderWidth: 2,
+ borderRadius: 4,
+ borderColor: '#a0a0a0',
+ height: '100%',
+ marginRight: 5
+ },
+ attachmentContainer: {
+ flex: 1,
+ flexDirection: 'column'
+ },
+ authorContainer: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ author: {
+ fontWeight: 'bold',
+ marginHorizontal: 5,
+ flex: 1
+ },
+ time: {
+ fontSize: 10,
+ fontWeight: 'normal',
+ color: '#888',
+ marginLeft: 5
+ },
+ fieldsContainer: {
+ flex: 1,
+ flexWrap: 'wrap',
+ flexDirection: 'row'
+ },
+ fieldContainer: {
+ flexDirection: 'column',
+ padding: 10
+ },
+ fieldTitle: {
+ fontWeight: 'bold'
+ }
+});
+
+const onPress = (attachment) => {
+ const url = attachment.title_link || attachment.author_link;
+ if (!url) {
+ return;
+ }
+ Linking.openURL(attachment.title_link || attachment.author_link);
+};
+
+// Support
+const formatText = text =>
+ text.replace(
+ new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
+ (match, url, title) => `[${ title }](${ url })`
+ );
+
+const Reply = ({ attachment, timeFormat }) => {
+ if (!attachment) {
+ return null;
+ }
+
+ const renderAvatar = () => {
+ if (!attachment.author_icon && !attachment.author_name) {
+ return null;
+ }
+ return (
+
+ );
+ };
+
+ const renderAuthor = () => (
+ attachment.author_name ? {attachment.author_name} : null
+ );
+
+ const renderTime = () => {
+ const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
+ return time ? { time } : null;
+ };
+
+ const renderTitle = () => {
+ if (!(attachment.author_icon || attachment.author_name || attachment.ts)) {
+ return null;
+ }
+ return (
+
+ {renderAvatar()}
+ {renderAuthor()}
+ {renderTime()}
+
+ );
+ };
+
+ const renderText = () => (
+ attachment.text ? : null
+ );
+
+ const renderFields = () => {
+ if (!attachment.fields) {
+ return null;
+ }
+
+ return (
+
+ {attachment.fields.map(field => (
+
+ {field.title}
+ {field.value}
+
+ ))}
+
+ );
+ };
+
+ return (
+ onPress(attachment)}
+ style={styles.button}
+ >
+
+
+ {renderTitle()}
+ {renderText()}
+ {renderFields()}
+ {attachment.attachments.map(attach => )}
+
+
+ );
+};
+
+Reply.propTypes = {
+ attachment: PropTypes.object.isRequired,
+ timeFormat: PropTypes.string.isRequired
+};
+
+export default Reply;
diff --git a/app/containers/message/Url.js b/app/containers/message/Url.js
new file mode 100644
index 000000000..bd930a53d
--- /dev/null
+++ b/app/containers/message/Url.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, Linking, StyleSheet, Image } from 'react-native';
+import PropTypes from 'prop-types';
+
+import QuoteMark from './QuoteMark';
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 2
+ },
+ quoteSign: {
+ borderWidth: 2,
+ borderRadius: 4,
+ borderColor: '#a0a0a0',
+ height: '100%',
+ marginRight: 5
+ },
+ image: {
+ height: 80,
+ width: 80,
+ resizeMode: 'cover',
+ borderRadius: 6
+ },
+ textContainer: {
+ flex: 1,
+ height: '100%',
+ flexDirection: 'column',
+ padding: 4,
+ justifyContent: 'flex-start',
+ alignItems: 'flex-start'
+ },
+ title: {
+ fontWeight: 'bold',
+ fontSize: 12
+ },
+ description: {
+ fontSize: 12
+ }
+});
+
+const onPress = (url) => {
+ Linking.openURL(url);
+};
+const Url = ({ url }) => {
+ if (!url) {
+ return null;
+ }
+ return (
+ onPress(url.url)} style={styles.button}>
+
+
+
+ {url.title}
+ {url.description}
+
+
+ );
+};
+
+Url.propTypes = {
+ url: PropTypes.object.isRequired
+};
+
+export default Url;
diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js
new file mode 100644
index 000000000..4955bc9a9
--- /dev/null
+++ b/app/containers/message/Video.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View, StyleSheet, TouchableOpacity, Image, Linking, Platform } from 'react-native';
+import Modal from 'react-native-modal';
+import VideoPlayer from 'react-native-video-controls';
+import Markdown from './Markdown';
+
+const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(Platform.OS === 'ios' ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
+const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ height: 100,
+ margin: 5
+ },
+ modal: {
+ margin: 0,
+ backgroundColor: '#000'
+ },
+ image: {
+ flex: 1,
+ width: null,
+ height: null,
+ resizeMode: 'contain'
+ }
+});
+
+export default class Video extends React.PureComponent {
+ static propTypes = {
+ file: PropTypes.object.isRequired,
+ baseUrl: PropTypes.string.isRequired,
+ user: PropTypes.object.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+ const { baseUrl, file, user } = props;
+ this.state = {
+ isVisible: false,
+ uri: `${ baseUrl }${ file.video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
+ };
+ }
+
+
+ toggleModal() {
+ this.setState({
+ isVisible: !this.state.isVisible
+ });
+ }
+
+ open() {
+ if (isTypeSupported(this.props.file.video_type)) {
+ return this.toggleModal();
+ }
+ Linking.openURL(this.state.uri);
+ }
+
+ render() {
+ const { isVisible, uri } = this.state;
+ const { description } = this.props.file;
+ return (
+
+ this.open()}
+ >
+
+
+
+
+ this.toggleModal()}
+ disableVolume
+ />
+
+
+ );
+ }
+}
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 4cc67c123..9f4d8b9d6 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -1,14 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
-import { emojify } from 'react-emojione';
-import Markdown from 'react-native-easy-markdown'; // eslint-disable-line
import { connect } from 'react-redux';
import { actionsShow } from '../../actions/messages';
-import Card from './Card';
+import Image from './Image';
import User from './User';
import Avatar from '../Avatar';
+import Audio from './Audio';
+import Video from './Video';
+import Markdown from './Markdown';
+import Url from './Url';
+import Reply from './Reply';
const styles = StyleSheet.create({
content: {
@@ -43,6 +46,7 @@ export default class Message extends React.Component {
baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
message: PropTypes.object.isRequired,
+ user: PropTypes.object.isRequired,
editing: PropTypes.bool,
actionsShow: PropTypes.func
}
@@ -53,28 +57,48 @@ export default class Message extends React.Component {
}
isDeleted() {
- return !this.props.item.msg;
+ return this.props.item.t === 'rm';
+ }
+
+ isPinned() {
+ return this.props.item.t === 'message_pinned';
}
attachments() {
- return this.props.item.attachments.length ? (
-
- ) : null;
+ if (this.props.item.attachments.length === 0) {
+ return null;
+ }
+
+ const file = this.props.item.attachments[0];
+ const { baseUrl, user } = this.props;
+ if (file.image_type) {
+ return ;
+ } else if (file.audio_type) {
+ return ;
+ } else if (file.video_type) {
+ return ;
+ }
+
+ return ;
}
renderMessageContent() {
if (this.isDeleted()) {
return Message removed;
+ } else if (this.isPinned()) {
+ return Message pinned;
+ }
+ return ;
+ }
+
+ renderUrl() {
+ if (this.props.item.urls.length === 0) {
+ return null;
}
- const msg = emojify(this.props.item.msg, { output: 'unicode' });
- return (
-
- {msg}
-
- );
+ return this.props.item.urls.map(url => (
+
+ ));
}
render() {
@@ -110,8 +134,9 @@ export default class Message extends React.Component {
Message_TimeFormat={this.props.Message_TimeFormat}
baseUrl={this.props.baseUrl}
/>
+ {this.renderMessageContent()}
{this.attachments()}
- {this.renderMessageContent(item)}
+ {this.renderUrl()}
);
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index ed70deb74..d39bc8a6e 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -28,7 +28,7 @@ const AuthRoutes = StackNavigator(
screen: RoomView,
navigationOptions({ navigation }) {
return {
- title: navigation.state.params.title || 'Room'
+ title: navigation.state.params.name || navigation.state.params.room.name || 'Room'
// [drawerIconPosition]: ()รท
};
}
diff --git a/app/containers/routes/NavigationService.js b/app/containers/routes/NavigationService.js
index 0be7f410b..38f0e9690 100644
--- a/app/containers/routes/NavigationService.js
+++ b/app/containers/routes/NavigationService.js
@@ -1,4 +1,5 @@
import { NavigationActions } from 'react-navigation';
+import reduxStore from '../../lib/createStore';
const config = {};
@@ -21,3 +22,24 @@ export function goBack() {
config.navigator.dispatch(action);
}
}
+
+
+export function goRoom({ rid, name }, counter = 0) {
+ // about counter: we can call this method before navigator be set. so we have to wait, if we tried a lot, we give up ...
+ if (!rid || !name || counter > 10) {
+ return;
+ }
+ if (!config.navigator) {
+ return setTimeout(() => goRoom({ rid, name }, counter + 1), 200);
+ }
+
+ const action = NavigationActions.reset({
+ index: 1,
+ actions: [
+ NavigationActions.navigate({ routeName: 'RoomsList' }),
+ NavigationActions.navigate({ routeName: 'Room', params: { room: { rid, name }, rid, name } })
+ ]
+ });
+
+ requestAnimationFrame(() => config.navigator.dispatch(action), reduxStore.getState().app.starting);
+}
diff --git a/app/lib/createStore.js b/app/lib/createStore.js
index 7a2f79a52..f894b5437 100644
--- a/app/lib/createStore.js
+++ b/app/lib/createStore.js
@@ -2,6 +2,7 @@ import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import { composeWithDevTools } from 'remote-redux-devtools';
+import applyAppStateListener from 'redux-enhancer-react-native-appstate';
import reducers from '../reducers';
import sagas from '../sagas';
@@ -13,12 +14,16 @@ if (__DEV__) {
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
enhacers = composeWithDevTools(
+ applyAppStateListener(),
applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware),
applyMiddleware(logger)
);
} else {
- enhacers = composeWithDevTools(applyMiddleware(sagaMiddleware));
+ enhacers = composeWithDevTools(
+ applyAppStateListener(),
+ applyMiddleware(sagaMiddleware)
+ );
}
const store = enhacers(createStore)(reducers);
diff --git a/app/lib/realm.js b/app/lib/realm.js
index bc36b6940..c98d3867d 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -95,24 +95,55 @@ const usersSchema = {
}
};
+const attachmentFields = {
+ name: 'attachmentFields',
+ properties: {
+ title: { type: 'string', optional: true },
+ value: { type: 'string', optional: true },
+ short: { type: 'bool', optional: true }
+ }
+};
+
const attachment = {
name: 'attachment',
properties: {
description: { type: 'string', optional: true },
-
image_size: { type: 'int', optional: true },
-
image_type: { type: 'string', optional: true },
-
image_url: { type: 'string', optional: true },
+ audio_size: { type: 'int', optional: true },
+ audio_type: { type: 'string', optional: true },
+ audio_url: { type: 'string', optional: true },
+ video_size: { type: 'int', optional: true },
+ video_type: { type: 'string', optional: true },
+ video_url: { type: 'string', optional: true },
title: { type: 'string', optional: true },
-
title_link: { type: 'string', optional: true },
title_link_download: { type: 'bool', optional: true },
- type: { type: 'string', optional: true }
+ type: { type: 'string', optional: true },
+ author_icon: { type: 'string', optional: true },
+ author_name: { type: 'string', optional: true },
+ author_link: { type: 'string', optional: true },
+ text: { type: 'string', optional: true },
+ color: { type: 'string', optional: true },
+ ts: { type: 'date', optional: true },
+ attachments: { type: 'list', objectType: 'attachment' },
+ fields: { type: 'list', objectType: 'attachmentFields' }
}
};
+const url = {
+ name: 'url',
+ properties: {
+ _id: 'int',
+ url: { type: 'string', optional: true },
+ title: { type: 'string', optional: true },
+ description: { type: 'string', optional: true },
+ image: { type: 'string', optional: true }
+ }
+};
+
+
const messagesEditedBySchema = {
name: 'messagesEditedBy',
properties: {
@@ -128,6 +159,7 @@ const messagesSchema = {
_id: 'string',
_server: 'servers',
msg: { type: 'string', optional: true },
+ t: { type: 'string', optional: true },
rid: 'string',
ts: 'date',
u: 'users',
@@ -138,6 +170,7 @@ const messagesSchema = {
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
+ urls: { type: 'list', objectType: 'url' },
_updatedAt: { type: 'date', optional: true },
temp: { type: 'bool', optional: true },
pinned: { type: 'bool', optional: true },
@@ -158,9 +191,11 @@ const realm = new Realm({
usersSchema,
roomsSchema,
attachment,
+ attachmentFields,
messagesEditedBySchema,
permissionsSchema,
- permissionsRolesSchema
+ permissionsRolesSchema,
+ url
],
deleteRealmIfMigrationNeeded: true
});
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 52c4e573a..885900b6d 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -63,14 +63,9 @@ const RocketChat = {
Meteor.ddp.on('connected', async() => {
Meteor.ddp.on('changed', (ddpMessage) => {
- const server = { id: reduxStore.getState().server.server };
if (ddpMessage.collection === 'stream-room-messages') {
return realm.write(() => {
- const message = ddpMessage.fields.args[0];
- message.temp = false;
- message._server = server;
- message.attachments = message.attachments || [];
- message.starred = message.starred && message.starred.length > 0;
+ const message = this._buildMessage(ddpMessage.fields.args[0]);
realm.create('messages', message, true);
});
}
@@ -246,6 +241,35 @@ const RocketChat = {
return call('raix:push-setuser', pushId);
},
+ _parseUrls(urls) {
+ return urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => {
+ const tmp = {};
+ const { meta } = url;
+ tmp._id = index;
+ tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle;
+ tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName;
+ let decodedOgImage;
+ if (meta.ogImage) {
+ decodedOgImage = meta.ogImage.replace(/&/g, '&');
+ }
+ tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl;
+ tmp.url = url.url;
+ return tmp;
+ });
+ },
+ _buildMessage(message) {
+ const { server } = reduxStore.getState().server;
+ message.temp = false;
+ message._server = { id: server };
+ message.attachments = message.attachments || [];
+ if (message.urls) {
+ message.urls = RocketChat._parseUrls(message.urls);
+ }
+ // loadHistory returns message.starred as object
+ // stream-room-messages returns message.starred as an array
+ message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred);
+ return message;
+ },
loadMessagesForRoom(rid, end, cb) {
return new Promise((resolve, reject) => {
Meteor.call('loadHistory', rid, end, 20, (err, data) => {
@@ -256,13 +280,9 @@ const RocketChat = {
return reject(err);
}
if (data && data.messages.length) {
+ const messages = data.messages.map(message => this._buildMessage(message));
realm.write(() => {
- data.messages.forEach((message) => {
- message.temp = false;
- message._server = { id: reduxStore.getState().server.server };
- message.attachments = message.attachments || [];
- // write('messages', message);
- message.starred = !!message.starred;
+ messages.forEach((message) => {
realm.create('messages', message, true);
});
});
@@ -500,6 +520,12 @@ const RocketChat = {
emitTyping(room, t = true) {
const { login } = reduxStore.getState();
return call('stream-notify-room', `${ room }/typing`, login.user.username, t);
+ },
+ setUserPresenceAway() {
+ return call('UserPresence:away');
+ },
+ setUserPresenceOnline() {
+ return call('UserPresence:online');
}
};
diff --git a/app/push.js b/app/push.js
index df9936369..6c0687d8b 100644
--- a/app/push.js
+++ b/app/push.js
@@ -1,6 +1,15 @@
import PushNotification from 'react-native-push-notification';
import { AsyncStorage } from 'react-native';
+import EJSON from 'ejson';
+import { goRoom } from './containers/routes/NavigationService';
+const handleNotification = (notification) => {
+ if (notification.usernInteraction) {
+ return;
+ }
+ const { rid, name } = EJSON.parse(notification.ejson);
+ return rid && name && goRoom({ rid, name });
+};
PushNotification.configure({
// (optional) Called when Token is generated (iOS and Android)
@@ -9,9 +18,7 @@ PushNotification.configure({
},
// (required) Called when a remote or local notification is opened or received
- onNotification(notification) {
- console.log('NOTIFICATION:', notification);
- },
+ onNotification: handleNotification,
// ANDROID ONLY: GCM Sender ID (optional - not required for local notifications, but is need to receive remote push notifications)
senderID: '673693445664',
@@ -25,7 +32,7 @@ PushNotification.configure({
// Should the initial notification be popped automatically
// default: true
- popInitialNotification: false,
+ popInitialNotification: true,
/**
* (optional) default: true
diff --git a/app/reducers/app.js b/app/reducers/app.js
index 3ae8bb9c5..486e54a95 100644
--- a/app/reducers/app.js
+++ b/app/reducers/app.js
@@ -1,19 +1,46 @@
+import { FOREGROUND, BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate';
import { APP } from '../actions/actionsTypes';
const initialState = {
- starting: true
+ starting: true,
+ ready: false,
+ inactive: false,
+ background: false
};
export default function app(state = initialState, action) {
switch (action.type) {
+ case FOREGROUND:
+ return {
+ ...state,
+ inactive: false,
+ foreground: true,
+ background: false
+ };
+ case BACKGROUND:
+ return {
+ ...state,
+ inactive: false,
+ foreground: false,
+ background: true
+ };
+ case INACTIVE:
+ return {
+ ...state,
+ inactive: true,
+ foreground: false,
+ background: false
+ };
case APP.INIT:
return {
...state,
+ ready: false,
starting: true
};
case APP.READY:
return {
...state,
+ ready: true,
starting: false
};
default:
diff --git a/app/reducers/login.js b/app/reducers/login.js
index e90531ffd..83b3187b6 100644
--- a/app/reducers/login.js
+++ b/app/reducers/login.js
@@ -11,6 +11,8 @@ const initialState = {
export default function login(state = initialState, action) {
switch (action.type) {
+ case types.APP.INIT:
+ return initialState;
case types.LOGIN.REQUEST:
return {
...state,
diff --git a/app/sagas/index.js b/app/sagas/index.js
index fcc8fd72f..068401c54 100644
--- a/app/sagas/index.js
+++ b/app/sagas/index.js
@@ -7,6 +7,7 @@ import messages from './messages';
import selectServer from './selectServer';
import createChannel from './createChannel';
import init from './init';
+import state from './state';
const root = function* root() {
yield all([
@@ -17,8 +18,9 @@ const root = function* root() {
login(),
connect(),
messages(),
- selectServer()
+ selectServer(),
+ state()
]);
};
-// Consider using takeEvery
+
export default root;
diff --git a/app/sagas/init.js b/app/sagas/init.js
index 024c26b45..5117c86ca 100644
--- a/app/sagas/init.js
+++ b/app/sagas/init.js
@@ -1,5 +1,5 @@
import { AsyncStorage } from 'react-native';
-import { call, put, take } from 'redux-saga/effects';
+import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../actions';
import { setServer } from '../actions/server';
import { restoreToken } from '../actions/login';
@@ -9,7 +9,6 @@ import RocketChat from '../lib/rocketchat';
const restore = function* restore() {
try {
- yield take(APP.INIT);
const token = yield call([AsyncStorage, 'getItem'], 'reactnativemeteor_usertoken');
if (token) {
yield put(restoreToken(token));
@@ -28,4 +27,8 @@ const restore = function* restore() {
console.log(e);
}
};
-export default restore;
+
+const root = function* root() {
+ yield takeLatest(APP.INIT, restore);
+};
+export default root;
diff --git a/app/sagas/login.js b/app/sagas/login.js
index eb767e87f..4941756d3 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -130,7 +130,9 @@ const handleSetUsernameRequest = function* handleSetUsernameRequest({ credential
const handleLogout = function* handleLogout() {
const server = yield select(getServer);
- yield call(logoutCall, { server });
+ if (server) {
+ yield call(logoutCall, { server });
+ }
};
const handleRegisterIncomplete = function* handleRegisterIncomplete() {
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index 0610babe2..747da4256 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -1,5 +1,6 @@
import { put, call, takeLatest, take, select, race, fork, cancel } from 'redux-saga/effects';
import { delay } from 'redux-saga';
+import { FOREGROUND } from 'redux-enhancer-react-native-appstate';
import * as types from '../actions/actionsTypes';
import { roomsSuccess, roomsFailure } from '../actions/rooms';
import { addUserTyping, removeUserTyping } from '../actions/room';
@@ -60,7 +61,6 @@ const watchRoomOpen = function* watchRoomOpen({ room }) {
if (open) {
return;
}
-
RocketChat.readMessages(room.rid);
subscriptions.push(RocketChat.subscribe('stream-room-messages', room.rid, false));
subscriptions.push(RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false));
@@ -89,9 +89,18 @@ const watchuserTyping = function* watchuserTyping({ status }) {
}
};
+const updateRoom = function* updateRoom() {
+ const room = yield select(state => state.room);
+ if (!room || !room.rid) {
+ return;
+ }
+ yield put(messagesRequest({ rid: room.rid }));
+};
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
+ yield takeLatest(FOREGROUND, updateRoom);
+ yield takeLatest(FOREGROUND, watchRoomsRequest);
};
export default root;
diff --git a/app/sagas/state.js b/app/sagas/state.js
new file mode 100644
index 000000000..58b9b7b92
--- /dev/null
+++ b/app/sagas/state.js
@@ -0,0 +1,37 @@
+import { takeLatest, select } from 'redux-saga/effects';
+import { FOREGROUND, BACKGROUND, INACTIVE } from 'redux-enhancer-react-native-appstate';
+
+import RocketChat from '../lib/rocketchat';
+
+const appHasComeBackToForeground = function* appHasComeBackToForeground() {
+ const auth = yield select(state => state.login.isAuthenticated);
+ if (!auth) {
+ return;
+ }
+ return yield RocketChat.setUserPresenceOnline();
+};
+
+const appHasComeBackToBackground = function* appHasComeBackToBackground() {
+ const auth = yield select(state => state.login.isAuthenticated);
+ if (!auth) {
+ return;
+ }
+ return yield RocketChat.setUserPresenceAway();
+};
+
+const root = function* root() {
+ yield takeLatest(
+ FOREGROUND,
+ appHasComeBackToForeground
+ );
+ yield takeLatest(
+ BACKGROUND,
+ appHasComeBackToBackground
+ );
+ yield takeLatest(
+ INACTIVE,
+ appHasComeBackToBackground
+ );
+};
+
+export default root;
diff --git a/app/views/RoomView.js b/app/views/RoomView.js
index e4c4ad80a..474930577 100644
--- a/app/views/RoomView.js
+++ b/app/views/RoomView.js
@@ -55,7 +55,8 @@ const typing = () => ;
server: state.server.server,
Site_Url: state.settings.Site_Url,
Message_TimeFormat: state.settings.Message_TimeFormat,
- loading: state.messages.isFetching
+ loading: state.messages.isFetching,
+ user: state.login.user
}),
dispatch => ({
actions: bindActionCreators(actions, dispatch),
@@ -67,10 +68,10 @@ export default class RoomView extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
openRoom: PropTypes.func.isRequired,
+ user: PropTypes.object.isRequired,
editCancel: PropTypes.func,
rid: PropTypes.string,
server: PropTypes.string,
- sid: PropTypes.string,
name: PropTypes.string,
Site_Url: PropTypes.string,
Message_TimeFormat: PropTypes.string,
@@ -79,12 +80,12 @@ export default class RoomView extends React.Component {
constructor(props) {
super(props);
-
- this.sid = props.navigation.state.params.room.sid;
this.rid =
props.rid ||
- props.navigation.state.params.room.rid ||
- realm.objectForPrimaryKey('subscriptions', this.sid).rid;
+ props.navigation.state.params.room.rid;
+ this.name = this.props.name ||
+ this.props.navigation.state.params.name ||
+ this.props.navigation.state.params.room.name;
this.data = realm
.objects('messages')
@@ -92,7 +93,6 @@ export default class RoomView extends React.Component {
.sorted('ts', true);
this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
- slow: false,
dataSource: ds.cloneWithRows([]),
loaded: true,
joined: typeof props.rid === 'undefined'
@@ -101,21 +101,14 @@ export default class RoomView extends React.Component {
componentWillMount() {
this.props.navigation.setParams({
- title:
- this.props.name ||
- this.props.navigation.state.params.room.name ||
- realm.objectForPrimaryKey('subscriptions', this.sid).name
+ title: this.name
});
- this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
- this.props.openRoom({ rid: this.rid });
+ this.props.openRoom({ rid: this.rid, name: this.name });
this.data.addListener(this.updateState);
}
componentDidMount() {
this.updateState();
}
- componentDidUpdate() {
- return !this.props.loading && clearTimeout(this.timer);
- }
componentWillUnmount() {
clearTimeout(this.timer);
this.data.removeAllListeners();
@@ -160,7 +153,7 @@ export default class RoomView extends React.Component {
};
renderBanner = () =>
- (this.state.slow && this.props.loading ? (
+ (this.props.loading ? (
Loading new messages...
@@ -172,6 +165,7 @@ export default class RoomView extends React.Component {
item={item}
baseUrl={this.props.Site_Url}
Message_TimeFormat={this.props.Message_TimeFormat}
+ user={this.props.user}
/>
);
diff --git a/app/views/RoomsListView.js b/app/views/RoomsListView.js
index b6ffcfe17..a9277ec9c 100644
--- a/app/views/RoomsListView.js
+++ b/app/views/RoomsListView.js
@@ -11,6 +11,7 @@ import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import RoomItem from '../presentation/RoomItem';
import Banner from '../containers/Banner';
+import { goRoom } from '../containers/routes/NavigationService';
const styles = StyleSheet.create({
container: {
@@ -191,11 +192,7 @@ export default class RoomsListView extends React.Component {
});
};
- _onPressItem = (id, item = {}) => {
- const navigateToRoom = (room) => {
- this.props.navigation.navigate('Room', { room, title: room.name });
- };
-
+ _onPressItem = (item = {}) => {
const clearSearch = () => {
this.setState({
searchText: ''
@@ -220,16 +217,16 @@ export default class RoomsListView extends React.Component {
}
});
}))
- .then(sub => navigateToRoom({ sid: sub._id, name: sub.name }))
+ .then(sub => goRoom({ room: sub, name: sub.name }))
.then(() => clearSearch());
} else {
clearSearch();
- navigateToRoom({ rid: item._id, name: item.name });
+ goRoom(item);
}
return;
}
- navigateToRoom({ sid: id, name: item.name });
+ goRoom(item);
clearSearch();
}
@@ -259,12 +256,12 @@ export default class RoomsListView extends React.Component {
userMentions={item.userMentions}
favorite={item.f}
name={item.name}
- _updatedAt={item._updatedAt}
+ _updatedAt={item.roomUpdatedAt}
key={item._id}
type={item.t}
baseUrl={this.props.Site_Url}
dateFormat='MM-DD-YYYY HH:mm:ss'
- onPress={() => this._onPressItem(item._id, item)}
+ onPress={() => this._onPressItem(item)}
/>
)
diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj
index ccca670e4..136e7b6a7 100644
--- a/ios/RocketChatRN.xcodeproj/project.pbxproj
+++ b/ios/RocketChatRN.xcodeproj/project.pbxproj
@@ -48,6 +48,7 @@
77C35F50C01C43668188886C /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A0EEFAF8AB14F5B9E796CDD /* libRNVectorIcons.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
8A159EDB97C44E52AF62D69C /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA50CE47374C4C35BE6D9D58 /* libRNSVG.a */; };
+ 8ECBD927DDAC4987B98E102E /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */; };
AE5D35882AE04CC29630FB3D /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = DC6EE17B5550465E98C70FF0 /* Entypo.ttf */; };
B88F586F1FBF57F600B352B8 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B88F58461FBF55E200B352B8 /* libRCTPushNotification.a */; };
B8E79AF41F3CD167005B464F /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB61A68108700A75B9A /* Info.plist */; };
@@ -290,6 +291,20 @@
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RCTLinking;
};
+ 7A7F5C981FCC982500024129 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTVideo;
+ };
+ 7A7F5C9A1FCC982500024129 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 641E28441F0EEC8500443AF6;
+ remoteInfo = "RCTVideo-tvOS";
+ };
832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
@@ -391,6 +406,7 @@
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = RocketChatRN/main.m; sourceTree = ""; };
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; };
1B0746E708284151B8AD1198 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; };
+ 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTVideo.a; sourceTree = ""; };
22A8B76C8EBA443BB97CE82D /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = ""; };
2D02E47B1E0B4A5D006451C7 /* RocketChatRN-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RocketChatRN-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
2D02E4901E0B4A5D006451C7 /* RocketChatRN-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RocketChatRN-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -411,6 +427,7 @@
8A2DD67ADD954AD9873F45FC /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; };
9A1E1766CCB84C91A62BD5A6 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; };
A18EFC3B0CFE40E0918A8F0C /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; };
+ AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTVideo.xcodeproj; path = "../node_modules/react-native-video/ios/RCTVideo.xcodeproj"; sourceTree = ""; };
B37C79D9BD0742CE936B6982 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
B88F58361FBF55E200B352B8 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; };
BAAE4B947F5D44959F0A9D5A /* libRNZeroconf.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNZeroconf.a; sourceTree = ""; };
@@ -455,6 +472,7 @@
77C35F50C01C43668188886C /* libRNVectorIcons.a in Frameworks */,
8A159EDB97C44E52AF62D69C /* libRNSVG.a in Frameworks */,
C758F0BD5C3244E2BA073E61 /* libRNImagePicker.a in Frameworks */,
+ 8ECBD927DDAC4987B98E102E /* libRCTVideo.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -640,6 +658,15 @@
name = Products;
sourceTree = "";
};
+ 7A7F5C831FCC982500024129 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7A7F5C991FCC982500024129 /* libRCTVideo.a */,
+ 7A7F5C9B1FCC982500024129 /* libRCTVideo.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -661,6 +688,7 @@
22A8B76C8EBA443BB97CE82D /* RNVectorIcons.xcodeproj */,
C23AEF1D9EBE4A38A1A6B97B /* RNSVG.xcodeproj */,
4B38C7E37A8748E0BC665078 /* RNImagePicker.xcodeproj */,
+ AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */,
);
name = Libraries;
sourceTree = "";
@@ -735,6 +763,7 @@
5A0EEFAF8AB14F5B9E796CDD /* libRNVectorIcons.a */,
DA50CE47374C4C35BE6D9D58 /* libRNSVG.a */,
3B696712EE2345A59F007A88 /* libRNImagePicker.a */,
+ 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */,
);
name = "Recovered References";
sourceTree = "";
@@ -936,6 +965,10 @@
ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */;
ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
},
+ {
+ ProductGroup = 7A7F5C831FCC982500024129 /* Products */;
+ ProjectRef = AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */;
+ },
{
ProductGroup = 139FDEE71B06529A00C62182 /* Products */;
ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
@@ -1197,6 +1230,20 @@
remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
+ 7A7F5C991FCC982500024129 /* libRCTVideo.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTVideo.a;
+ remoteRef = 7A7F5C981FCC982500024129 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 7A7F5C9B1FCC982500024129 /* libRCTVideo.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTVideo.a;
+ remoteRef = 7A7F5C9A1FCC982500024129 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1434,6 +1481,7 @@
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = RocketChatRNTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@@ -1442,6 +1490,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@@ -1468,6 +1518,7 @@
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = RocketChatRNTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
@@ -1476,6 +1527,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@@ -1507,6 +1560,7 @@
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
"$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/**",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1543,6 +1597,7 @@
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
"$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj/**",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1582,6 +1637,7 @@
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = "RocketChatRN-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1589,6 +1645,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@@ -1625,6 +1683,7 @@
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
"$(SRCROOT)/../node_modules/react-native-autogrow-textinput/ios",
+ "$(SRCROOT)/../node_modules/react-native-video/ios",
);
INFOPLIST_FILE = "RocketChatRN-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1632,6 +1691,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@@ -1663,6 +1724,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.RocketChatRN-tvOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1690,6 +1753,8 @@
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
+ "\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.RocketChatRN-tvOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/package.json b/package.json
index 11a2a0a4e..5c3429de7 100644
--- a/package.json
+++ b/package.json
@@ -24,15 +24,15 @@
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-remove-console": "^6.8.5",
"babel-polyfill": "^6.26.0",
- "moment": "^2.19.2",
+ "ejson": "^2.1.2",
+ "moment": "^2.19.3",
"prop-types": "^15.6.0",
- "react": "16.1.1",
+ "react": "^16.2.0",
"react-emojione": "^5.0.0",
- "react-native": "0.50.3",
- "react-native-action-button": "^2.8.1",
+ "react-native": "^0.50.4",
+ "react-native-action-button": "^2.8.3",
"react-native-actionsheet": "^2.3.0",
"react-native-animatable": "^1.2.4",
- "react-native-card-view": "0.0.3",
"react-native-easy-markdown": "git+https://github.com/lappalj4/react-native-easy-markdown.git",
"react-native-fetch-blob": "^0.10.8",
"react-native-image-picker": "^0.26.7",
@@ -43,42 +43,46 @@
"react-native-modal": "^4.1.1",
"react-native-optimized-flatlist": "^1.0.3",
"react-native-push-notification": "^3.0.1",
+ "react-native-slider": "^0.11.0",
"react-native-svg": "^6.0.0",
"react-native-svg-image": "^2.0.1",
"react-native-vector-icons": "^4.4.2",
+ "react-native-video": "^2.0.0",
+ "react-native-video-controls": "^2.0.0",
"react-native-zeroconf": "^0.8.3",
"react-navigation": "^1.0.0-beta.19",
"react-redux": "^5.0.6",
- "realm": "^2.0.7",
+ "realm": "^2.0.11",
"redux": "^3.7.2",
+ "redux-enhancer-react-native-appstate": "^0.3.0",
"redux-immutable-state-invariant": "^2.1.0",
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.0",
"regenerator-runtime": "^0.11.0",
"remote-redux-devtools": "^0.5.12",
- "strip-ansi": "^4.0.0",
- "snyk": "^1.41.1"
+ "@storybook/react-native": "^3.2.15",
+ "snyk": "^1.41.1",
+ "strip-ansi": "^4.0.0"
},
"devDependencies": {
"@storybook/addon-storyshots": "^3.2.15",
- "@storybook/react-native": "^3.2.15",
"babel-eslint": "^8.0.2",
"babel-jest": "21.2.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react-native": "4.0.0",
"codecov": "^3.0.0",
- "eslint": "^4.11.0",
+ "eslint": "^4.12.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
- "eslint-plugin-react": "^7.4.0",
- "eslint-plugin-react-native": "^3.1.0",
+ "eslint-plugin-react": "^7.5.1",
+ "eslint-plugin-react-native": "^3.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "21.2.1",
"jest-cli": "^21.2.1",
- "react-dom": "16.1.1",
- "react-test-renderer": "16.1.1"
+ "react-dom": "^16.2.0",
+ "react-test-renderer": "^16.2.0"
},
"jest": {
"preset": "react-native",
diff --git a/storybook/storybook.js b/storybook/storybook.js
index 17f1fef97..841d13edd 100644
--- a/storybook/storybook.js
+++ b/storybook/storybook.js
@@ -1,6 +1,4 @@
-/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, global-require */
-
-import { Navigation } from 'react-native-navigation';
+import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@storybook/react-native';
// import stories
@@ -11,15 +9,6 @@ configure(() => {
// This assumes that storybook is running on the same host as your RN packager,
// to set manually use, e.g. host: 'localhost' option
const StorybookUI = getStorybookUI({ port: 7007, onDeviceUI: true });
-Navigation.registerComponent('storybook.UI', () => StorybookUI);
-Navigation.startSingleScreenApp({
- screen: {
- screen: 'storybook.UI',
- title: 'Storybook',
- navigatorStyle: {
- navBarHidden: true
- }
- }
-});
+AppRegistry.registerComponent('RocketChatRN', () => StorybookUI);
export default StorybookUI;