diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c41804c..449d859 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -25,6 +25,12 @@
+
+
+
+
+
diff --git a/app/api/gitter.js b/app/api/gitter.js
index b661e82..8701a65 100644
--- a/app/api/gitter.js
+++ b/app/api/gitter.js
@@ -63,6 +63,14 @@ export function currentUserSuggested(token, id) {
return callApi(endpoint, token)
}
+/**
+ * Groups
+ */
+
+export function groupRooms(token, id) {
+ return callApi(`groups/${id}/rooms`, token)
+}
+
/**
* Rooms resource
*/
@@ -71,6 +79,10 @@ export function room(token, id) {
return callApi('rooms/' + id, token)
}
+export function roomsByUri(token, uri) {
+ return callApi(`rooms?q=${uri}`, token)
+}
+
export function roomMessages(token, id, limit) {
return callApi(`rooms/${id}/chatMessages?limit=${limit}`, token)
}
diff --git a/app/components/ParsedText/index.js b/app/components/ParsedText/index.js
index 7f7381e..d9b7ed3 100644
--- a/app/components/ParsedText/index.js
+++ b/app/components/ParsedText/index.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
-import {Text} from 'react-native';
+import {Text, Linking} from 'react-native'
import Parser from 'react-native-parsed-text'
import Emoji from '../Emoji'
import s from './styles'
@@ -33,9 +33,9 @@ const renderCodespan = (matchingString, matches) => {
return component
}
-const ParsedText = ({text, username, handleUrlPress}) => {
+const ParsedText = ({text, username}) => {
const patterns = [
- {type: 'url', style: s.url, onPress: handleUrlPress},
+ {type: 'url', style: s.url, onPress: (url) => Linking.openURL(url)},
{pattern: new RegExp(`@${username}`), style: s.selfMention},
{pattern: MENTION_REGEX, style: s.mention},
{pattern: GROUP_MENTION_REGEX, style: s.groupMention},
diff --git a/app/constants.js b/app/constants.js
index 6eb8c67..9d0d732 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -55,3 +55,10 @@ export const icons = {
'forward': iOS ? {icon: 'chevron-right', color: 'white', size: 40} : {icon: 'arrow-forward', color: 'white', size: 24},
'expand-more': {icon: 'expand-more', color: 'white', size: 24}
}
+
+export const GITTER_REGEXPS = {
+ baseUrl: /\bhttps:\/\/gitter.im\/\b/,
+ roomParamsExp: /([a-zA-Z0-9]+)\/\b([a-zA-Z0-9]+)\b/,
+ messageParamsExp: /\b([a-zA-Z0-9/]+)\b\?at=([a-z0-9]{24})/,
+ groupParamsExp: /([a-zA-Z0-9]+)\/\bhome\b/
+}
diff --git a/app/modules/groups.js b/app/modules/groups.js
new file mode 100644
index 0000000..d93a532
--- /dev/null
+++ b/app/modules/groups.js
@@ -0,0 +1,135 @@
+import _ from 'lodash'
+import * as Api from '../api/gitter'
+import {LOGOUT} from './auth'
+
+/**
+ * Action Creators
+ */
+
+export const GROUP_ROOMS = 'groups/GROUPS_ROOMS'
+export const GROUP_ROOMS_RECEIVED = 'groups/GROUP_ROOMS_RECEIVED'
+export const GROUP_FOUND = 'groups/GROUP_FOUND'
+export const GROUP_ROOMS_FAILED = 'groups/GROUP_ROOMS_FAILED'
+const func = () => {}
+
+export function getGroupIdByName(groupName, navigateOnSuccess = func, handleError = func) {
+ return async (dispatch, getState) => {
+ try {
+ const {token} = getState().auth
+ const {results: rooms} = await Api.roomsByUri(token, groupName)
+ const groupRegExp = new RegExp(`\\b${groupName}\/`)
+
+ if (!rooms) {
+ throw new Error('Probably such group is not exist.')
+ }
+
+ // Get group id
+ const matchedGroup = _.find(rooms, ({uri}) => groupRegExp.test(uri))
+
+ if (!matchedGroup) {
+ throw new Error(`Group '${groupName}' not found.`)
+ }
+
+ const payload = {
+ id: matchedGroup.groupId,
+ name: groupName
+ }
+
+ dispatch({type: GROUP_FOUND, payload})
+ navigateOnSuccess(matchedGroup.groupId)
+ } catch (error) {
+ handleError(error)
+ }
+ }
+}
+
+export function getGroupRooms(groupId) {
+ return async (dispatch, getState) => {
+ dispatch({type: GROUP_ROOMS})
+
+ try {
+ const {token} = getState().auth
+
+ const results = await Api.groupRooms(token, groupId)
+ const payload = {
+ groupId,
+ rooms: results
+ }
+
+ dispatch({type: GROUP_ROOMS_RECEIVED, payload})
+ } catch (error) {
+ dispatch({type: GROUP_ROOMS_FAILED, error})
+ }
+ }
+}
+
+/**
+ * Reducer
+ */
+
+const initialState = {
+ isLoading: false,
+ ids: [],
+ groups: {},
+ error: false,
+ errors: []
+}
+
+export default function groups(state = initialState, action) {
+ switch (action.type) {
+ case GROUP_ROOMS: {
+ return {...state,
+ isLoading: true
+ }
+ }
+
+ case GROUP_FOUND: {
+ const groupId = action.payload.id
+ const group = state.groups[groupId]
+
+ return {...state,
+ isLoading: false,
+ ids: state.ids.concat(groupId),
+ groups: {
+ ...state.groups,
+ [groupId]: {
+ ...group,
+ ...action.payload
+ }
+ }
+ }
+ }
+
+ case GROUP_ROOMS_RECEIVED: {
+ const currentGroupId = action.payload.groupId
+ const group = state.groups[currentGroupId]
+
+ return {...state,
+ isLoading: false,
+ ids: state.ids.concat(currentGroupId),
+ groups: {
+ ...state.groups,
+ [currentGroupId]: {
+ ...group,
+ rooms: action.payload.rooms
+ }
+ }
+ }
+ }
+
+ case GROUP_ROOMS_FAILED: {
+ return {...state,
+ isLoading: false,
+ error: true,
+ errors: action.error
+ }
+ }
+
+ case LOGOUT: {
+ return initialState
+ }
+
+ default :
+ return state
+ }
+}
diff --git a/app/modules/index.js b/app/modules/index.js
index a5518fa..a7ff1b2 100644
--- a/app/modules/index.js
+++ b/app/modules/index.js
@@ -14,6 +14,7 @@ import settings from './settings'
import realtime from './realtime'
import activity from './activity'
import navigation from './navigation'
+import groups from './groups'
const rootReducer = combineReducers({
ui,
@@ -29,6 +30,7 @@ const rootReducer = combineReducers({
settings,
realtime,
activity,
+ groups,
navigation
})
diff --git a/app/modules/messages.js b/app/modules/messages.js
index 0be57f0..ee045a3 100644
--- a/app/modules/messages.js
+++ b/app/modules/messages.js
@@ -150,12 +150,12 @@ export function getRoomMessagesIfNeeded(roomId) {
export function receiveRoomMessagesSnapshot(roomId, snapshot) {
return (dispatch, getState) => {
const listView = getState().messages.listView[roomId]
- if (!listView.data.length | !snapshot.length) {
+ if (!listView || !listView.data.length || !snapshot.length) {
return
}
const index = _.findIndex(snapshot, listView.data[0])
- if (index === -1 | index === 29) {
+ if (index === -1 || index === 29) {
return
}
@@ -297,8 +297,8 @@ export function deleteFailedMessage(rowId, roomId) {
export function getSingleMessage(roomId, messageId) {
return async (dispatch, getState) => {
+ dispatch({type: SINGLE_MESSAGE})
const {token} = getState().auth
- dispatch({type: SINGLE_MESSAGE, roomId, messageId})
try {
const payload = await Api.getMessage(token, roomId, messageId)
@@ -358,6 +358,7 @@ const initialState = {
// [id]: bool
},
isLoadingMessage: false,
+ singleMessageError: false,
messages: {}
}
@@ -752,12 +753,12 @@ export default function messages(state = initialState, action) {
case SINGLE_MESSAGE_OK:
return {...state,
isLoadingMessage: false,
- messages: {...state.messages,
+ entities: {...state.entities,
[action.messageId]: action.payload
- }
+ },
+ singleMessageError: false
}
- case SINGLE_MESSAGE_ERROR:
case ROOM_MESSAGES_BEFORE_FAILED:
case ROOM_MESSAGES_FAILED:
return {...state,
@@ -768,6 +769,11 @@ export default function messages(state = initialState, action) {
errors: action.error
}
+ case SINGLE_MESSAGE_ERROR:
+ return {...state,
+ singleMessageError: true
+ }
+
default:
return state
}
diff --git a/app/modules/rooms.js b/app/modules/rooms.js
index 354ce20..37eb734 100644
--- a/app/modules/rooms.js
+++ b/app/modules/rooms.js
@@ -46,6 +46,7 @@ export const GET_NOTIFICATION_SETTINGS_OK = 'rooms/GET_NOTIFICATION_SETTINGS_OK'
export const GET_NOTIFICATION_SETTINGS_ERROR = 'rooms/GET_NOTIFICATION_SETTINGS_ERROR'
export const CHANGE_NOTIFICATION_SETTINGS_OK = 'rooms/CHANGE_NOTIFICATION_SETTINGS_OK'
export const CHANGE_NOTIFICATION_SETTINGS_ERROR = 'rooms/CHANGE_NOTIFICATION_SETTINGS_ERROR'
+const func = () => {}
/**
* Action Creators
@@ -93,6 +94,30 @@ export function getRoom(id) {
}
}
+/**
+ * Return room by uri
+ */
+
+export function getRoomByUrl(url, navigateOnSuccess = func, handleError = func) {
+ return async (dispatch, getState) => {
+ const {token} = getState().auth
+ dispatch({type: ROOM})
+ try {
+ const {results: [room]} = await Api.roomsByUri(token, url)
+ if (!room) {
+ throw new Error('Room not found.')
+ }
+
+ dispatch({type: ROOM_RECEIVED, payload: room})
+
+ navigateOnSuccess(room.id)
+ } catch (error) {
+ handleError(error)
+ dispatch({type: ROOM_FAILED, error})
+ }
+ }
+}
+
/**
* Returns suggested rooms by user id
*/
@@ -172,6 +197,7 @@ export function joinUserRoom(username) {
const payload = await Api.joinRoomByUserName(token, username)
dispatch({type: JOIN_USER_ROOM_OK, payload})
+ return Promise.resolve(payload)
} catch (error) {
dispatch({type: JOIN_USER_ROOM_FAILED, error})
}
diff --git a/app/screens/Group/index.js b/app/screens/Group/index.js
new file mode 100644
index 0000000..40e54db
--- /dev/null
+++ b/app/screens/Group/index.js
@@ -0,0 +1,111 @@
+import React, {PropTypes, Component} from 'react'
+import {connect} from 'react-redux'
+import {View, ListView} from 'react-native'
+import RoomItem from '../Home/HomeRoomItem'
+import Loading from '../../components/Loading'
+import {THEMES} from '../../constants'
+import s from './styles'
+import {getGroupRooms} from '../../modules/groups'
+import {iconsMap} from '../../utils/iconsMap'
+
+const {colors} = THEMES.gitterDefault
+
+const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
+
+class Group extends Component {
+ constructor(props) {
+ super(props)
+
+ this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this))
+ this.props.navigator.setTitle({title: this.props.name})
+ this.props.navigator.setButtons({
+ leftButtons: [{
+ title: 'Menu',
+ id: 'sideMenu',
+ icon: iconsMap['menu-white'],
+ iconColor: 'white',
+ showAsAction: 'always'
+ }]
+ })
+
+ this.renderListItem = this.renderListItem.bind(this)
+ this.onRoomPress = this.onRoomPress.bind(this)
+ }
+
+ componentWillMount() {
+ const {groupId, dispatch} = this.props
+
+ dispatch(getGroupRooms(groupId))
+ }
+
+ onNavigatorEvent(event) {
+ if (event.type === 'NavBarButtonPress') {
+ if (event.id === 'sideMenu') {
+ this.props.navigator.toggleDrawer({side: 'left', animated: true})
+ }
+ }
+ }
+
+ onRoomPress(id) {
+ this.props.navigator.push({screen: 'gm.Room', passProps: {roomId: id}})
+ }
+
+ renderListItem(item) {
+ return
+ }
+
+ render() {
+ const {isLoading} = this.props
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+ )
+ }
+}
+
+Group.navigatorStyle = {
+ navBarBackgroundColor: colors.raspberry,
+ navBarButtonColor: 'white',
+ navBarTextColor: 'white',
+ topBarElevationShadowEnabled: true,
+ statusBarColor: colors.darkRed,
+ statusBarTextColorScheme: 'dark'
+}
+
+Group.propTypes = {
+ groupId: PropTypes.string,
+ rooms: PropTypes.array,
+ navigator: PropTypes.object,
+ isLoading: PropTypes.bool,
+ dispatch: PropTypes.func,
+ name: PropTypes.string
+}
+
+const mapStateToProps = ({groups: {groups, ...info}}, {groupId}) => {
+ const {rooms, name} = groups[groupId]
+
+ return ({
+ rooms: rooms || [],
+ name,
+ ...info
+ })
+}
+
+export default connect(mapStateToProps)(Group)
diff --git a/app/screens/Group/styles.js b/app/screens/Group/styles.js
new file mode 100644
index 0000000..8f1df1c
--- /dev/null
+++ b/app/screens/Group/styles.js
@@ -0,0 +1,13 @@
+import {StyleSheet} from 'react-native'
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: 'white',
+ flex: 1
+ },
+ loadingWrap: {
+ height: 200
+ }
+})
+
+export default styles
diff --git a/app/screens/Message/index.js b/app/screens/Message/index.js
index 6794ff9..69d8f47 100644
--- a/app/screens/Message/index.js
+++ b/app/screens/Message/index.js
@@ -1,22 +1,28 @@
import PropTypes from 'prop-types'
-import React, { Component } from 'react'
-import {View, ScrollView, Platform} from 'react-native';
+import React, {Component} from 'react'
+import {View, ScrollView, Platform} from 'react-native'
import {connect} from 'react-redux'
import s from './styles'
import navigationStyles from '../../styles/common/navigationStyles'
-
+import Loading from '../../components/Loading'
+import {THEMES} from '../../constants'
+import {getSingleMessage} from '../../modules/messages'
import {subscribeToReadBy, unsubscribeFromReadBy} from '../../modules/realtime'
+import FailedToLoad from '../../components/FailedToLoad'
import ReadBy from './ReadBy'
import Msg from './Message'
+const {colors} = THEMES.gitterDefault
+
class Message extends Component {
constructor(props) {
super(props)
this.renderMessage = this.renderMessage.bind(this)
- // this.renderReadBy = this.renderReadBy.bind(this)
+ this.renderReadBy = this.renderReadBy.bind(this)
this.handleAvatarPress = this.handleAvatarPress.bind(this)
+ this.retryToLoadMessage = this.componentWillMount.bind(this)
this.props.navigator.setTitle({title: 'Message'})
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this))
@@ -34,8 +40,14 @@ class Message extends Component {
}
componentWillMount() {
- const {dispatch, roomId, messageId} = this.props
- dispatch(subscribeToReadBy(roomId, messageId))
+ const {dispatch, route, roomId, messageId} = this.props
+ if (route) {
+ dispatch(getSingleMessage(roomId, messageId))
+ }
+
+ if (!this.props.isLoadingMessage) {
+ dispatch(subscribeToReadBy(roomId, messageId))
+ }
}
componentWillUnmount() {
@@ -64,6 +76,10 @@ class Message extends Component {
? roomMessagesResult.find(item => item.id === messageId)
: messages[messageId]
+ if (!message) {
+ return null
+ }
+
return (
+
+
+ )
+ }
+
+ if (this.props.error) {
+ return (
+
+ )
+ }
+
return (
@@ -98,8 +130,17 @@ class Message extends Component {
}
Message.propTypes = {
+ fromSearch: PropTypes.bool,
+ roomId: PropTypes.string,
+ messageId: PropTypes.string,
+ navigator: PropTypes.object,
dispatch: PropTypes.func,
- route: PropTypes.object,
+ route: PropTypes.shape({
+ roomId: PropTypes.string,
+ messageId: PropTypes.string
+ }),
+ error: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ isLoadingMessage: PropTypes.bool,
messages: PropTypes.object,
readBy: PropTypes.object,
viewer: PropTypes.object,
@@ -111,12 +152,16 @@ Message.navigatorStyle = {
screenBackgroundColor: 'white'
}
-function mapStateToProps(state) {
+function mapStateToProps(state, props) {
return {
+ isLoadingMessage: state.messages.isLoadingMessage,
messages: state.messages.entities,
readBy: state.readBy.byMessage,
viewer: state.viewer.user,
- roomMessagesResult: state.search.roomMessagesResult
+ roomMessagesResult: state.search.roomMessagesResult,
+ roomId: props.roomId || props.route.roomId,
+ messageId: props.messageId || props.route.messageId,
+ error: state.messages.singleMessageError ? state.messages.errors : null
}
}
diff --git a/app/screens/Room/Message/index.js b/app/screens/Room/Message/index.js
index 7b0495c..16897ca 100644
--- a/app/screens/Room/Message/index.js
+++ b/app/screens/Room/Message/index.js
@@ -12,6 +12,8 @@ import Avatar from '../../../components/Avatar'
import StatusMessage from '../StatusMessage'
import Button from '../../../components/Button'
+const handleUrlPress = (url) => Linking.openURL(url)
+
class Message extends Component {
constructor(props) {
super(props)
@@ -22,16 +24,12 @@ class Message extends Component {
}
shouldComponentUpdate(nextProps) {
- if (!_.isEqual(this.props, nextProps)) {
- return true
- } else {
- return false
- }
+ return !_.isEqual(this.props, nextProps)
}
onMessagePress() {
const {id, onPress, text, rowId} = this.props
- const failed = !!this.props.failed && this.props.failed === true
+ const failed = this.props.failed === true
onPress(id, rowId, text, failed)
}
@@ -40,10 +38,6 @@ class Message extends Component {
onLongPress(id)
}
- handleUrlPress(url) {
- Linking.openURL(url)
- }
-
renderDate() {
const {sent} = this.props
@@ -76,9 +70,9 @@ class Message extends Component {
}
return (
+ username={username} />
)
}
@@ -106,7 +100,7 @@ class Message extends Component {
text={text}
onLongPress={this.onLongPress.bind(this)}
onPress={this.onMessagePress.bind(this)}
- handleUrlPress={this.handleUrlPress.bind(this)}
+ handleUrlPress={handleUrlPress}
backgroundColor={backgroundColor}
opacity={readStatusOpacity} />
)
@@ -188,6 +182,7 @@ Message.defaultProps = {
}
Message.propTypes = {
+ navigator: PropTypes.object,
id: PropTypes.string,
rowId: PropTypes.number,
text: PropTypes.string,
diff --git a/app/screens/Room/MessagesList/index.js b/app/screens/Room/MessagesList/index.js
index b52824f..71ad2c0 100644
--- a/app/screens/Room/MessagesList/index.js
+++ b/app/screens/Room/MessagesList/index.js
@@ -86,6 +86,7 @@ export default class MessagesList extends Component {
this.handleOnLayout(e, rowId)}
onPress={onPress}
+ navigator={this.props.navigator}
rowId={rowId}
isCollapsed={isCollapsed}
onLongPress={onLongPress}
@@ -153,6 +154,7 @@ export default class MessagesList extends Component {
MessagesList.propTypes = {
onPress: PropTypes.func,
+ navigator: PropTypes.object,
listViewData: PropTypes.object,
dispatch: PropTypes.func,
onEndReached: PropTypes.func,
diff --git a/app/screens/Room/index.js b/app/screens/Room/index.js
index 3c77044..7e820d8 100644
--- a/app/screens/Room/index.js
+++ b/app/screens/Room/index.js
@@ -1,8 +1,9 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
-import {Keyboard, ActionSheetIOS, DrawerLayoutAndroid, ToastAndroid, Clipboard, Alert, ListView, View, Platform, KeyboardAvoidingView} from 'react-native';
+import {Linking, Keyboard, ActionSheetIOS, DrawerLayoutAndroid, ToastAndroid, Clipboard, Alert, ListView, View, Platform, KeyboardAvoidingView} from 'react-native';
import {connect} from 'react-redux'
import Share from 'react-native-share'
+import Toast, {DURATION} from 'react-native-easy-toast'
import navigationStyles from '../../styles/common/navigationStyles'
import _ from 'lodash'
import {roomUsers} from '../../modules/users'
@@ -11,6 +12,8 @@ import BottomSheet from '../../../libs/react-native-android-bottom-sheet/index'
import s from './styles'
import {quoteLink} from '../../utils/links'
import {THEMES} from '../../constants'
+import {parseGitterGroupUrl, parseGitterMessageUrl, parseGitterRoomUrl} from '../../utils/parseUrl'
+import {GITTER_REGEXPS} from '../../constants'
import {
getRoom,
selectRoom,
@@ -19,7 +22,8 @@ import {
leaveRoom,
markAllAsRead,
getNotificationSettings,
- changeNotificationSettings
+ changeNotificationSettings,
+ getRoomByUrl
} from '../../modules/rooms'
import {
getRoomMessages,
@@ -34,6 +38,7 @@ import {
readMessages,
sendStatusMessage
} from '../../modules/messages'
+import { getGroupIdByName } from '../../modules/groups'
import {changeRoomInfoDrawerState} from '../../modules/ui'
import RoomInfoScreen from '../RoomInfo'
import Loading from '../../components/Loading'
@@ -43,6 +48,7 @@ import JoinRoomField from './JoinRoomField'
import FailedToLoad from '../../components/FailedToLoad'
import {iconsMap} from '../../utils/iconsMap'
+const {baseUrl, groupParamsExp, messageParamsExp, roomParamsExp} = GITTER_REGEXPS
const COMMAND_REGEX = /\/\S+/
const iOS = Platform.OS === 'ios'
const {colors} = THEMES.gitterDefault
@@ -79,6 +85,11 @@ class Room extends Component {
this.handleSharingMessage = this.handleSharingMessage.bind(this)
this.handleShowModal = this.handleShowModal.bind(this)
this.toggleDrawerState = this.toggleDrawerState.bind(this)
+ this.handleUrlClick = this.handleUrlClick.bind(this)
+ this.handleGitterRoomUrlClick = this.handleGitterRoomUrlClick.bind(this)
+ this.handleGitterGroupUrlClick = this.handleGitterGroupUrlClick.bind(this)
+ this.handleGitterMessageUrlClick = this.handleGitterMessageUrlClick.bind(this)
+ this.showToast = _.curry(this.showToast.bind(this))
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this))
@@ -145,9 +156,19 @@ class Room extends Component {
// dispatch(unsubscribeToChatMessages(roomId))
}
- onNavigatorEvent(event) {
- if (event.type === 'NavBarButtonPress') {
- this.handleToolbarActionSelected(event)
+ onNavigatorEvent({type, id}) {
+ if (type === 'NavBarButtonPress') {
+ this.handleToolbarActionSelected(id)
+ }
+
+ switch (id) {
+ case 'willAppear': {
+ return Linking.addEventListener('url', this.handleUrlClick)
+ }
+ case 'willDisappear': {
+ return Linking.removeEventListener('url', this.handleUrlClick);
+ }
+ default: break
}
}
@@ -378,6 +399,49 @@ class Room extends Component {
return onlyFiltered ? actions.filter(item => item.showInBottomSheet !== true) : actions
}
+ handleUrlClick({url}) {
+ const messageUrlPattern = new RegExp(`${baseUrl.source}${messageParamsExp.source}`)
+ const groupUrlPattern = new RegExp(`${baseUrl.source}${groupParamsExp.source}`)
+ const roomUrlPattern = new RegExp(`${baseUrl.source}${roomParamsExp.source}`)
+
+ if (messageUrlPattern.test(url)) {
+ return this.handleGitterMessageUrlClick(url)
+ }
+
+ if (groupUrlPattern.test(url)) {
+ return this.handleGitterGroupUrlClick(url)
+ }
+
+ if (roomUrlPattern.test(url)) {
+ return this.handleGitterRoomUrlClick(url)
+ }
+ }
+
+ handleGitterRoomUrlClick(url) {
+ const {dispatch, navigator} = this.props
+ const {uri} = parseGitterRoomUrl(url)
+ const navigateOnSuccess = roomId => navigator.push({screen: 'gm.Room', passProps: {roomId}})
+
+ dispatch(getRoomByUrl(uri, navigateOnSuccess, this.showToast('Unable to access room data.')))
+ }
+
+ handleGitterMessageUrlClick(url) {
+ const {dispatch, navigator} = this.props
+ const {roomName, atParam: messageId} = parseGitterMessageUrl(url)
+ const navigateOnSuccess = roomId =>
+ navigator.push({screen: 'gm.Message', passProps: {route: {roomId, messageId}}})
+
+ dispatch(getRoomByUrl(roomName, navigateOnSuccess, this.showToast('Unable to access room data.')))
+ }
+
+ handleGitterGroupUrlClick(url) {
+ const {dispatch, navigator} = this.props
+ const {groupName} = parseGitterGroupUrl(url)
+ const navigateOnSuccess = groupId => navigator.push({screen: 'gm.Group', passProps: {groupId}})
+
+ dispatch(getGroupIdByName(groupName, navigateOnSuccess, this.showToast('Unable to access group data.')))
+ }
+
handleOverflowClick() {
const {title: titleProp, room} = this.props
const actions = this.getButtons(room, false)
@@ -491,7 +555,7 @@ class Room extends Component {
this.roomInfoDrawer.closeDrawer()
}
- handleToolbarActionSelected({id}) {
+ handleToolbarActionSelected(id) {
const {dispatch, route: {roomId}, navigator} = this.props
switch (id) {
case 'drawerMenu': return navigator.toggleDrawer({side: 'left', animated: true})
@@ -613,6 +677,7 @@ class Room extends Component {
prepareDataSources() {
const {listViewData, route: {roomId}, dispatch} = this.props
+
if (!listViewData[roomId]) {
const ds = new ListView.DataSource({rowHasChanged: (row1, row2) => {
return row1 !== row2
@@ -621,6 +686,9 @@ class Room extends Component {
}
}
+ showToast(errorTitle, {message}) {
+ this.toast.show(`${errorTitle}${'\n'}${message}`, DURATION.LENGTH_SHORT)
+ }
renderBottom() {
const {rooms, route: {roomId}} = this.props
@@ -671,6 +739,7 @@ class Room extends Component {
- {this.renderListView()}
- {getMessagesError || isLoadingMessages || _.has(listView, 'data') &&
- listView.data.length === 0 ? null : this.renderBottom()}
+ {this.renderListView()}
+ {getMessagesError || isLoadingMessages || _.has(listView, 'data') &&
+ listView.data.length === 0 ? null : this.renderBottom()}
+ this.toast = toast}
+ style={{backgroundColor: colors.red}}
+ position="top" />
)
}
@@ -764,7 +837,7 @@ function mapStateToProps(state, ownProps) {
const {activeRoom, rooms, notifications} = state.rooms
const {roomInfoDrawerState} = state.ui
- const room = rooms[ownProps.roomId]
+ const room = rooms[ownProps.roomId || ownProps.route.roomId]
let title = !!room ? room.name : 'Room'
title = title.split('/').reverse()[0]
diff --git a/app/screens/RoomInfo/index.js b/app/screens/RoomInfo/index.js
index 453b8de..a637aa5 100644
--- a/app/screens/RoomInfo/index.js
+++ b/app/screens/RoomInfo/index.js
@@ -197,7 +197,9 @@ class RoomInfoScreen extends Component {
RoomInfoScreen.propTypes = {
dispatch: PropTypes.func,
drawer: PropTypes.element,
- route: PropTypes.object,
+ route: PropTypes.shape({
+ roomId: PropTypes.string
+ }),
roomInfo: PropTypes.object,
rooms: PropTypes.object,
roomInfoDrawerState: PropTypes.string,
diff --git a/app/screens/index.js b/app/screens/index.js
index b642202..975b6af 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -20,6 +20,7 @@ import SearchMessages from './SearchMessages'
import RoomInfo from './RoomInfo'
import RoomSettings from './RoomSettings'
import LoginByWebView from './LoginByWebView'
+import Group from './Group'
export default class Application {
constructor(store, Provider) {
@@ -48,7 +49,8 @@ export default class Application {
Settings,
SearchMessages,
RoomSettings,
- LoginByWebView
+ LoginByWebView,
+ Group
}
Object.keys(screens).map(key => {
diff --git a/app/utils/parseUrl.js b/app/utils/parseUrl.js
new file mode 100644
index 0000000..b05703f
--- /dev/null
+++ b/app/utils/parseUrl.js
@@ -0,0 +1,31 @@
+import { GITTER_REGEXPS } from '../constants'
+
+const {baseUrl, groupParamsExp, messageParamsExp, roomParamsExp} = GITTER_REGEXPS
+
+export const parseGitterRoomUrl = (url) => {
+ const uri = url.replace(baseUrl, '')
+ const [, ownerName, roomName] = roomParamsExp.exec(uri)
+
+ return {
+ uri,
+ ownerName,
+ roomName
+ }
+}
+
+export const parseGitterGroupUrl = (url) => {
+ const [, groupName] = groupParamsExp.exec(url.replace(baseUrl, ''))
+
+ return {
+ groupName
+ }
+}
+
+export const parseGitterMessageUrl = (url) => {
+ const [, roomName, atParam] = messageParamsExp.exec(url.replace(baseUrl, ''))
+
+ return {
+ atParam,
+ roomName
+ }
+}
diff --git a/ios/GitterMobile/GitterMobile.entitlements b/ios/GitterMobile/GitterMobile.entitlements
new file mode 100644
index 0000000..8047797
--- /dev/null
+++ b/ios/GitterMobile/GitterMobile.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.associated-domains
+
+ webcredentials:gitter.im
+
+
+
diff --git a/ios/gittermobile.xcodeproj/project.pbxproj b/ios/gittermobile.xcodeproj/project.pbxproj
index cd28c28..8699df1 100644
--- a/ios/gittermobile.xcodeproj/project.pbxproj
+++ b/ios/gittermobile.xcodeproj/project.pbxproj
@@ -256,20 +256,6 @@
remoteGlobalIDString = D8AFADBD1BEE6F3F00A4592D;
remoteInfo = ReactNativeNavigation;
};
- DE99757D1EF9A87700C50752 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 3D383D3C1EBD27B6005632C8;
- remoteInfo = "third-party-tvOS";
- };
- DE99757F1EF9A87700C50752 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 3D383D621EBD27B9005632C8;
- remoteInfo = "double-conversion-tvOS";
- };
DEFF81AF1EF983C100E38121 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
@@ -277,24 +263,9 @@
remoteGlobalIDString = 83CBBA2D1A601D0E00E9B192;
remoteInfo = React;
};
- DEFF81CC1EF983C100E38121 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 139D7ECE1E25DB7D00323FB7;
- remoteInfo = "third-party";
- };
- DEFF81CE1EF983C100E38121 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 139D7E881E25C6D100323FB7;
- remoteInfo = "double-conversion";
- };
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; };
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; };
00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; };
00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; };
@@ -320,6 +291,7 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; };
8BA6F015997B8EC10E29E478 /* Pods-GitterMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GitterMobile.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GitterMobile/Pods-GitterMobile.debug.xcconfig"; sourceTree = ""; };
+ AE4355901F038FB3006AAC02 /* GitterMobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = GitterMobile.entitlements; path = GitterMobile/GitterMobile.entitlements; sourceTree = ""; };
BF06B00B1EA29D9A0090FB51 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = ""; };
DE151CB61EA81238002A2063 /* ReactNativeNavigation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactNativeNavigation.xcodeproj; path = "../node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj"; sourceTree = ""; };
DE7E019E1EA2A52F0081B152 /* FayeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FayeManager.h; path = GitterMobile/FayeManager.h; sourceTree = ""; };
@@ -350,6 +322,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */,
DEAB4DB01EA816D200A44FF8 /* libRNVectorIcons.a in Frameworks */,
DE151CD61EA8124E002A2063 /* libReactNativeNavigation.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
@@ -357,7 +330,6 @@
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */,
- 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */,
00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */,
139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */,
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
@@ -472,7 +444,7 @@
13B07FAE1A68108700A75B9A /* GitterMobile */ = {
isa = PBXGroup;
children = (
- 008F07F21AC5B25A0029DE68 /* main.jsbundle */,
+ AE4355901F038FB3006AAC02 /* GitterMobile.entitlements */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
@@ -494,10 +466,6 @@
3DAD3EAB1DF850E9000B6D8A /* libcxxreact.a */,
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */,
3DAD3EAF1DF850E9000B6D8A /* libjschelpers.a */,
- DEFF81CD1EF983C100E38121 /* libthird-party.a */,
- DE99757E1EF9A87700C50752 /* libthird-party.a */,
- DEFF81CF1EF983C100E38121 /* libdouble-conversion.a */,
- DE9975801EF9A87700C50752 /* libdouble-conversion.a */,
);
name = Products;
sourceTree = "";
@@ -728,6 +696,14 @@
CreatedOnToolsVersion = 6.2;
TestTargetID = 13B07F861A680F5B00A75B9A;
};
+ 13B07F861A680F5B00A75B9A = {
+ ProvisioningStyle = Manual;
+ SystemCapabilities = {
+ com.apple.SafariKeychain = {
+ enabled = 1;
+ };
+ };
+ };
2D02E47A1E0B4A5D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
ProvisioningStyle = Automatic;
@@ -1004,34 +980,6 @@
remoteRef = DE151CD31EA81238002A2063 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
- DE99757E1EF9A87700C50752 /* libthird-party.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = "libthird-party.a";
- remoteRef = DE99757D1EF9A87700C50752 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- DE9975801EF9A87700C50752 /* libdouble-conversion.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = "libdouble-conversion.a";
- remoteRef = DE99757F1EF9A87700C50752 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- DEFF81CD1EF983C100E38121 /* libthird-party.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = "libthird-party.a";
- remoteRef = DEFF81CC1EF983C100E38121 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- DEFF81CF1EF983C100E38121 /* libdouble-conversion.a */ = {
- isa = PBXReferenceProxy;
- fileType = archive.ar;
- path = "libdouble-conversion.a";
- remoteRef = DEFF81CE1EF983C100E38121 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
@@ -1267,14 +1215,17 @@
baseConfigurationReference = 8BA6F015997B8EC10E29E478 /* Pods-GitterMobile.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = GitterMobile/GitterMobile.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO;
+ DEVELOPMENT_TEAM = "";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"\"${PODS_ROOT}/Headers/Public\"",
"\"${PODS_ROOT}/Headers/Public/MZFayeClient\"",
"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/**",
);
INFOPLIST_FILE = GitterMobile/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1290,6 +1241,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = GitterMobile;
+ PROVISIONING_PROFILE_SPECIFIER = "";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -1299,13 +1251,16 @@
baseConfigurationReference = EAA5A150C150D13C821F7649 /* Pods-GitterMobile.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = GitterMobile/GitterMobile.entitlements;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"\"${PODS_ROOT}/Headers/Public\"",
"\"${PODS_ROOT}/Headers/Public/MZFayeClient\"",
"\"${PODS_ROOT}/Headers/Public/SocketRocket\"",
"$(SRCROOT)/../node_modules/react-native-navigation/ios/**",
+ "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/**",
);
INFOPLIST_FILE = GitterMobile/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1321,6 +1276,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = GitterMobile;
+ PROVISIONING_PROFILE_SPECIFIER = "";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -1438,6 +1394,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -1483,6 +1440,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
diff --git a/ios/gittermobile/AppDelegate.m b/ios/gittermobile/AppDelegate.m
index d469877..81e298a 100644
--- a/ios/gittermobile/AppDelegate.m
+++ b/ios/gittermobile/AppDelegate.m
@@ -1,5 +1,6 @@
#import "AppDelegate.h"
#import
+#import "RCTLinkingManager.h"
// **********************************************
// *** DON'T MISS: THE NEXT LINE IS IMPORTANT ***
@@ -13,6 +14,22 @@
@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
+ restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
+{
+ return [RCTLinkingManager application:application
+ continueUserActivity:userActivity
+ restorationHandler:restorationHandler];
+}
+
+- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
+ sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
+{
+ return [RCTLinkingManager application:application openURL:url
+ sourceApplication:sourceApplication annotation:annotation];
+}
+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
@@ -22,15 +39,15 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
-
-
+
+
// **********************************************
// *** DON'T MISS: THIS IS HOW WE BOOTSTRAP *****
// **********************************************
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation];
-
+
/*
// original RN bootstrap - remove this part
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
@@ -43,8 +60,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
*/
-
-
+
+
return YES;
}
diff --git a/ios/gittermobile/Info.plist b/ios/gittermobile/Info.plist
index 7c68fff..4dbdcdf 100644
--- a/ios/gittermobile/Info.plist
+++ b/ios/gittermobile/Info.plist
@@ -2,8 +2,6 @@
- UIViewControllerBasedStatusBarAppearance
-
CFBundleDevelopmentRegion
en
CFBundleDisplayName
@@ -22,8 +20,21 @@
1.0
CFBundleSignature
????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ gitterim
+
+
+
CFBundleVersion
1
+ LSApplicationCategoryType
+
LSRequiresIPhoneOS
NSAppTransportSecurity
@@ -62,5 +73,7 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/package.json b/package.json
index 06ddc1e..f0e64be 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"test": "jest",
"android:check": "adb devices && adb reverse tcp:8081 tcp:8081",
"android": "node node_modules/react-native/local-cli/cli.js run-android",
+ "ios": "node node_modules/react-native/local-cli/cli.js run-ios",
"lint": "node node_modules/eslint/bin/eslint -c .eslintrc ./",
"release": "cd android && ./gradlew assembleRelease",
"android-release": "node node_modules/react-native/local-cli/cli.js run-android --variant=release"
@@ -30,6 +31,7 @@
"react-native-device-info": "^0.10.2",
"react-native-dialogs": "0.0.19",
"react-native-drawer-layout": "^1.3.0",
+ "react-native-easy-toast": "^1.0.6",
"react-native-invertible-scroll-view": "^1.0.0",
"react-native-navigation": "^1.1.24",
"react-native-parsed-text": "0.0.16",