diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap index 8f1df86e2..ba93694b3 100644 --- a/__tests__/__snapshots__/RoomItem.js.snap +++ b/__tests__/__snapshots__/RoomItem.js.snap @@ -35,15 +35,16 @@ exports[`render channel 1`] = ` Array [ Object { "alignItems": "center", - "borderRadius": 4, - "height": 40, "justifyContent": "center", "overflow": "hidden", - "width": 40, }, Object { "backgroundColor": "#00BCD4", + "borderRadius": 4, + "height": 40, + "width": 40, }, + undefined, ] } > @@ -57,10 +58,14 @@ exports[`render channel 1`] = ` "color": undefined, "fontSize": 12, }, - Object { - "color": "#fff", - "fontSize": 20, - }, + Array [ + Object { + "color": "#ffffff", + }, + Object { + "fontSize": 20, + }, + ], Object { "fontFamily": "Material Design Icons", "fontStyle": "normal", @@ -151,6 +156,53 @@ exports[`render no icon 1`] = ` testID={undefined} tvParallaxProperties={undefined} > + + + + + + + + + + - ); + if (type === 'd') { + const uri = avatar || `${ baseUrl }/avatar/${ text }`; + const image = (avatar || baseUrl) && ( + + ); + return ( + + {initials} + {image} + ); + } + + const icon = { + c: 'pound', + p: 'lock', + l: 'account' + }[type]; return ( - {initials} - {image} - ); + + + ); } } @@ -64,6 +78,7 @@ Avatar.propTypes = { text: PropTypes.string.isRequired, avatar: PropTypes.string, size: PropTypes.number, - borderRadius: PropTypes.number + borderRadius: PropTypes.number, + type: PropTypes.string }; export default Avatar; diff --git a/app/containers/Header.js b/app/containers/Header.js index 4ffa7d9e9..b285d4121 100644 --- a/app/containers/Header.js +++ b/app/containers/Header.js @@ -1,18 +1,8 @@ import React from 'react'; -import { Text, View, StyleSheet, Platform, TouchableOpacity, Dimensions } from 'react-native'; -import Icon from 'react-native-vector-icons/Ionicons'; +import { View, StyleSheet, Platform } from 'react-native'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import Modal from 'react-native-modal'; import { SafeAreaView } from 'react-navigation'; -import DrawerMenuButton from '../presentation/DrawerMenuButton'; -import Avatar from './Avatar'; -import RocketChat from '../lib/rocketchat'; -import { STATUS_COLORS } from '../constants/colors'; - -const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; - let platformContainerStyles; if (Platform.OS === 'ios') { platformContainerStyles = { @@ -32,7 +22,6 @@ if (Platform.OS === 'ios') { } const appBarHeight = Platform.OS === 'ios' ? 44 : 56; -const { width } = Dimensions.get('window'); const styles = StyleSheet.create({ container: { backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF', @@ -41,171 +30,20 @@ const styles = StyleSheet.create({ }, appBar: { flex: 1 - }, - header: { - flexDirection: 'row', - alignItems: 'center', - flex: 1 - }, - titleContainer: { - left: TITLE_OFFSET, - right: TITLE_OFFSET, - position: 'absolute', - alignItems: 'center', - justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start', - flexDirection: 'row' - }, - status: { - borderRadius: 4, - width: 8, - height: 8, - marginRight: 10 - }, - avatar: { - marginRight: 10 - }, - title: { - fontWeight: 'bold' - }, - left: { - left: 0, - position: 'absolute' - }, - right: { - right: 0, - position: 'absolute' - }, - modal: { - width: width - 60, - height: width - 60, - backgroundColor: '#F7F7F7', - borderRadius: 4, - flexDirection: 'column' - }, - modalButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'transparent', - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: 'rgba(0, 0, 0, .3)', - paddingHorizontal: 20 } }); -@connect(state => ({ - user: state.login.user, - Site_Url: state.settings.Site_Url -})) export default class extends React.PureComponent { static propTypes = { - navigation: PropTypes.object.isRequired, - user: PropTypes.object.isRequired, - Site_Url: PropTypes.string + subview: PropTypes.object.isRequired } - constructor(props) { - super(props); - this.state = { - isModalVisible: false - }; - } - - onPressModalButton(status) { - RocketChat.setUserPresenceDefaultStatus(status); - this.hideModal(); - } - - showModal() { - this.setState({ isModalVisible: true }); - } - - hideModal() { - this.setState({ isModalVisible: false }); - } - - createChannel() { - this.props.navigation.navigate('SelectUsers'); - } - - renderTitle() { - if (!this.props.user.username) { - return null; - } - return ( - this.showModal()}> - - - {this.props.user.username} - - ); - } - - renderRight() { - if (Platform.OS !== 'ios') { - return; - } - return ( - - this.createChannel()} - /> - - ); - } - - renderModalButton = (status, text) => { - const statusStyle = [styles.status, { backgroundColor: STATUS_COLORS[status] }]; - const textStyle = { flex: 1, fontWeight: this.props.user.status === status ? 'bold' : 'normal' }; - return ( - this.onPressModalButton(status)} - > - - - {text || status.charAt(0).toUpperCase() + status.slice(1)} - - - ); - }; - render() { return ( - - - - - {this.renderTitle()} - {this.renderRight()} - + {this.props.subview} - this.hideModal()} - onBackdropPress={() => this.hideModal()} - > - - {this.renderModalButton('online')} - {this.renderModalButton('busy')} - {this.renderModalButton('away')} - {this.renderModalButton('offline', 'Invisible')} - - ); } diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js index 95cbd0a9d..a5ab0837d 100644 --- a/app/containers/routes/AuthRoutes.js +++ b/app/containers/routes/AuthRoutes.js @@ -1,38 +1,19 @@ -import React from 'react'; import { Platform } from 'react-native'; import { StackNavigator, DrawerNavigator } from 'react-navigation'; import Sidebar from '../../containers/Sidebar'; -import DrawerMenuButton from '../../presentation/DrawerMenuButton'; -import Header from '../../containers/Header'; - import RoomsListView from '../../views/RoomsListView'; import RoomView from '../../views/RoomView'; import CreateChannelView from '../../views/CreateChannelView'; import SelectUsersView from '../../views/SelectUsersView'; -const drawerPosition = 'left'; -const drawerIconPosition = 'headerLeft'; - const AuthRoutes = StackNavigator( { RoomsList: { - screen: RoomsListView, - navigationOptions({ navigation }) { - return { - title: 'Rooms', - header:
- }; - } + screen: RoomsListView }, Room: { - screen: RoomView, - navigationOptions({ navigation }) { - return { - title: navigation.state.params.name || navigation.state.params.room.name || 'Room' - // [drawerIconPosition]: ()รท - }; - } + screen: RoomView }, CreateChannel: { screen: CreateChannelView, @@ -53,18 +34,11 @@ const AuthRoutes = StackNavigator( const Routes = DrawerNavigator( { Home: { - screen: AuthRoutes, - navigationOptions({ navigation }) { - return { - title: 'Rooms', - [drawerIconPosition]: - }; - } + screen: AuthRoutes } }, { contentComponent: Sidebar, - drawerPosition, navigationOptions: { drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked' } diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 4b2c6abee..c7144600a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -11,6 +11,7 @@ import * as actions from '../actions'; import { someoneTyping } from '../actions/room'; import { setUser } from '../actions/login'; import { disconnect, connectSuccess } from '../actions/connect'; +import { requestActiveUser } from '../actions/activeUsers'; export { Accounts } from 'react-native-meteor'; @@ -47,6 +48,23 @@ const RocketChat = { } throw new Error({ error: 'invalid server' }); }, + _setUser(ddpMessage) { + let status; + if (!ddpMessage.fields) { + status = 'offline'; + } else { + status = ddpMessage.fields.status || 'offline'; + } + + const { user } = reduxStore.getState().login; + if (user && user.id === ddpMessage.id) { + return reduxStore.dispatch(setUser({ status })); + } + + const activeUser = {}; + activeUser[ddpMessage.id] = status; + return reduxStore.dispatch(requestActiveUser(activeUser)); + }, connect(_url) { return new Promise((resolve) => { const url = `${ _url }/websocket`; @@ -63,6 +81,16 @@ const RocketChat = { }); Meteor.ddp.on('connected', async() => { + Meteor.ddp.on('added', (ddpMessage) => { + if (ddpMessage.collection === 'users') { + return RocketChat._setUser(ddpMessage); + } + }); + Meteor.ddp.on('removed', (ddpMessage) => { + if (ddpMessage.collection === 'users') { + return RocketChat._setUser(ddpMessage); + } + }); Meteor.ddp.on('changed', (ddpMessage) => { if (ddpMessage.collection === 'stream-room-messages') { return realm.write(() => { @@ -96,7 +124,7 @@ const RocketChat = { } } if (ddpMessage.collection === 'users') { - return reduxStore.dispatch(setUser({ status: ddpMessage.fields.status || ddpMessage.fields.statusDefault })); + return RocketChat._setUser(ddpMessage); } }); RocketChat.getSettings(); @@ -436,7 +464,7 @@ const RocketChat = { }); Meteor.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false); Meteor.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false); - Meteor.subscribe('userData', null, false); + Meteor.subscribe('activeUsers', null, false); return data; }, logout({ server }) { diff --git a/app/presentation/DrawerMenuButton.js b/app/presentation/DrawerMenuButton.js deleted file mode 100644 index c1d743a75..000000000 --- a/app/presentation/DrawerMenuButton.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { TouchableOpacity } from 'react-native'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -const DrawerMenuButton = ({ navigation }) => ( - navigation.navigate('DrawerOpen')} - > - - -); - -DrawerMenuButton.propTypes = { - navigation: PropTypes.object.isRequired -}; - -export default DrawerMenuButton; diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index a6bd3ce0a..3cf02dd65 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -1,10 +1,8 @@ import React from 'react'; import moment from 'moment'; -import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import PropTypes from 'prop-types'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import Avatar from '../containers/Avatar'; -import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; const styles = StyleSheet.create({ container: { @@ -49,22 +47,6 @@ const styles = StyleSheet.create({ fontSize: 10, height: 10, color: '#888' - }, - iconContainer: { - height: 40, - width: 40, - borderRadius: 4, - overflow: 'hidden', - justifyContent: 'center', - alignItems: 'center' - }, - icon: { - fontSize: 20, - color: '#fff' - }, - avatarInitials: { - fontSize: 20, - color: '#ffffff' } }); @@ -83,30 +65,7 @@ export default class RoomItem extends React.PureComponent { get icon() { const { type, name, baseUrl } = this.props; - - const icon = { - d: 'at', - c: 'pound', - p: 'lock', - l: 'account' - }[type]; - - if (!icon) { - return null; - } - - if (type === 'd') { - return ( - - ); - } - const { color } = avatarInitialsAndColor(name); - - return ( - - - - ); + return ; } formatDate = date => moment(date).calendar(null, { diff --git a/app/reducers/activeUsers.js b/app/reducers/activeUsers.js new file mode 100644 index 000000000..71aa7c4a4 --- /dev/null +++ b/app/reducers/activeUsers.js @@ -0,0 +1,15 @@ +import * as types from '../actions/actionsTypes'; + +const initialState = {}; + +export default (state = initialState, action) => { + switch (action.type) { + case types.ACTIVE_USERS.SET: + return { + ...state, + ...action.data + }; + default: + return state; + } +}; diff --git a/app/reducers/index.js b/app/reducers/index.js index 860dbdc35..88ca8ae6f 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -4,12 +4,14 @@ import login from './login'; import meteor from './connect'; import messages from './messages'; import room from './room'; +import rooms from './rooms'; import server from './server'; import navigator from './navigator'; import createChannel from './createChannel'; import app from './app'; import permissions from './permissions'; +import activeUsers from './activeUsers'; export default combineReducers({ - settings, login, meteor, messages, server, navigator, createChannel, app, room, permissions + settings, login, meteor, messages, server, navigator, createChannel, app, room, rooms, permissions, activeUsers }); diff --git a/app/reducers/rooms.js b/app/reducers/rooms.js index bd7ac7e98..0565dc80c 100644 --- a/app/reducers/rooms.js +++ b/app/reducers/rooms.js @@ -2,7 +2,8 @@ import * as types from '../actions/actionsTypes'; const initialState = { isFetching: false, - failure: false + failure: false, + searchText: '' }; export default function login(state = initialState, action) { @@ -24,8 +25,11 @@ export default function login(state = initialState, action) { failure: true, errorMessage: action.err }; - // case types.LOGOUT: - // return initialState; + case types.ROOMS.SET_SEARCH: + return { + ...state, + searchText: action.searchText + }; default: return state; } diff --git a/app/sagas/activeUsers.js b/app/sagas/activeUsers.js new file mode 100644 index 000000000..de5c02582 --- /dev/null +++ b/app/sagas/activeUsers.js @@ -0,0 +1,30 @@ +import { put, take, race, fork } from 'redux-saga/effects'; +import { delay } from 'redux-saga'; +import * as types from '../actions/actionsTypes'; + +import { setActiveUser } from '../actions/activeUsers'; + +const watchActiveUsers = function* handleInput() { + let obj = {}; + while (true) { + const { status, timeout } = yield race({ + status: take(types.ACTIVE_USERS.REQUEST), + timeout: delay(3000) + }); + if (timeout && Object.keys(obj).length > 0) { + yield put(setActiveUser(obj)); + obj = {}; + } + if (status) { + obj = { + ...obj, + ...status.user + }; + } + } +}; + +const root = function* root() { + yield fork(watchActiveUsers); +}; +export default root; diff --git a/app/sagas/index.js b/app/sagas/index.js index 068401c54..a27f59a54 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -8,6 +8,7 @@ import selectServer from './selectServer'; import createChannel from './createChannel'; import init from './init'; import state from './state'; +import activeUsers from './activeUsers'; const root = function* root() { yield all([ @@ -19,7 +20,8 @@ const root = function* root() { connect(), messages(), selectServer(), - state() + state(), + activeUsers() ]); }; diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js new file mode 100644 index 000000000..85485ac61 --- /dev/null +++ b/app/views/RoomView/Header/index.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { Text, View, Platform, TouchableOpacity } from 'react-native'; +import Icon from 'react-native-vector-icons/Ionicons'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { HeaderBackButton } from 'react-navigation'; + +import realm from '../../../lib/realm'; +import Avatar from '../../../containers/Avatar'; +import { STATUS_COLORS } from '../../../constants/colors'; +import styles from './styles'; + +@connect(state => ({ + user: state.login.user, + baseUrl: state.settings.Site_Url, + activeUsers: state.activeUsers +})) +export default class extends React.Component { + static propTypes = { + navigation: PropTypes.object.isRequired, + user: PropTypes.object.isRequired, + baseUrl: PropTypes.string, + activeUsers: PropTypes.object + } + + constructor(props) { + super(props); + this.state = { + room: {}, + roomName: props.navigation.state.params.name + }; + this.rid = props.navigation.state.params.room.rid; + this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid); + this.room.addListener(this.updateState); + } + + componentDidMount() { + this.updateState(); + } + componentWillUnmount() { + this.room.removeAllListeners(); + } + + getUserStatus() { + const userId = this.rid.replace(this.props.user.id, '').trim(); + return this.props.activeUsers[userId] || 'offline'; + } + + getUserStatusLabel() { + const status = this.getUserStatus(); + return status.charAt(0).toUpperCase() + status.slice(1); + } + + updateState = () => { + this.setState({ room: this.room[0] }); + }; + + isDirect = () => this.state.room && this.state.room.t === 'd'; + + renderLeft = () => this.props.navigation.goBack(null)} tintColor='#292E35' />; + + renderTitle() { + if (!this.state.roomName) { + return null; + } + return ( + + {this.isDirect() ? + + : null + } + + + {this.state.roomName} + {this.isDirect() ? + {this.getUserStatusLabel()} + : null + } + + + ); + } + + renderRight = () => ( + + {}} + > + + + + ); + + render() { + return ( + + {this.renderLeft()} + {this.renderTitle()} + {this.renderRight()} + + ); + } +} diff --git a/app/views/RoomView/Header/styles.js b/app/views/RoomView/Header/styles.js new file mode 100644 index 000000000..794dc83ea --- /dev/null +++ b/app/views/RoomView/Header/styles.js @@ -0,0 +1,49 @@ +import { StyleSheet, Platform } from 'react-native'; + +const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; +export default StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + flex: 1 + }, + titleContainer: { + left: TITLE_OFFSET, + right: TITLE_OFFSET, + position: 'absolute', + alignItems: 'center', + justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start', + flexDirection: 'row', + height: 44 + }, + status: { + borderRadius: 4, + width: 8, + height: 8, + marginRight: 10 + }, + userStatus: { + fontSize: 10, + color: '#888' + }, + title: { + fontWeight: '500', + color: '#292E35' + }, + left: { + left: 0, + position: 'absolute' + }, + right: { + right: 0, + position: 'absolute', + flexDirection: 'row' + }, + headerButton: { + backgroundColor: 'transparent', + height: 44, + width: 44, + alignItems: 'center', + justifyContent: 'center' + } +}); diff --git a/app/views/RoomView.js b/app/views/RoomView/index.js similarity index 80% rename from app/views/RoomView.js rename to app/views/RoomView/index.js index 474930577..85f19bf8a 100644 --- a/app/views/RoomView.js +++ b/app/views/RoomView/index.js @@ -1,54 +1,26 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, StyleSheet, Button, SafeAreaView } from 'react-native'; +import { Text, View, Button, SafeAreaView } from 'react-native'; import { ListView } from 'realm/react-native'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import * as actions from '../actions'; -import { openRoom } from '../actions/room'; -import { editCancel } from '../actions/messages'; -import realm from '../lib/realm'; -import RocketChat from '../lib/rocketchat'; -import Message from '../containers/message'; -import MessageActions from '../containers/MessageActions'; -import MessageBox from '../containers/MessageBox'; -import Typing from '../containers/Typing'; -import KeyboardView from '../presentation/KeyboardView'; +import * as actions from '../../actions'; +import { openRoom } from '../../actions/room'; +import { editCancel } from '../../actions/messages'; +import realm from '../../lib/realm'; +import RocketChat from '../../lib/rocketchat'; +import Message from '../../containers/message'; +import MessageActions from '../../containers/MessageActions'; +import MessageBox from '../../containers/MessageBox'; +import Typing from '../../containers/Typing'; +import KeyboardView from '../../presentation/KeyboardView'; +import Header from '../../containers/Header'; +import RoomsHeader from './Header'; +import styles from './styles'; const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); -const styles = StyleSheet.create({ - typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 }, - container: { - flex: 1, - backgroundColor: '#fff' - }, - safeAreaView: { - flex: 1 - }, - list: { - flex: 1, - transform: [{ scaleY: -1 }] - }, - separator: { - height: 1, - backgroundColor: '#CED0CE' - }, - bannerContainer: { - backgroundColor: 'orange' - }, - bannerText: { - margin: 5, - textAlign: 'center', - color: '#a00' - }, - loadingMore: { - transform: [{ scaleY: -1 }], - textAlign: 'center', - padding: 5, - color: '#ccc' - } -}); + const typing = () => ; @connect( state => ({ @@ -78,6 +50,10 @@ export default class RoomView extends React.Component { loading: PropTypes.bool }; + static navigationOptions = ({ navigation }) => ({ + header:
} /> + }); + constructor(props) { super(props); this.rid = diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js new file mode 100644 index 000000000..66ddd313d --- /dev/null +++ b/app/views/RoomView/styles.js @@ -0,0 +1,34 @@ +import { StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 }, + container: { + flex: 1, + backgroundColor: '#fff' + }, + safeAreaView: { + flex: 1 + }, + list: { + flex: 1, + transform: [{ scaleY: -1 }] + }, + separator: { + height: 1, + backgroundColor: '#CED0CE' + }, + bannerContainer: { + backgroundColor: 'orange' + }, + bannerText: { + margin: 5, + textAlign: 'center', + color: '#a00' + }, + loadingMore: { + transform: [{ scaleY: -1 }], + textAlign: 'center', + padding: 5, + color: '#ccc' + } +}); diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js new file mode 100644 index 000000000..302fce5fe --- /dev/null +++ b/app/views/RoomsListView/Header/index.js @@ -0,0 +1,210 @@ +import React from 'react'; +import { Text, View, Platform, TouchableOpacity, TextInput } from 'react-native'; +import Icon from 'react-native-vector-icons/Ionicons'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import Modal from 'react-native-modal'; +import { CachedImage } from 'react-native-img-cache'; +import { HeaderBackButton } from 'react-navigation'; + +import Avatar from '../../../containers/Avatar'; +import RocketChat from '../../../lib/rocketchat'; +import { STATUS_COLORS } from '../../../constants/colors'; +import { setSearch } from '../../../actions/rooms'; +import styles from './styles'; + +@connect(state => ({ + user: state.login.user, + baseUrl: state.settings.Site_Url +}), dispatch => ({ + setSearch: searchText => dispatch(setSearch(searchText)) +})) +export default class extends React.Component { + static propTypes = { + navigation: PropTypes.object.isRequired, + user: PropTypes.object.isRequired, + baseUrl: PropTypes.string, + setSearch: PropTypes.func + } + + constructor(props) { + super(props); + this.state = { + isModalVisible: false, + searching: false + }; + } + + onPressModalButton(status) { + RocketChat.setUserPresenceDefaultStatus(status); + this.hideModal(); + } + + onSearchChangeText(text) { + this.props.setSearch(text.trim()); + } + + onPressCancelSearchButton() { + this.setState({ searching: false }); + this.props.setSearch(''); + } + + onPressSearchButton() { + this.setState({ searching: true }); + requestAnimationFrame(() => { + this.inputSearch.focus(); + }); + } + + showModal() { + this.setState({ isModalVisible: true }); + } + + hideModal() { + this.setState({ isModalVisible: false }); + } + + createChannel() { + this.props.navigation.navigate('SelectUsers'); + } + + renderLeft() { + return ( + + this.props.navigation.navigate('DrawerOpen')} + > + + + + ); + } + + renderTitle() { + if (!this.props.user.username) { + return null; + } + return ( + this.showModal()}> + + + {this.props.user.username} + + ); + } + + renderRight() { + return ( + + {Platform.OS === 'android' ? + this.onPressSearchButton()} + > + + : null} + {Platform.OS === 'ios' ? + this.createChannel()} + > + + : null} + + ); + } + + renderModalButton = (status, text) => { + const statusStyle = [styles.status, { backgroundColor: STATUS_COLORS[status] }]; + const textStyle = { flex: 1, fontWeight: this.props.user.status === status ? 'bold' : 'normal' }; + return ( + this.onPressModalButton(status)} + > + + + {text || status.charAt(0).toUpperCase() + status.slice(1)} + + + ); + }; + + renderHeader() { + if (this.state.searching) { + return null; + } + return ( + + {this.renderLeft()} + {this.renderTitle()} + {this.renderRight()} + + ); + } + + renderSearch() { + if (!this.state.searching) { + return null; + } + return ( + + + this.onPressCancelSearchButton()} /> + + this.inputSearch = inputSearch} + underlineColorAndroid='transparent' + style={styles.inputSearch} + onChangeText={text => this.onSearchChangeText(text)} + returnKeyType='search' + placeholder='Search' + clearButtonMode='while-editing' + blurOnSubmit + /> + + ); + } + + render() { + return ( + + {this.renderHeader()} + {this.renderSearch()} + this.hideModal()} + onBackdropPress={() => this.hideModal()} + > + + {this.renderModalButton('online')} + {this.renderModalButton('busy')} + {this.renderModalButton('away')} + {this.renderModalButton('offline', 'Invisible')} + + + + ); + } +} diff --git a/app/views/RoomsListView/Header/styles.js b/app/views/RoomsListView/Header/styles.js new file mode 100644 index 000000000..33d252ce8 --- /dev/null +++ b/app/views/RoomsListView/Header/styles.js @@ -0,0 +1,75 @@ +import { StyleSheet, Platform, Dimensions } from 'react-native'; + +const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; +const { width } = Dimensions.get('window'); +export default StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + flex: 1 + }, + titleContainer: { + left: TITLE_OFFSET, + right: TITLE_OFFSET, + position: 'absolute', + alignItems: 'center', + justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start', + flexDirection: 'row', + height: 44 + }, + status: { + borderRadius: 4, + width: 8, + height: 8, + marginRight: 10 + }, + avatar: { + marginRight: 10 + }, + title: { + fontWeight: '500', + color: '#292E35' + }, + left: { + left: 0, + position: 'absolute' + }, + right: { + right: 0, + position: 'absolute', + flexDirection: 'row' + }, + modal: { + width: width - 60, + height: width - 60, + backgroundColor: '#F7F7F7', + borderRadius: 4, + flexDirection: 'column' + }, + modalButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'transparent', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: 'rgba(0, 0, 0, .3)', + paddingHorizontal: 20 + }, + headerButton: { + backgroundColor: 'transparent', + height: 44, + width: 44, + alignItems: 'center', + justifyContent: 'center' + }, + serverImage: { + width: 24, + height: 24, + borderRadius: 4 + }, + inputSearch: { + flex: 1, + marginLeft: 44 + } +}); diff --git a/app/views/RoomsListView.js b/app/views/RoomsListView/index.js similarity index 77% rename from app/views/RoomsListView.js rename to app/views/RoomsListView/index.js index 7ae5eeb8f..410524010 100644 --- a/app/views/RoomsListView.js +++ b/app/views/RoomsListView/index.js @@ -3,69 +3,26 @@ import { ListView } from 'realm/react-native'; import React from 'react'; import PropTypes from 'prop-types'; import Icon from 'react-native-vector-icons/Ionicons'; -import { Platform, View, StyleSheet, TextInput, SafeAreaView } from 'react-native'; +import { Platform, View, TextInput, SafeAreaView } from 'react-native'; import { connect } from 'react-redux'; -import * as actions from '../actions'; -import * as server from '../actions/connect'; -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: { - flex: 1, - alignItems: 'stretch', - justifyContent: 'center' - }, - separator: { - height: 1, - backgroundColor: '#E7E7E7' - }, - list: { - width: '100%', - backgroundColor: '#FFFFFF' - }, - emptyView: { - flexGrow: 1, - alignItems: 'stretch', - justifyContent: 'center' - }, - emptyText: { - textAlign: 'center', - fontSize: 18, - color: '#ccc' - }, - actionButtonIcon: { - fontSize: 20, - height: 22, - color: 'white' - }, - searchBoxView: { - backgroundColor: '#eee' - }, - searchBox: { - backgroundColor: '#fff', - margin: 5, - borderRadius: 5, - padding: 5, - paddingLeft: 10, - color: '#aaa' - }, - safeAreaView: { - flex: 1, - backgroundColor: '#fff' - } -}); +import * as actions from '../../actions'; +import * as server from '../../actions/connect'; +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'; +import Header from '../../containers/Header'; +import RoomsListHeader from './Header'; +import styles from './styles'; const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); @connect(state => ({ server: state.server.server, login: state.login, Site_Url: state.settings.Site_Url, - canShowList: state.login.token || state.login.user.token - // Message_DateFormat: state.settings.Message_DateFormat + canShowList: state.login.token || state.login.user.token, + searchText: state.rooms.searchText }), dispatch => ({ login: () => dispatch(actions.login()), connect: () => dispatch(server.connectRequest()) @@ -75,10 +32,14 @@ export default class RoomsListView extends React.Component { static propTypes = { navigation: PropTypes.object.isRequired, Site_Url: PropTypes.string, - // Message_DateFormat: PropTypes.string, - server: PropTypes.string + server: PropTypes.string, + searchText: PropTypes.string } + static navigationOptions = ({ navigation }) => ({ + header:
} /> + }); + constructor(props) { super(props); @@ -104,6 +65,8 @@ export default class RoomsListView extends React.Component { this.data.removeListener(this.updateState); this.data = realm.objects('subscriptions').filtered('_server.id = $0', props.server).sorted('roomUpdatedAt', true); this.data.addListener(this.updateState); + } else if (this.props.searchText !== props.searchText) { + this.search(props.searchText); } } @@ -111,11 +74,13 @@ export default class RoomsListView extends React.Component { this.data.removeAllListeners(); } - onSearchChangeText = (text) => { + onSearchChangeText(text) { + this.setState({ searchText: text }); + this.search(text); + } + + search(text) { const searchText = text.trim(); - this.setState({ - searchText: text - }); if (searchText === '') { return this.setState({ dataSource: ds.cloneWithRows(this.data) @@ -222,7 +187,7 @@ export default class RoomsListView extends React.Component { underlineColorAndroid='transparent' style={styles.searchBox} value={this.state.searchText} - onChangeText={this.onSearchChangeText} + onChangeText={text => this.onSearchChangeText(text)} returnKeyType='search' placeholder='Search' clearButtonMode='while-editing' @@ -251,8 +216,8 @@ export default class RoomsListView extends React.Component { dataSource={this.state.dataSource} style={styles.list} renderRow={this.renderItem} - renderHeader={this.renderSearchBar} - contentOffset={{ x: 0, y: 38 }} + renderHeader={Platform.OS === 'ios' ? this.renderSearchBar : null} + contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}} enableEmptySections keyboardShouldPersistTaps='always' /> diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js new file mode 100644 index 000000000..f3417bf10 --- /dev/null +++ b/app/views/RoomsListView/styles.js @@ -0,0 +1,47 @@ +import { StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + container: { + flex: 1, + alignItems: 'stretch', + justifyContent: 'center' + }, + separator: { + height: 1, + backgroundColor: '#E7E7E7' + }, + list: { + width: '100%', + backgroundColor: '#FFFFFF' + }, + emptyView: { + flexGrow: 1, + alignItems: 'stretch', + justifyContent: 'center' + }, + emptyText: { + textAlign: 'center', + fontSize: 18, + color: '#ccc' + }, + actionButtonIcon: { + fontSize: 20, + height: 22, + color: 'white' + }, + searchBoxView: { + backgroundColor: '#eee' + }, + searchBox: { + backgroundColor: '#fff', + margin: 5, + borderRadius: 5, + padding: 5, + paddingLeft: 10, + color: '#aaa' + }, + safeAreaView: { + flex: 1, + backgroundColor: '#fff' + } +});