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",