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 ); 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;