diff --git a/android/app/src/main/res/drawable-hdpi/plus.png b/android/app/src/main/res/drawable-hdpi/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..238a8cc9a05e6dedfb026729cceaf2044b793928 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/plus.png differ diff --git a/android/app/src/main/res/drawable-hdpi/textinput_search.png b/android/app/src/main/res/drawable-hdpi/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..274376fd04eba5c3cc100dfe32116d9706aefd68 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/textinput_search.png differ diff --git a/android/app/src/main/res/drawable-mdpi/plus.png b/android/app/src/main/res/drawable-mdpi/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..cce622415373cec9bcd8d00b0abb6808f50dc088 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/plus.png differ diff --git a/android/app/src/main/res/drawable-mdpi/textinput_search.png b/android/app/src/main/res/drawable-mdpi/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..4eefb38cdf08f3356af850ae8bdf538b1b147dab Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/textinput_search.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/plus.png b/android/app/src/main/res/drawable-xhdpi/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..251c8d181cc70c57bf8c2068c0e8fe695bca9870 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/plus.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/textinput_search.png b/android/app/src/main/res/drawable-xhdpi/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..a8d16731036c8544af8aa30e3f2f98458f8e6e8b Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/textinput_search.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/plus.png b/android/app/src/main/res/drawable-xxhdpi/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..71db08c7acda5cadd0b6c5da25b931113c969f01 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/plus.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/textinput_search.png b/android/app/src/main/res/drawable-xxhdpi/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9db2d094d48f7ad826836ebd8b5a78421251af Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/textinput_search.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/plus.png b/android/app/src/main/res/drawable-xxxhdpi/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..320adb8bf8ef5d5f177e971d8a5a6163cd6957c3 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/plus.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/textinput_search.png b/android/app/src/main/res/drawable-xxxhdpi/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..363ff5db6a52ab5a443825afc11bdfa3cd3299fb Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/textinput_search.png differ diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 49bd9cea56f296fd200d08ba608b25e6ccb477eb..5b8f16c9f5ccbb917c83fd3df829e2e3155ed029 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -84,7 +84,9 @@ export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']); export const SERVER = createRequestTypes('SERVER', [ ...defaultTypes, 'SELECT_SUCCESS', - 'SELECT_REQUEST' + 'SELECT_REQUEST', + 'INIT_ADD', + 'FINISH_ADD' ]); export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']); export const LOGOUT = 'LOGOUT'; // logout is always success diff --git a/app/actions/server.js b/app/actions/server.js index 51d1bafff15c1356e73b842bc8a6d4cf347d66af..a6bab715ca198960f6b7241d589f93a04a12608a 100644 --- a/app/actions/server.js +++ b/app/actions/server.js @@ -33,3 +33,15 @@ export function serverFailure(err) { err }; } + +export function serverInitAdd() { + return { + type: SERVER.INIT_ADD + }; +} + +export function serverFinishAdd() { + return { + type: SERVER.FINISH_ADD + }; +} diff --git a/app/constants/colors.js b/app/constants/colors.js index 47c587eb96efe9a17dd89ed0ffd349cfb1bd31a1..5a6037428241b09a5de8176431e0ae3d4553e538 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -1,8 +1,8 @@ export const AVATAR_COLORS = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; -export const ESLINT_FIX = null; export const COLOR_DANGER = '#f5455c'; export const COLOR_BUTTON_PRIMARY = '#2D6AEA'; export const COLOR_TEXT = '#292E35'; +export const COLOR_SEPARATOR = '#CBCED1'; export const STATUS_COLORS = { online: '#2de0a5', busy: COLOR_DANGER, diff --git a/app/containers/SearchBox.js b/app/containers/SearchBox.js new file mode 100644 index 0000000000000000000000000000000000000000..5135ba197ac95560c1bac113eac4f084afdf09dc --- /dev/null +++ b/app/containers/SearchBox.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { View, StyleSheet, Image, TextInput, Platform } from 'react-native'; +import PropTypes from 'prop-types'; + +import I18n from '../i18n'; + +const styles = StyleSheet.create({ + container: { + backgroundColor: Platform.OS === 'ios' ? '#F7F8FA' : '#54585E' + }, + searchBox: { + alignItems: 'center', + backgroundColor: '#E1E5E8', + borderRadius: 10, + color: '#8E8E93', + flexDirection: 'row', + fontSize: 17, + height: 36, + margin: 16, + marginVertical: 10, + paddingHorizontal: 10 + }, + icon: { + width: 14, + height: 14 + }, + input: { + color: '#8E8E93', + flex: 1, + fontSize: 17, + marginLeft: 8, + paddingTop: 0, + paddingBottom: 0 + } +}); + +const SearchBox = ({ onChangeText, testID }) => ( + <View style={styles.container}> + <View style={styles.searchBox}> + <Image source={{ uri: 'textinput_search' }} style={styles.icon} /> + <TextInput + autoCapitalize='none' + autoCorrect={false} + blurOnSubmit + clearButtonMode='while-editing' + placeholder={I18n.t('Search')} + returnKeyType='search' + style={styles.input} + testID={testID} + underlineColorAndroid='transparent' + onChangeText={onChangeText} + /> + </View> + </View> +); + +SearchBox.propTypes = { + onChangeText: PropTypes.func.isRequired, + testID: PropTypes.string +}; + +export default SearchBox; diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index 0cbf8aa3f09fac14652aaf12e2c41282ebecaeb4..382939ae6358750835ae970a1ceb8b555fec2417 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -236,6 +236,7 @@ export default class Sidebar extends Component { setTimeout(() => { NavigationActions.push({ screen: 'NewServerView', + backButtonTitle: '', passProps: { server: item.id }, diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 4426147901b6958c04eaf3c8a22cebf45222b71c..8af6fa337704b88613884f023d670e898b13dc79 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -1,6 +1,7 @@ export default { '1_online_member': '1 online member', '1_person_reacted': '1 person reacted', + '1_user': '1 user', 'error-action-not-allowed': '{{action}} is not allowed', 'error-application-not-found': 'Application not found', 'error-archived-duplicate-name': 'There\'s an archived channel with name {{room_name}}', @@ -110,6 +111,7 @@ export default { Cancel_recording: 'Cancel recording', Cancel: 'Cancel', changing_avatar: 'changing avatar', + creating_channel: 'creating channel', Channel_Name: 'Channel Name', Channels: 'Channels', Chats: 'Chats', @@ -160,6 +162,7 @@ export default { Has_left_the_channel: 'Has left the channel', I_have_an_account: 'I have an account', Invisible: 'Invisible', + Invite: 'Invite', is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance', is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance', is_typing: 'is typing', @@ -188,13 +191,15 @@ export default { muted: 'muted', My_servers: 'My servers', N_online_members: '{{n}} online members', - N_person_reacted: '{{n}} people reacted', + N_people_reacted: '{{n}} people reacted', + N_users: '{{n}} users', name: 'name', Name: 'Name', New_in_RocketChat_question_mark: 'New in Rocket.Chat?', New_Message: 'New Message', New_Password: 'New Password', New_Server: 'New Server', + Next: 'Next', No_files: 'No files', No_mentioned_messages: 'No mentioned messages', No_pinned_messages: 'No pinned messages', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index ed2b028971e45d5eef58a3f2da681b42bdbae878..711d5d99ee850c17d4a52fb2e9a58598f260510a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -528,6 +528,56 @@ const RocketChat = { return _sendMessageCall(JSON.parse(JSON.stringify(message))); }, + async search({ text, filterUsers = true, filterRooms = true }) { + const searchText = text.trim(); + if (searchText === '') { + delete this.oldPromise; + return []; + } + + let data = database.objects('subscriptions').filtered('name CONTAINS[c] $0', searchText); + + if (filterUsers && !filterRooms) { + data = data.filtered('t = $0', 'd'); + } else if (!filterUsers && filterRooms) { + data = data.filtered('t != $0', 'd'); + } + data = data.slice(0, 7); + + const usernames = data.map(sub => sub.name); + try { + if (data.length < 7) { + if (this.oldPromise) { + this.oldPromise('cancel'); + } + + const { users, rooms } = await Promise.race([ + RocketChat.spotlight(searchText, usernames, { users: filterUsers, rooms: filterRooms }), + new Promise((resolve, reject) => this.oldPromise = reject) + ]); + + data = data.concat(users.map(user => ({ + ...user, + rid: user.username, + name: user.username, + t: 'd', + search: true + })), rooms.map(room => ({ + rid: room._id, + ...room, + search: true + }))); + + delete this.oldPromise; + } + + return data; + } catch (e) { + console.warn(e); + return []; + } + }, + spotlight(search, usernames, type) { return call('spotlight', search, usernames, type); }, diff --git a/app/presentation/UserItem.js b/app/presentation/UserItem.js index cf8c07de2afccdac7a207b5abb6d315cb95735f9..f4e03dbd4bae41ff18ede618ca2e2174ee66dea4 100644 --- a/app/presentation/UserItem.js +++ b/app/presentation/UserItem.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Text, View, StyleSheet, Platform } from 'react-native'; +import { Text, View, StyleSheet, Platform, ViewPropTypes, Image } from 'react-native'; import PropTypes from 'prop-types'; import Avatar from '../containers/Avatar'; @@ -7,7 +7,8 @@ import Touch from '../utils/touch'; const styles = StyleSheet.create({ button: { - height: 54 + height: 54, + backgroundColor: '#fff' }, container: { flexDirection: 'row' @@ -24,24 +25,33 @@ const styles = StyleSheet.create({ fontSize: 18, color: '#0C0D0F', marginTop: Platform.OS === 'ios' ? 6 : 3, - marginBottom: 1 + marginBottom: 1, + textAlign: 'left' }, username: { fontSize: 14, color: '#9EA2A8' + }, + icon: { + width: 20, + height: 20, + marginHorizontal: 15, + resizeMode: 'contain', + alignSelf: 'center' } }); const UserItem = ({ - name, username, onPress, testID, onLongPress + name, username, onPress, testID, onLongPress, style, icon }) => ( <Touch onPress={onPress} onLongPress={onLongPress} style={styles.button} testID={testID}> - <View style={styles.container}> + <View style={[styles.container, style]}> <Avatar text={username} size={30} type='d' style={styles.avatar} /> <View style={styles.textContainer}> <Text style={styles.name}>{name}</Text> <Text style={styles.username}>@{username}</Text> </View> + {icon ? <Image source={{ uri: icon }} style={styles.icon} /> : null} </View> </Touch> ); @@ -51,7 +61,9 @@ UserItem.propTypes = { username: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired, testID: PropTypes.string.isRequired, - onLongPress: PropTypes.func + onLongPress: PropTypes.func, + style: ViewPropTypes.style, + icon: PropTypes.string }; export default UserItem; diff --git a/app/reducers/server.js b/app/reducers/server.js index db6d5911949c73fd866a7f175b8724b66f6a8a72..a423033e29ce8cfa1f98e209a635e37ea0a5f83b 100644 --- a/app/reducers/server.js +++ b/app/reducers/server.js @@ -6,7 +6,7 @@ const initialState = { failure: false, server: '', loading: true, - adding: true + adding: false }; @@ -16,16 +16,14 @@ export default function server(state = initialState, action) { return { ...state, connecting: true, - failure: false, - adding: true + failure: false }; case SERVER.FAILURE: return { ...state, connecting: false, connected: false, - failure: true, - adding: false + failure: true }; case SERVER.SELECT_REQUEST: return { @@ -43,6 +41,16 @@ export default function server(state = initialState, action) { connected: true, loading: false }; + case SERVER.INIT_ADD: + return { + ...state, + adding: true + }; + case SERVER.FINISH_ADD: + return { + ...state, + adding: false + }; default: return state; } diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index 9f64e3b40962445836a73e66dd72a04bcbd38f34..bd5ff2d91dc42e3e2504bc9d2659a4b7c33dc12c 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -12,25 +12,26 @@ const create = function* create(data) { const handleRequest = function* handleRequest({ data }) { try { - // yield delay(1000); const auth = yield select(state => state.login.isAuthenticated); if (!auth) { yield take(LOGIN.SUCCESS); } const result = yield call(create, data); + yield put(createChannelSuccess(result)); + yield delay(300); const { rid, name } = result; - NavigationActions.popToRoot(); - yield delay(1000); + NavigationActions.dismissModal(); + yield delay(600); NavigationActions.push({ screen: 'RoomView', title: name, + backButtonTitle: '', passProps: { room: { rid, name }, rid, name } }); - yield put(createChannelSuccess(result)); } catch (err) { yield put(createChannelFailure(err)); } diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index ce21d94b49b098d0b64cd0e0636a452d8dc0d550..8f3bf1d6fba0557f975157a04cbc29613beb81ac 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -17,6 +17,7 @@ const navigate = function* go({ params, sameServer = true }) { if (canOpenRoom) { return NavigationActions.push({ screen: 'RoomView', + backButtonTitle: '', passProps: { rid: params.rid } diff --git a/app/sagas/login.js b/app/sagas/login.js index 12f1b01e12bdf145d7a350d945e14d31c6ad973c..93c1ce86e569d998f9ec04520729870635f9d336 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -4,6 +4,7 @@ import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions'; +import { serverFinishAdd } from '../actions/server'; import { // loginRequest, // loginSubmit, @@ -38,13 +39,18 @@ const forgotPasswordCall = args => RocketChat.forgotPassword(args); const handleLoginSuccess = function* handleLoginSuccess() { try { const user = yield select(getUser); + const adding = yield select(state => state.server.adding); yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); if (!user.username || user.isRegistering) { yield put(registerIncomplete()); } else { yield delay(300); - NavigationActions.dismissModal(); - yield put(appStart('inside')); + if (adding) { + NavigationActions.dismissModal(); + } else { + yield put(appStart('inside')); + } + yield put(serverFinishAdd()); } } catch (e) { log('handleLoginSuccess', e); diff --git a/app/sagas/messages.js b/app/sagas/messages.js index f959fd18b1c3cbf3e89fa0219919c77ef0c1c06f..556b66a7d0ae149af48300296e40156c84d6b921 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -80,6 +80,7 @@ const goRoom = function* goRoom({ rid, name }) { yield delay(1000); NavigationActions.push({ screen: 'RoomView', + backButtonTitle: '', passProps: { room: { rid, name }, rid, diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 9a1e4f24890cc81aac6df6872c4803ec0c58cd4a..be132c445d16259bd194216a18439fa4d2b06dd5 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -44,7 +44,7 @@ const handleSelectServer = function* handleSelectServer({ server }) { const handleServerRequest = function* handleServerRequest({ server }) { try { yield call(validate, server); - yield call(NavigationActions.push, { screen: 'LoginSignupView', title: server }); + yield call(NavigationActions.push, { screen: 'LoginSignupView', title: server, backButtonTitle: '' }); database.databases.serversDB.write(() => { database.databases.serversDB.create('servers', { id: server, current: false }, true); }); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 9f9a62cabc57e92696d6106605ab29f52ed8304a..5ce8e870b22f2c9eaeed86254248e54faaf6d563 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -1,29 +1,85 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { View, Text, Switch, SafeAreaView, ScrollView, Platform } from 'react-native'; +import { View, Text, Switch, SafeAreaView, ScrollView, TextInput, StyleSheet, FlatList, Platform } from 'react-native'; -import RCTextInput from '../containers/TextInput'; import Loading from '../containers/Loading'; import LoggedView from './View'; import { createChannelRequest } from '../actions/createChannel'; -import styles from './Styles'; +import { removeUser } from '../actions/selectedUsers'; +import sharedStyles from './Styles'; import KeyboardView from '../presentation/KeyboardView'; import scrollPersistTaps from '../utils/scrollPersistTaps'; -import Button from '../containers/Button'; import I18n from '../i18n'; +import UserItem from '../presentation/UserItem'; +import { showErrorAlert } from '../utils/info'; + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#f7f8fa' + }, + list: { + width: '100%', + backgroundColor: '#FFFFFF' + }, + separator: { + marginLeft: 60 + }, + formSeparator: { + marginLeft: 15 + }, + input: { + height: 54, + paddingHorizontal: 18, + color: '#9EA2A8', + backgroundColor: '#fff', + fontSize: 18 + }, + swithContainer: { + height: 54, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + paddingHorizontal: 18 + }, + label: { + color: '#0C0D0F', + fontSize: 18, + fontWeight: '500' + }, + invitedHeader: { + marginTop: 18, + marginHorizontal: 15, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + invitedTitle: { + color: '#2F343D', + fontSize: 22, + fontWeight: 'bold', + lineHeight: 41 + }, + invitedCount: { + color: '#9EA2A8', + fontSize: 15 + } +}); @connect(state => ({ createChannel: state.createChannel, users: state.selectedUsers.users }), dispatch => ({ - create: data => dispatch(createChannelRequest(data)) + create: data => dispatch(createChannelRequest(data)), + removeUser: user => dispatch(removeUser(user)) })) /** @extends React.Component */ export default class CreateChannelView extends LoggedView { static propTypes = { navigator: PropTypes.object, create: PropTypes.func.isRequired, + removeUser: PropTypes.func.isRequired, createChannel: PropTypes.object.isRequired, users: PropTypes.array.isRequired }; @@ -36,6 +92,7 @@ export default class CreateChannelView extends LoggedView { readOnly: false, broadcast: false }; + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); } componentDidMount() { @@ -44,6 +101,36 @@ export default class CreateChannelView extends LoggedView { }, 600); } + componentDidUpdate(prevProps) { + if (this.props.createChannel.error && prevProps.createChannel.error !== this.props.createChannel.error) { + setTimeout(() => { + const msg = this.props.createChannel.error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); + showErrorAlert(msg); + }, 300); + } + } + + onChangeText = (channelName) => { + const rightButtons = []; + if (channelName.trim().length > 0) { + rightButtons.push({ + id: 'create', + title: 'Create', + testID: 'create-channel-submit' + }); + } + this.props.navigator.setButtons({ rightButtons }); + this.setState({ channelName }); + } + + async onNavigatorEvent(event) { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'create') { + this.submit(); + } + } + } + submit = () => { if (!this.state.channelName.trim() || this.props.createChannel.isFetching) { return; @@ -62,47 +149,35 @@ export default class CreateChannelView extends LoggedView { }); } - renderChannelNameError() { - if ( - !this.props.createChannel.failure || - this.props.createChannel.error.error !== 'error-duplicate-channel-name' - ) { - return null; + removeUser = (user) => { + if (this.props.users.length === 1) { + return; } - - return ( - <Text style={[styles.label_white, styles.label_error]} testID='create-channel-error'> - {this.props.createChannel.error.reason} - </Text> - ); + this.props.removeUser(user); } renderSwitch = ({ - id, value, label, description, onValueChange, disabled = false + id, value, label, onValueChange, disabled = false }) => ( - <View style={{ marginBottom: 15 }}> - <View style={styles.switchContainer}> - <Switch - value={value} - onValueChange={onValueChange} - testID={`create-channel-${ id }`} - onTintColor='#2de0a5' - tintColor={Platform.OS === 'android' ? '#f5455c' : null} - disabled={disabled} - /> - <Text style={styles.switchLabel}>{label}</Text> - </View> - <Text style={styles.switchDescription}>{description}</Text> + <View style={styles.swithContainer}> + <Text style={styles.label}>{I18n.t(label)}</Text> + <Switch + value={value} + onValueChange={onValueChange} + testID={`create-channel-${ id }`} + onTintColor='#2de0a5' + tintColor={Platform.OS === 'android' ? '#f5455c' : null} + disabled={disabled} + /> </View> - ); + ) renderType() { const { type } = this.state; return this.renderSwitch({ id: 'type', value: type, - label: type ? I18n.t('Private_Channel') : I18n.t('Public_Channel'), - description: type ? I18n.t('Just_invited_people_can_access_this_channel') : I18n.t('Everyone_can_access_this_channel'), + label: 'Private_Channel', onValueChange: value => this.setState({ type: value }) }); } @@ -112,8 +187,7 @@ export default class CreateChannelView extends LoggedView { return this.renderSwitch({ id: 'readonly', value: readOnly, - label: I18n.t('Read_Only_Channel'), - description: readOnly ? I18n.t('Only_authorized_users_can_write_new_messages') : I18n.t('All_users_in_the_channel_can_write_new_messages'), + label: 'Read_Only_Channel', onValueChange: value => this.setState({ readOnly: value }), disabled: broadcast }); @@ -124,8 +198,7 @@ export default class CreateChannelView extends LoggedView { return this.renderSwitch({ id: 'broadcast', value: broadcast, - label: I18n.t('Broadcast_Channel'), - description: I18n.t('Broadcast_channel_Description'), + label: 'Broadcast_Channel', onValueChange: (value) => { this.setState({ broadcast: value, @@ -135,39 +208,70 @@ export default class CreateChannelView extends LoggedView { }); } + renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> + + renderFormSeparator = () => <View style={[sharedStyles.separator, styles.formSeparator]} /> + + renderItem = ({ item }) => ( + <UserItem + name={item.fname} + username={item.name} + onPress={() => this.removeUser(item)} + testID={`create-channel-view-item-${ item.name }`} + /> + ) + + renderInvitedList = () => ( + <FlatList + data={this.props.users} + extraData={this.props.users} + keyExtractor={item => item._id} + style={[styles.list, sharedStyles.separatorVertical]} + renderItem={this.renderItem} + ItemSeparatorComponent={this.renderSeparator} + enableEmptySections + keyboardShouldPersistTaps='always' + /> + ) + render() { + const userCount = this.props.users.length; return ( <KeyboardView - contentContainerStyle={styles.container} + contentContainerStyle={[sharedStyles.container, styles.container]} keyboardVerticalOffset={128} > - <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> - <SafeAreaView testID='create-channel-view'> - <RCTextInput - inputRef={ref => this.channelNameRef = ref} - label={I18n.t('Channel_Name')} - value={this.state.channelName} - onChangeText={channelName => this.setState({ channelName })} - placeholder={I18n.t('Type_the_channel_name_here')} - returnKeyType='done' - testID='create-channel-name' - /> - {this.renderChannelNameError()} - {this.renderType()} - {this.renderReadOnly()} - {this.renderBroadcast()} - <View style={styles.alignItemsFlexStart}> - <Button - title={I18n.t('Create')} - type='primary' - onPress={this.submit} - disabled={this.state.channelName.length === 0 || this.props.createChannel.isFetching} - testID='create-channel-submit' + <SafeAreaView testID='create-channel-view'> + <ScrollView {...scrollPersistTaps}> + <View style={sharedStyles.separatorVertical}> + <TextInput + ref={ref => this.channelNameRef = ref} + style={styles.input} + label={I18n.t('Channel_Name')} + value={this.state.channelName} + onChangeText={this.onChangeText} + placeholder={I18n.t('Channel_Name')} + returnKeyType='done' + testID='create-channel-name' + autoCorrect={false} + autoCapitalize='none' + underlineColorAndroid='transparent' /> + {this.renderFormSeparator()} + {this.renderType()} + {this.renderFormSeparator()} + {this.renderReadOnly()} + {this.renderFormSeparator()} + {this.renderBroadcast()} + </View> + <View style={styles.invitedHeader}> + <Text style={styles.invitedTitle}>{I18n.t('Invite')}</Text> + <Text style={styles.invitedCount}>{userCount === 1 ? I18n.t('1_user') : I18n.t('N_users', { n: userCount })}</Text> </View> + {this.renderInvitedList()} <Loading visible={this.props.createChannel.isFetching} /> - </SafeAreaView> - </ScrollView> + </ScrollView> + </SafeAreaView> </KeyboardView> ); } diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index 9560021fd58c40321408e00482071fe5e800dc67..a9e86e178a909cb9505233e2f5f7650ab87fcf42 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -182,7 +182,7 @@ export default class LoginSignupView extends LoggedView { this.props.navigator.push({ screen: 'LoginView', title: this.props.server, - backButtonTitle: I18n.t('Welcome') + backButtonTitle: '' }); } @@ -190,7 +190,7 @@ export default class LoginSignupView extends LoggedView { this.props.navigator.push({ screen: 'RegisterView', title: this.props.server, - backButtonTitle: I18n.t('Welcome') + backButtonTitle: '' }); } diff --git a/app/views/LoginView.js b/app/views/LoginView.js index 0050d5592e68aca795538d8c234ed99181076b9d..aa8c6110aef4e86ab74a16a505e6f6bbe43a3e90 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -68,7 +68,7 @@ export default class LoginView extends LoggedView { this.props.navigator.push({ screen: 'RegisterView', title: this.props.server, - backButtonTitle: I18n.t('Login') + backButtonTitle: '' }); } @@ -76,7 +76,7 @@ export default class LoginView extends LoggedView { this.props.navigator.push({ screen: 'ForgotPasswordView', title: I18n.t('Forgot_Password'), - backButtonTitle: I18n.t('Login') + backButtonTitle: '' }); } diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js new file mode 100644 index 0000000000000000000000000000000000000000..13ac58641f64de0921369e0399d720e5ca207246 --- /dev/null +++ b/app/views/NewMessageView.js @@ -0,0 +1,166 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet, SafeAreaView, FlatList, Text, Platform, Image } from 'react-native'; + +import database from '../lib/realm'; +import RocketChat from '../lib/rocketchat'; +import UserItem from '../presentation/UserItem'; +import debounce from '../utils/debounce'; +import LoggedView from './View'; +import sharedStyles from './Styles'; +import I18n from '../i18n'; +import Touch from '../utils/touch'; +import SearchBox from '../containers/SearchBox'; + +const styles = StyleSheet.create({ + safeAreaView: { + flex: 1, + backgroundColor: Platform.OS === 'ios' ? '#F7F8FA' : '#E1E5E8' + }, + separator: { + marginLeft: 60 + }, + createChannelButton: { + marginVertical: 25 + }, + createChannelContainer: { + height: 47, + backgroundColor: '#fff', + flexDirection: 'row', + alignItems: 'center' + }, + createChannelIcon: { + width: 24, + height: 24, + marginHorizontal: 18 + }, + createChannelText: { + color: '#1D74F5', + fontSize: 18 + } +}); + +/** @extends React.Component */ +export default class SelectedUsersView extends LoggedView { + static navigatorButtons = { + leftButtons: [{ + id: 'cancel', + title: I18n.t('Cancel') + }] + } + + static propTypes = { + navigator: PropTypes.object, + onPressItem: PropTypes.func.isRequired + }; + + constructor(props) { + super('NewMessageView', props); + this.data = database.objects('subscriptions').filtered('t = $0', 'd').sorted('roomUpdatedAt', true); + this.state = { + search: [] + }; + this.data.addListener(this.updateState); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); + } + + componentWillUnmount() { + this.updateState.stop(); + this.data.removeAllListeners(); + } + + async onNavigatorEvent(event) { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'cancel') { + this.props.navigator.dismissModal(); + } + } + } + + onSearchChangeText(text) { + this.search(text); + } + + onPressItem = (item) => { + this.props.navigator.dismissModal(); + setTimeout(() => { + this.props.onPressItem(item); + }, 600); + } + + updateState = debounce(() => { + this.forceUpdate(); + }, 1000); + + search = async(text) => { + const result = await RocketChat.search({ text, filterRooms: false }); + this.setState({ + search: result + }); + } + + createChannel = () => { + this.props.navigator.push({ + screen: 'SelectedUsersView', + title: I18n.t('Select_Users'), + backButtonTitle: '', + passProps: { + nextAction: 'CREATE_CHANNEL' + } + }); + } + + renderHeader = () => ( + <View> + <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' /> + <Touch onPress={this.createChannel} style={styles.createChannelButton} testID='new-message-view-create-channel'> + <View style={[sharedStyles.separatorVertical, styles.createChannelContainer]}> + <Image style={styles.createChannelIcon} source={{ uri: 'plus' }} /> + <Text style={styles.createChannelText}>{I18n.t('Create_Channel')}</Text> + </View> + </Touch> + </View> + ) + + renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; + + renderItem = ({ item, index }) => { + let style = {}; + if (index === 0) { + style = { ...sharedStyles.separatorTop }; + } + if (this.state.search.length > 0 && index === this.state.search.length - 1) { + style = { ...style, ...sharedStyles.separatorBottom }; + } + if (this.state.search.length === 0 && index === this.data.length - 1) { + style = { ...style, ...sharedStyles.separatorBottom }; + } + return ( + <UserItem + name={item.search ? item.name : item.fname} + username={item.search ? item.username : item.name} + onPress={() => this.onPressItem(item)} + testID={`new-message-view-item-${ item.name }`} + style={style} + /> + ); + } + + renderList = () => ( + <FlatList + data={this.state.search.length > 0 ? this.state.search : this.data} + extraData={this.state} + keyExtractor={item => item._id} + ListHeaderComponent={this.renderHeader} + renderItem={this.renderItem} + ItemSeparatorComponent={this.renderSeparator} + keyboardShouldPersistTaps='always' + /> + ) + + render = () => ( + <SafeAreaView style={styles.safeAreaView} testID='new-message-view'> + {this.renderList()} + </SafeAreaView> + ); +} diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 66e14a7b044c20b59179b40a8e5d2156eeca86c1..cbff4379a9322b6e315170bf08da1bec072091e1 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, ScrollView, Keyboard, SafeAreaView, Image, Alert, StyleSheet, Platform } from 'react-native'; +import { Text, ScrollView, Keyboard, SafeAreaView, Image, Alert, StyleSheet } from 'react-native'; import { connect } from 'react-redux'; -import { serverRequest } from '../actions/server'; +import { serverRequest, selectServerRequest, serverInitAdd, serverFinishAdd } from '../actions/server'; import sharedStyles from './Styles'; import scrollPersistTaps from '../utils/scrollPersistTaps'; import Button from '../containers/Button'; @@ -12,7 +12,6 @@ import LoggedView from './View'; import I18n from '../i18n'; import { scale, verticalScale, moderateScale } from '../utils/scaling'; import KeyboardView from '../presentation/KeyboardView'; -import { iconsMap } from '../Icons'; const styles = StyleSheet.create({ image: { @@ -45,9 +44,13 @@ const defaultServer = 'https://open.rocket.chat'; @connect(state => ({ connecting: state.server.connecting, failure: state.server.failure, - currentServer: state.server.server + currentServer: state.server.server, + adding: state.server.adding }), dispatch => ({ - connectServer: url => dispatch(serverRequest(url)) + initAdd: () => dispatch(serverInitAdd()), + finishAdd: () => dispatch(serverFinishAdd()), + connectServer: server => dispatch(serverRequest(server)), + selectServer: server => dispatch(selectServerRequest(server)) })) /** @extends React.Component */ export default class NewServerView extends LoggedView { @@ -55,10 +58,14 @@ export default class NewServerView extends LoggedView { navigator: PropTypes.object, server: PropTypes.string, connecting: PropTypes.bool.isRequired, + adding: PropTypes.bool, failure: PropTypes.bool.isRequired, connectServer: PropTypes.func.isRequired, + selectServer: PropTypes.func.isRequired, previousServer: PropTypes.string, - currentServer: PropTypes.string + currentServer: PropTypes.string, + initAdd: PropTypes.func, + finishAdd: PropTypes.func } constructor(props) { @@ -69,25 +76,8 @@ export default class NewServerView extends LoggedView { props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); } - componentWillMount() { - // if previousServer exists, New Server View is a modal - if (this.props.previousServer) { - const closeButton = { - id: 'close', - testID: 'new-server-close', - title: I18n.t('Close') - }; - if (Platform.OS === 'android') { - closeButton.icon = iconsMap.close; - } - this.props.navigator.setButtons({ - leftButtons: [closeButton] - }); - } - } - componentDidMount() { - const { server } = this.props; + const { server, previousServer } = this.props; if (server) { this.props.connectServer(server); this.setState({ text: server }); @@ -96,6 +86,9 @@ export default class NewServerView extends LoggedView { this.input.focus(); }, 600); } + if (previousServer) { + this.props.initAdd(); + } } componentWillReceiveProps(nextProps) { @@ -104,16 +97,22 @@ export default class NewServerView extends LoggedView { } } + componentWillUnmount() { + const { + selectServer, previousServer, currentServer, adding, finishAdd + } = this.props; + if (adding) { + if (previousServer !== currentServer) { + selectServer(previousServer); + } + finishAdd(); + } + } + onNavigatorEvent(event) { if (event.type === 'NavBarButtonPress') { - if (event.id === 'close') { - const { - navigator, connectServer, previousServer, currentServer - } = this.props; - navigator.dismissModal(); - if (previousServer !== currentServer) { - connectServer(previousServer); - } + if (event.id === 'cancel') { + this.props.navigator.dismissModal(); } } } diff --git a/app/views/OAuthView.js b/app/views/OAuthView.js index e2c73547222385dce0d1bd497681a367bb9d0071..c054955e6e5044de1fef07201d81ca7fb4c32833 100644 --- a/app/views/OAuthView.js +++ b/app/views/OAuthView.js @@ -13,7 +13,7 @@ const userAgent = Platform.OS === 'ios' ? 'UserAgent' : userAgentAndroid; @connect(state => ({ server: state.server.server })) -export default class TermsServiceView extends React.PureComponent { +export default class OAuthView extends React.PureComponent { static navigatorButtons = { leftButtons: [{ id: 'close', diff --git a/app/views/OnboardingView/index.js b/app/views/OnboardingView/index.js index 5d5ed15bd71a932d20a695d373957fa8924607aa..0a9501a64506d6145588dd79cff833742c35d8c6 100644 --- a/app/views/OnboardingView/index.js +++ b/app/views/OnboardingView/index.js @@ -21,6 +21,7 @@ export default class OnboardingView extends LoggedView { connectServer = () => { this.props.navigator.push({ screen: 'NewServerView', + backButtonTitle: '', navigatorStyle: { navBarHidden: true } @@ -30,6 +31,7 @@ export default class OnboardingView extends LoggedView { joinCommunity = () => { this.props.navigator.push({ screen: 'NewServerView', + backButtonTitle: '', passProps: { server: 'https://open.rocket.chat' }, diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 0b2fb65ec3827c1092eae064c351a7f8a10d19d7..94454f23f76c04b5fe91aca8b495edc83d043005 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, ScrollView, SafeAreaView, Keyboard, Platform, Dimensions } from 'react-native'; +import { View, ScrollView, SafeAreaView, Keyboard, Dimensions } from 'react-native'; import { connect } from 'react-redux'; import Dialog from 'react-native-dialog'; import SHA256 from 'js-sha256'; @@ -61,7 +61,7 @@ export default class ProfileView extends LoggedView { componentWillMount() { this.props.navigator.setButtons({ leftButtons: [{ - id: 'sideMenu', + id: 'settings', icon: { uri: 'settings', scale: Dimensions.get('window').scale } }] }); @@ -91,7 +91,7 @@ export default class ProfileView extends LoggedView { onNavigatorEvent(event) { if (event.type === 'NavBarButtonPress') { - if (event.id === 'sideMenu' && Platform.OS === 'ios') { + if (event.id === 'settings') { this.props.navigator.toggleDrawer({ side: 'left' }); diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index ff7e65b0b5651ed2a66137ef9d8d6e0f14093d34..e5e91483d0cca66112263edfffa71eec4f5793eb 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -93,7 +93,7 @@ export default class RegisterView extends LoggedView { this.props.navigator.push({ screen: 'TermsServiceView', title: I18n.t('Terms_of_Service'), - backButtonTitle: I18n.t('Sign_Up') + backButtonTitle: '' }); } @@ -101,7 +101,7 @@ export default class RegisterView extends LoggedView { this.props.navigator.push({ screen: 'PrivacyPolicyView', title: I18n.t('Privacy_Policy'), - backButtonTitle: I18n.t('Sign_Up') + backButtonTitle: '' }); } diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 93af3e2bdc62176fb63d493cb712fddd442e8c78..f17dc54385779f564eb0d497d4ec6039751d2c7c 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -65,7 +65,8 @@ export default class RoomActionsView extends LoggedView { this.props.navigator.push({ screen: item.route, title: item.name, - passProps: item.params + passProps: item.params, + backButtonTitle: '' }); } if (item.event) { diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 69343c69e6c7453af44e9468c676f7d7ddee4c77..89a382a157acca8ebe0cf8539b4b766824fea4e1 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -118,6 +118,7 @@ export default class RoomInfoView extends LoggedView { this.props.navigator.push({ screen: 'RoomInfoEditView', title: I18n.t('Room_Info_Edit'), + backButtonTitle: '', passProps: { rid: this.props.rid } diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 65e8a235e2ac101aa7c84406eb631cda2ed2786c..1e6427c8639bcc78fddc0cc0ce815a28f47edc3b 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View, TextInput, Vibration, SafeAreaView } from 'react-native'; +import { FlatList, View, Vibration, SafeAreaView } from 'react-native'; import ActionSheet from 'react-native-actionsheet'; import LoggedView from '../View'; @@ -12,6 +12,7 @@ import database from '../../lib/realm'; import { showToast } from '../../utils/info'; import log from '../../utils/log'; import I18n from '../../i18n'; +import SearchBox from '../../containers/SearchBox'; /** @extends React.Component */ export default class RoomMembersView extends LoggedView { @@ -132,6 +133,7 @@ export default class RoomMembersView extends LoggedView { this.props.navigator.push({ screen: 'RoomView', title: name, + backButtonTitle: '', passProps: { room: { rid, name }, rid, @@ -162,20 +164,7 @@ export default class RoomMembersView extends LoggedView { } renderSearchBar = () => ( - <View style={styles.searchBoxView}> - <TextInput - underlineColorAndroid='transparent' - style={styles.searchBox} - onChangeText={text => this.onSearchChangeText(text)} - returnKeyType='search' - placeholder={I18n.t('Search')} - clearButtonMode='while-editing' - blurOnSubmit - autoCorrect={false} - autoCapitalize='none' - testID='room-members-view-search' - /> - </View> + <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='room-members-view-search' /> ) renderSeparator = () => <View style={styles.separator} />; diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index a754e573617f448573f757a4c6b700a6b914f13f..b739e938909ae1e9a3a47acef35fed02f24fba1e 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -127,6 +127,7 @@ export default class RoomView extends LoggedView { this.props.navigator.push({ screen: 'RoomActionsView', title: I18n.t('Actions'), + backButtonTitle: '', passProps: { rid: this.state.room.rid } diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index b399780e38c18b287857a883f3bd0dca75a9a669..92508ceb2f26b12c6ce62a544ea47940eee731d0 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -86,6 +86,13 @@ export default class ServerDropdown extends Component { title: I18n.t('Add_Server'), passProps: { previousServer: this.props.server + }, + navigatorButtons: { + leftButtons: [{ + id: 'cancel', + testID: 'new-server-close', + title: I18n.t('Close') + }] } }); }, ANIMATION_DURATION); @@ -101,6 +108,7 @@ export default class ServerDropdown extends Component { setTimeout(() => { this.props.navigator.push({ screen: 'NewServerView', + backButtonTitle: '', passProps: { server }, diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index f82dd681abd29acc541157d3d0424a2943586a7d..bc11cacf4b56e92d67685f3fd416e2fa8f9daa4f 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Platform, View, TextInput, FlatList, BackHandler, ActivityIndicator, SafeAreaView, Text, Image, Dimensions, ScrollView, Keyboard } from 'react-native'; +import { Platform, View, FlatList, BackHandler, ActivityIndicator, SafeAreaView, Text, Image, Dimensions, ScrollView, Keyboard } from 'react-native'; import { connect } from 'react-redux'; import { isEqual } from 'lodash'; +import SearchBox from '../../containers/SearchBox'; import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import RoomItem from '../../presentation/RoomItem'; @@ -26,7 +27,7 @@ const leftButtons = [{ testID: 'rooms-list-view-sidebar' }]; const rightButtons = [{ - id: 'createChannel', + id: 'newMessage', icon: { uri: 'new_channel', scale: Dimensions.get('window').scale }, testID: 'rooms-list-view-create-channel' }]; @@ -38,7 +39,6 @@ if (Platform.OS === 'android') { }); } - @connect((state) => { let result = { userId: state.login.user && state.login.user.id, @@ -74,7 +74,6 @@ export default class RoomsListView extends LoggedView { static navigatorStyle = { navBarCustomView: 'RoomsListHeaderView', - navBarComponentAlignment: 'fill', navBarBackgroundColor: isAndroid() ? '#2F343D' : undefined, navBarTextColor: isAndroid() ? '#FFF' : undefined, navBarButtonColor: isAndroid() ? '#FFF' : undefined @@ -157,6 +156,10 @@ export default class RoomsListView extends LoggedView { this.removeListener(this.direct); this.removeListener(this.livechat); + if (database && database.deleteAll) { + database.deleteAll(); + } + if (this.timeout) { clearTimeout(this.timeout); } @@ -165,12 +168,12 @@ export default class RoomsListView extends LoggedView { onNavigatorEvent(event) { const { navigator } = this.props; if (event.type === 'NavBarButtonPress') { - if (event.id === 'createChannel') { - this.props.navigator.push({ - screen: 'SelectedUsersView', - title: I18n.t('Select_Users'), + if (event.id === 'newMessage') { + this.props.navigator.showModal({ + screen: 'NewMessageView', + title: I18n.t('New_Message'), passProps: { - nextAction: 'CREATE_CHANNEL' + onPressItem: this._onPressItem } }); } else if (event.id === 'settings') { @@ -190,10 +193,6 @@ export default class RoomsListView extends LoggedView { } } - onSearchChangeText(text) { - this.search(text); - } - getSubscriptions = () => { if (this.props.server && this.hasActiveDB()) { if (this.props.sidebarSortby === 'alphabetical') { @@ -285,7 +284,6 @@ export default class RoomsListView extends LoggedView { navigator.setButtons({ leftButtons, rightButtons }); navigator.setStyle({ navBarCustomView: 'RoomsListHeaderView', - navBarComponentAlignment: 'fill', navBarBackgroundColor: isAndroid() ? '#2F343D' : undefined, navBarTextColor: isAndroid() ? '#FFF' : undefined, navBarButtonColor: isAndroid() ? '#FFF' : undefined @@ -327,55 +325,18 @@ export default class RoomsListView extends LoggedView { _isUnread = item => item.unread > 0 || item.alert - async search(text) { - const searchText = text.trim(); - if (searchText === '') { - delete this.oldPromise; - return this.setState({ - search: [] - }); - } - - let data = database.objects('subscriptions').filtered('name CONTAINS[c] $0', searchText).slice(0, 7); - - const usernames = data.map(sub => sub.name); - try { - if (data.length < 7) { - if (this.oldPromise) { - this.oldPromise('cancel'); - } - - const { users, rooms } = await Promise.race([ - RocketChat.spotlight(searchText, usernames, { users: true, rooms: true }), - new Promise((resolve, reject) => this.oldPromise = reject) - ]); - - data = data.concat(users.map(user => ({ - ...user, - rid: user.username, - name: user.username, - t: 'd', - search: true - })), rooms.map(room => ({ - rid: room._id, - ...room, - search: true - }))); - - delete this.oldPromise; - } - this.setState({ - search: data - }); - } catch (e) { - // alert(JSON.stringify(e)); - } + search = async(text) => { + const result = await RocketChat.search({ text }); + this.setState({ + search: result + }); } goRoom = (rid, name) => { this.props.navigator.push({ screen: 'RoomView', title: name, + backButtonTitle: '', passProps: { room: { rid, name }, rid, @@ -429,22 +390,7 @@ export default class RoomsListView extends LoggedView { renderSearchBar = () => { if (Platform.OS === 'ios') { - return ( - <View style={styles.searchBoxView}> - <TextInput - underlineColorAndroid='transparent' - style={styles.searchBox} - onChangeText={text => this.onSearchChangeText(text)} - returnKeyType='search' - placeholder={I18n.t('Search')} - clearButtonMode='while-editing' - blurOnSubmit - autoCorrect={false} - autoCapitalize='none' - testID='rooms-list-view-search' - /> - </View> - ); + return <SearchBox onChangeText={text => this.search(text)} testID='rooms-list-view-search' />; } } @@ -537,7 +483,7 @@ export default class RoomsListView extends LoggedView { return ( <ScrollView - contentOffset={Platform.OS === 'ios' ? { x: 0, y: 37 } : {}} + contentOffset={Platform.OS === 'ios' ? { x: 0, y: 56 } : {}} keyboardShouldPersistTaps='always' testID='rooms-list-view-list' > diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 1f5afa5ea3c24fbe8ea41299201c0ad94f46a03e..433289e7965fc0ddcfd0f204ea70c0d2755a2ba5 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -31,17 +31,6 @@ export default StyleSheet.create({ height: 22, color: 'white' }, - searchBoxView: { - backgroundColor: '#eee' - }, - searchBox: { - backgroundColor: '#fff', - margin: 5, - borderRadius: 5, - padding: 5, - paddingLeft: 10, - color: '#aaa' - }, loading: { flex: 1 }, diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index beb94b16d5180dfc210b039747c14cfcf814efd6..45c091a3f931f43b276fe5ddddfdc0eef3e70134 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -1,56 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, TextInput, Text, TouchableOpacity, SafeAreaView, FlatList, LayoutAnimation } from 'react-native'; +import { View, StyleSheet, SafeAreaView, FlatList, LayoutAnimation, Platform } from 'react-native'; import { connect } from 'react-redux'; import { addUser, removeUser, reset, setLoading } from '../actions/selectedUsers'; import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; import UserItem from '../presentation/UserItem'; -import Avatar from '../containers/Avatar'; import Loading from '../containers/Loading'; import debounce from '../utils/debounce'; import LoggedView from './View'; import I18n from '../i18n'; import log from '../utils/log'; -import { iconsMap } from '../Icons'; +import SearchBox from '../containers/SearchBox'; +import sharedStyles from './Styles'; const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'stretch', - justifyContent: 'center' - }, safeAreaView: { flex: 1, - backgroundColor: '#FFFFFF' - }, - list: { - width: '100%', - backgroundColor: '#FFFFFF' + backgroundColor: Platform.OS === 'ios' ? '#F7F8FA' : '#E1E5E8' }, - searchBoxView: { - backgroundColor: '#eee' - }, - searchBox: { - backgroundColor: '#fff', - margin: 5, - borderRadius: 5, - padding: 5, - paddingLeft: 10, - color: '#aaa' - }, - selectItemView: { - width: 80, - height: 80, - padding: 8, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center' + header: { + backgroundColor: '#fff' }, separator: { - height: StyleSheet.hairlineWidth, - backgroundColor: '#E1E5E8', marginLeft: 60 } }); @@ -95,15 +68,19 @@ export default class SelectedUsersView extends LoggedView { }); } - componentWillReceiveProps(nextProps) { - if (nextProps.users.length !== this.props.users.length) { - const { length } = nextProps.users; + async componentDidUpdate(prevProps) { + const isVisible = await this.props.navigator.screenIsCurrentlyVisible(); + if (!isVisible) { + return; + } + if (prevProps.users.length !== this.props.users.length) { + const { length } = this.props.users; const rightButtons = []; if (length > 0) { rightButtons.push({ id: 'create', - testID: 'selected-users-view-submit', - icon: iconsMap.add + title: I18n.t('Next'), + testID: 'selected-users-view-submit' }); } this.props.navigator.setButtons({ rightButtons }); @@ -123,7 +100,8 @@ export default class SelectedUsersView extends LoggedView { if (nextAction === 'CREATE_CHANNEL') { this.props.navigator.push({ screen: 'CreateChannelView', - title: I18n.t('Create_Channel') + title: I18n.t('Create_Channel'), + backButtonTitle: '' }); } else { try { @@ -148,90 +126,40 @@ export default class SelectedUsersView extends LoggedView { this.forceUpdate(); }, 1000); - async search(text) { - const searchText = text.trim(); - if (searchText === '') { - delete this.oldPromise; - return this.setState({ - search: [] - }); - } - - let data = this.data.filtered('name CONTAINS[c] $0 AND t = $1', searchText, 'd').slice(0, 7); - - const usernames = data.map(sub => sub.map); - try { - if (data.length < 7) { - if (this.oldPromise) { - this.oldPromise('cancel'); - } - - const { users } = await Promise.race([ - RocketChat.spotlight(searchText, usernames, { users: true, rooms: false }), - new Promise((resolve, reject) => this.oldPromise = reject) - ]); - - data = users.map(user => ({ - ...user, - rid: user.username, - name: user.username, - t: 'd', - search: true - })); - - delete this.oldPromise; - } - this.setState({ - search: data - }); - } catch (e) { - // alert(JSON.stringify(e)); - } + search = async(text) => { + const result = await RocketChat.search({ text, filterRooms: false }); + this.setState({ + search: result + }); } + isChecked = username => this.props.users.findIndex(el => el.name === username) !== -1; + toggleUser = (user) => { LayoutAnimation.easeInEaseOut(); - const index = this.props.users.findIndex(el => el.name === user.name); - if (index === -1) { + if (!this.isChecked(user.name)) { this.props.addUser(user); } else { this.props.removeUser(user); } - }; + } _onPressItem = (id, item = {}) => { if (item.search) { - this.toggleUser({ _id: item._id, name: item.username }); + this.toggleUser({ _id: item._id, name: item.username, fname: item.name }); } else { - this.toggleUser({ _id: item._id, name: item.name }); + this.toggleUser({ _id: item._id, name: item.name, fname: item.fname }); } - }; + } _onPressSelectedItem = item => this.toggleUser(item); renderHeader = () => ( - <View style={styles.container}> - {this.renderSearchBar()} + <View style={styles.header}> + <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='select-users-view-search' /> {this.renderSelected()} </View> - ); - - renderSearchBar = () => ( - <View style={styles.searchBoxView}> - <TextInput - underlineColorAndroid='transparent' - style={styles.searchBox} - onChangeText={text => this.onSearchChangeText(text)} - returnKeyType='search' - placeholder={I18n.t('Search')} - clearButtonMode='while-editing' - blurOnSubmit - testID='select-users-view-search' - autoCorrect={false} - autoCapitalize='none' - /> - </View> - ); + ) renderSelected = () => { if (this.props.users.length === 0) { @@ -241,57 +169,70 @@ export default class SelectedUsersView extends LoggedView { <FlatList data={this.props.users} keyExtractor={item => item._id} - style={styles.list} + style={[styles.list, sharedStyles.separatorTop]} + contentContainerStyle={{ marginVertical: 5 }} renderItem={this.renderSelectedItem} enableEmptySections keyboardShouldPersistTaps='always' horizontal /> ); - }; + } renderSelectedItem = ({ item }) => ( - <TouchableOpacity - key={item._id} - style={styles.selectItemView} + <UserItem + name={item.fname} + username={item.name} onPress={() => this._onPressSelectedItem(item)} testID={`selected-user-${ item.name }`} - > - <Avatar text={item.name} size={40} /> - <Text ellipsizeMode='tail' numberOfLines={1} style={{ fontSize: 10 }}> - {item.name} - </Text> - </TouchableOpacity> - ); - - renderSeparator = () => <View style={styles.separator} />; - - renderItem = ({ item }) => ( - <UserItem - name={item.fname ? item.fname : item.name} - username={item.fname ? item.name : item.username} - onPress={() => this._onPressItem(item._id, item)} - testID={`select-users-view-item-${ item.name }`} + style={{ paddingRight: 15 }} /> ) + renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} /> + + renderItem = ({ item, index }) => { + const name = item.search ? item.name : item.fname; + const username = item.search ? item.username : item.name; + let style = {}; + if (index === 0) { + style = { ...sharedStyles.separatorTop }; + } + if (this.state.search.length > 0 && index === this.state.search.length - 1) { + style = { ...style, ...sharedStyles.separatorBottom }; + } + if (this.state.search.length === 0 && index === this.data.length - 1) { + style = { ...style, ...sharedStyles.separatorBottom }; + } + return ( + <UserItem + name={name} + username={username} + onPress={() => this._onPressItem(item._id, item)} + testID={`select-users-view-item-${ item.name }`} + icon={this.isChecked(username) ? 'check' : null} + style={style} + /> + ); + } + renderList = () => ( <FlatList data={this.state.search.length > 0 ? this.state.search : this.data} extraData={this.props} keyExtractor={item => item._id} - style={styles.list} renderItem={this.renderItem} - ListHeaderComponent={this.renderHeader} ItemSeparatorComponent={this.renderSeparator} + ListHeaderComponent={this.renderHeader} enableEmptySections keyboardShouldPersistTaps='always' /> - ); + ) + render = () => ( <SafeAreaView style={styles.safeAreaView} testID='select-users-view'> {this.renderList()} <Loading visible={this.props.loading} /> </SafeAreaView> - ); + ) } diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index bf3b0691953944fd959bf6c2d8f56be904409419..2de1393d06ea30e2d7e81171cd8bbbd8a52683c8 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, ScrollView, SafeAreaView, Platform, Dimensions } from 'react-native'; +import { View, ScrollView, SafeAreaView, Dimensions } from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import { connect } from 'react-redux'; @@ -50,7 +50,7 @@ export default class SettingsView extends LoggedView { componentWillMount() { this.props.navigator.setButtons({ leftButtons: [{ - id: 'sideMenu', + id: 'settings', icon: { uri: 'settings', scale: Dimensions.get('window').scale } }] }); @@ -65,7 +65,7 @@ export default class SettingsView extends LoggedView { onNavigatorEvent(event) { if (event.type === 'NavBarButtonPress') { - if (event.id === 'sideMenu' && Platform.OS === 'ios') { + if (event.id === 'settings') { this.props.navigator.toggleDrawer({ side: 'left' }); diff --git a/app/views/Styles.js b/app/views/Styles.js index c0fe421ff04aab841301bfa810d4e8eb7ace375a..facf01c60938af9dc42f7cb45b41a39d45167d93 100644 --- a/app/views/Styles.js +++ b/app/views/Styles.js @@ -1,6 +1,6 @@ import { StyleSheet, Platform } from 'react-native'; -import { COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_TEXT } from '../constants/colors'; +import { COLOR_DANGER, COLOR_BUTTON_PRIMARY, COLOR_TEXT, COLOR_SEPARATOR } from '../constants/colors'; export default StyleSheet.create({ container: { @@ -197,5 +197,22 @@ export default StyleSheet.create({ width: 44, alignItems: 'center', justifyContent: 'center' + }, + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: COLOR_SEPARATOR + }, + separatorTop: { + borderColor: COLOR_SEPARATOR, + borderTopWidth: StyleSheet.hairlineWidth + }, + separatorBottom: { + borderColor: COLOR_SEPARATOR, + borderBottomWidth: StyleSheet.hairlineWidth + }, + separatorVertical: { + borderColor: COLOR_SEPARATOR, + borderTopWidth: StyleSheet.hairlineWidth, + borderBottomWidth: StyleSheet.hairlineWidth } }); diff --git a/app/views/index.js b/app/views/index.js index 1b47ee6cf788b5a9c94e109f6d8153b6798de4c4..85e1e7a5b5b5137c7757998721d9e303ab3a800e 100644 --- a/app/views/index.js +++ b/app/views/index.js @@ -6,6 +6,7 @@ import ForgotPasswordView from './ForgotPasswordView'; import LoginSignupView from './LoginSignupView'; import LoginView from './LoginView'; import MentionedMessagesView from './MentionedMessagesView'; +import NewMessageView from './NewMessageView'; import NewServerView from './NewServerView'; import OAuthView from './OAuthView'; import OnboardingView from './OnboardingView'; @@ -36,6 +37,7 @@ export const registerScreens = (store) => { Navigation.registerComponent('LoginSignupView', () => LoginSignupView, store, Provider); Navigation.registerComponent('LoginView', () => LoginView, store, Provider); Navigation.registerComponent('MentionedMessagesView', () => MentionedMessagesView, store, Provider); + Navigation.registerComponent('NewMessageView', () => NewMessageView, store, Provider); Navigation.registerComponent('NewServerView', () => NewServerView, store, Provider); Navigation.registerComponent('OAuthView', () => OAuthView, store, Provider); Navigation.registerComponent('OnboardingView', () => OnboardingView, store, Provider); diff --git a/e2e/01-welcome.spec.js b/e2e/01-welcome.spec.js index 5001ef680441440fa97931e45525bfe7d7859676..81a6075b164fcebd2a15eadbf0eb8d3b7351f4c8 100644 --- a/e2e/01-welcome.spec.js +++ b/e2e/01-welcome.spec.js @@ -30,14 +30,14 @@ describe('Welcome screen', () => { await element(by.id('welcome-view-login')).tap(); await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('login-view'))).toBeVisible(); - await tapBack('Welcome'); + await tapBack(); }); it('should navigate to register', async() => { await element(by.id('welcome-view-register')).tap(); await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('register-view'))).toBeVisible(); - await tapBack('Welcome'); + await tapBack(); }); afterEach(async() => { diff --git a/e2e/04-login.spec.js b/e2e/04-login.spec.js index c017646f39cdccd2446a0b4e96fec4baad9e2b4d..4540f91e3fe4c460a017ca0376a8751ab734bfbe 100644 --- a/e2e/04-login.spec.js +++ b/e2e/04-login.spec.js @@ -49,18 +49,18 @@ describe('Login screen', () => { await element(by.id('login-view-register')).tap(); await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('register-view'))).toBeVisible(); - await tapBack('Login'); + await tapBack(); }); it('should navigate to forgot password', async() => { await element(by.id('login-view-forgot-password')).tap(); await waitFor(element(by.id('forgot-password-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('forgot-password-view'))).toBeVisible(); - await tapBack('Login'); + await tapBack(); }); it('should navigate to welcome', async() => { - await tapBack('Welcome'); + await tapBack(); await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('welcome-view'))).toBeVisible(); await element(by.id('welcome-view-login')).tap(); diff --git a/e2e/05-roomslist.spec.js b/e2e/05-roomslist.spec.js index 645e35933f0a1e05c327253cd5238c926617bc20..309ebf87da48bfef212d16822bbbb98fce17cc76 100644 --- a/e2e/05-roomslist.spec.js +++ b/e2e/05-roomslist.spec.js @@ -11,9 +11,9 @@ describe('Rooms list screen', () => { await expect(element(by.id('rooms-list-view'))).toBeVisible(); }); - it('should have rooms list', async() => { - await expect(element(by.id('rooms-list-view-list'))).toBeVisible(); - }); + // it('should have rooms list', async() => { + // await expect(element(by.id('rooms-list-view-list'))).toBeVisible(); + // }); it('should have room item', async() => { await expect(element(by.id('rooms-list-view-item-general'))).toExist(); @@ -38,9 +38,12 @@ describe('Rooms list screen', () => { describe('Usage', async() => { it('should search room and navigate', async() => { - await element(by.id('rooms-list-view-list')).swipe('down'); - await waitFor(element(by.id('rooms-list-view-search'))).toBeVisible().withTimeout(2000); - await expect(element(by.id('rooms-list-view-search'))).toBeVisible(); + // await element(by.id('rooms-list-view-list')).swipe('down'); + // await waitFor(element(by.id('rooms-list-view-search'))).toBeVisible().withTimeout(2000); + // await expect(element(by.id('rooms-list-view-search'))).toBeVisible(); + + await waitFor(element(by.id('rooms-list-view-search'))).toExist().withTimeout(2000); + await element(by.id('rooms-list-view-search')).replaceText('rocket.cat'); await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(60000); await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible(); @@ -49,7 +52,7 @@ describe('Rooms list screen', () => { await expect(element(by.id('room-view'))).toBeVisible(); await waitFor(element(by.text('rocket.cat'))).toBeVisible().withTimeout(60000); await expect(element(by.text('rocket.cat'))).toBeVisible(); - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('rooms-list-view'))).toBeVisible(); await element(by.id('rooms-list-view-search')).replaceText(''); diff --git a/e2e/06-createroom.spec.js b/e2e/06-createroom.spec.js index 19d153548016375f95b9930bfa4548fff4cabd4f..dc056586b40ee64643d56ed30fe452e53c76582a 100644 --- a/e2e/06-createroom.spec.js +++ b/e2e/06-createroom.spec.js @@ -3,37 +3,70 @@ const { } = require('detox'); const { takeScreenshot } = require('./helpers/screenshot'); const data = require('./data'); -const { tapBack } = require('./helpers/app'); +const { tapBack, sleep } = require('./helpers/app'); describe('Create room screen', () => { before(async() => { + await sleep(5000); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await device.reloadReactNative(); await element(by.id('rooms-list-view-create-channel')).tap(); - await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000); }); - describe('Render', async() => { - it('should have select users screen', async() => { - await expect(element(by.id('select-users-view'))).toBeVisible(); - }); + describe('New Message', async() => { + describe('Render', async() => { + it('should have new message screen', async() => { + await expect(element(by.id('new-message-view'))).toBeVisible(); + }); + + it('should have search input', async() => { + await waitFor(element(by.id('new-message-view-search'))).toExist().withTimeout(2000); + await expect(element(by.id('new-message-view-search'))).toExist(); + }); + + after(async() => { + takeScreenshot(); + }); + }) - it('should have search input', async() => { - await expect(element(by.id('select-users-view-search'))).toBeVisible(); - }); + describe('Usage', async() => { + it('should back to rooms list', async() => { + await tapBack(); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('rooms-list-view'))).toBeVisible(); + await element(by.id('rooms-list-view-create-channel')).tap(); + await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000); + }); - after(async() => { - takeScreenshot(); - }); - }); + it('should search user and navigate', async() => { + await element(by.id('new-message-view-search')).replaceText('rocket.cat'); + await waitFor(element(by.id('new-message-view-item-rocket.cat'))).toBeVisible().withTimeout(60000); + await expect(element(by.id('new-message-view-item-rocket.cat'))).toBeVisible(); + await element(by.id('new-message-view-item-rocket.cat')).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(10000); + await expect(element(by.id('room-view'))).toBeVisible(); + await waitFor(element(by.text('rocket.cat'))).toBeVisible().withTimeout(60000); + await expect(element(by.text('rocket.cat'))).toBeVisible(); + await tapBack(2); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await element(by.id('rooms-list-view-create-channel')).tap(); + }); - describe('Usage', async() => { - it('should back to rooms list', async() => { - await tapBack('Messages'); - await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); - await expect(element(by.id('rooms-list-view'))).toBeVisible(); - await element(by.id('rooms-list-view-create-channel')).tap(); - await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); - }); + it('should navigate to select users', async() => { + await element(by.id('new-message-view-create-channel')).tap(); + await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('select-users-view'))).toBeVisible(); + }); + + + after(async() => { + takeScreenshot(); + }); + }) + }); + describe('Select Users', async() => { it('should search users', async() => { await element(by.id('select-users-view-search')).replaceText('rocket.cat'); await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000); @@ -57,52 +90,65 @@ describe('Create room screen', () => { await element(by.id('selected-users-view-submit')).tap(); await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000); await expect(element(by.id('create-channel-view'))).toBeVisible(); - await expect(element(by.id('create-channel-name'))).toBeVisible(); - await expect(element(by.id('create-channel-type'))).toBeVisible(); - await expect(element(by.id('create-channel-readonly'))).toBeVisible(); - await expect(element(by.id('create-channel-broadcast'))).toExist(); - await expect(element(by.id('create-channel-submit'))).toExist(); }); + }) - it('should get invalid room', async() => { - await element(by.id('create-channel-name')).replaceText('general'); - await element(by.id('create-channel-submit')).tap(); - await waitFor(element(by.id('create-channel-error'))).toBeVisible().withTimeout(60000); - await expect(element(by.id('create-channel-error'))).toBeVisible(); - }); - - it('should create public room', async() => { - await element(by.id('create-channel-name')).replaceText(`public${ data.random }`); - await element(by.id('create-channel-type')).tap(); - await element(by.id('create-channel-submit')).tap(); - await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); - await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(`public${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.text(`public${ data.random }`))).toBeVisible(); - await tapBack('Messages'); - await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); - await waitFor(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible(); - }); + describe('Create Channel', async() => { + describe('Render', async() => { + it('should render all fields', async() => { + await expect(element(by.id('create-channel-name'))).toBeVisible(); + await expect(element(by.id('create-channel-type'))).toBeVisible(); + await expect(element(by.id('create-channel-readonly'))).toBeVisible(); + await expect(element(by.id('create-channel-broadcast'))).toExist(); + }) + }) - it('should create private room', async() => { - await element(by.id('rooms-list-view-create-channel')).tap(); - await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); - await element(by.id('select-users-view-item-rocket.cat')).tap(); - await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000); - await element(by.id('selected-users-view-submit')).tap(); - await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000); - await element(by.id('create-channel-name')).replaceText(`private${ data.random }`); - await element(by.id('create-channel-submit')).tap(); - await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); - await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(`private${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.text(`private${ data.random }`))).toBeVisible(); - await tapBack('Messages'); - await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); - await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible(); - }); + describe('Usage', async() => { + it('should get invalid room', async() => { + await element(by.id('create-channel-name')).replaceText('general'); + await element(by.id('create-channel-submit')).tap(); + await waitFor(element(by.text(`A channel with name 'general' exists`))).toBeVisible().withTimeout(60000); + await expect(element(by.text(`A channel with name 'general' exists`))).toBeVisible(); + await element(by.text('OK')).tap(); + }); + + it('should create public room', async() => { + await element(by.id('create-channel-name')).replaceText(`public${ data.random }`); + await element(by.id('create-channel-type')).tap(); + await element(by.id('create-channel-submit')).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); + await expect(element(by.id('room-view'))).toBeVisible(); + await waitFor(element(by.text(`public${ data.random }`))).toBeVisible().withTimeout(60000); + await expect(element(by.text(`public${ data.random }`))).toBeVisible(); + await tapBack(2); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible().withTimeout(60000); + await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible(); + }); + + it('should create private room', async() => { + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await device.reloadReactNative(); + await element(by.id('rooms-list-view-create-channel')).tap(); + await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000); + await element(by.id('new-message-view-create-channel')).tap(); + await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); + await element(by.id('select-users-view-item-rocket.cat')).tap(); + await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000); + await element(by.id('selected-users-view-submit')).tap(); + await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000); + await element(by.id('create-channel-name')).replaceText(`private${ data.random }`); + await element(by.id('create-channel-submit')).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); + await expect(element(by.id('room-view'))).toBeVisible(); + await waitFor(element(by.text(`private${ data.random }`))).toBeVisible().withTimeout(60000); + await expect(element(by.text(`private${ data.random }`))).toBeVisible(); + await tapBack(2); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000); + await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible(); + }); + }) afterEach(async() => { takeScreenshot(); diff --git a/e2e/07-room.spec.js b/e2e/07-room.spec.js index 2100ad0f2c18a2aa8b54c54272277af2d9f2d805..69a726837a8c8a61e390b2e8d41d412c58524d0a 100644 --- a/e2e/07-room.spec.js +++ b/e2e/07-room.spec.js @@ -74,7 +74,7 @@ describe('Room screen', () => { describe('Usage', async() => { describe('Header', async() => { it('should back to rooms list', async() => { - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('rooms-list-view'))).toBeVisible(); await navigateToRoom(); @@ -84,7 +84,7 @@ describe('Room screen', () => { await element(by.id('room-view-header-actions')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('room-actions-view'))).toBeVisible(); - await tapBack(`private${ data.random }`); + await tapBack(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); }); }); @@ -287,7 +287,8 @@ describe('Room screen', () => { }); after(async() => { - await tapBack('Messages'); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('rooms-list-view'))).toBeVisible(); }); diff --git a/e2e/08-roomactions.spec.js b/e2e/08-roomactions.spec.js index cc314a8fb14b1c81e59a84805d76faafab35aee0..a6b939e48578ff53c99ca8ae649a498764551ab0 100644 --- a/e2e/08-roomactions.spec.js +++ b/e2e/08-roomactions.spec.js @@ -21,15 +21,16 @@ async function navigateToRoomActions(type) { await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); } -async function backToActions() { - await tapBack('Actions'); - await waitFor(element(by.id('rooms-actions-view'))).toBeVisible().withTimeout(2000); +async function backToActions(index = 0) { + await tapBack(index); + await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('room-actions-view'))).toBeVisible(); } -async function backToRoomsList(room) { - await tapBack(room); +async function backToRoomsList() { + await tapBack(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000); - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); } @@ -98,7 +99,7 @@ describe('Room actions screen', () => { }); after(async() => { - await backToRoomsList('rocket.cat'); + await backToRoomsList(); }); }); @@ -247,10 +248,10 @@ describe('Room actions screen', () => { await element(by.id('room-actions-search')).tap(); await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000); await expect(element(by.id('search-message-view-input'))).toBeVisible(); - await element(by.id('search-message-view-input')).tap(); await element(by.id('search-message-view-input')).replaceText(`/${ data.random }message/`); await waitFor(element(by.text(`${ data.random }message`).withAncestor(by.id('search-messages-view'))).atIndex(0)).toBeVisible().withTimeout(60000); await expect(element(by.text(`${ data.random }message`).withAncestor(by.id('search-messages-view'))).atIndex(0)).toBeVisible(); + await element(by.traits(['button'])).atIndex(0).tap(); await backToActions(); }); @@ -284,7 +285,7 @@ describe('Room actions screen', () => { await expect(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toBeVisible(); await takeScreenshot(); await element(by.text('OK')).tap(); - await waitFor(element(by.id('rooms-actions-view'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000); }); describe('Add User', async() => { @@ -304,7 +305,7 @@ describe('Room actions screen', () => { await element(by.id('room-members-view-toggle-status')).tap(); await waitFor(element(by.id(`room-members-view-item-${ data.alternateUser }`))).toBeVisible().withTimeout(60000); await expect(element(by.id(`room-members-view-item-${ data.alternateUser }`))).toBeVisible(); - await backToActions(); + await backToActions(1); }); after(async() => { @@ -363,8 +364,8 @@ describe('Room actions screen', () => { await expect(element(by.id('room-view'))).toBeVisible(); await waitFor(element(by.text(data.alternateUser))).toBeVisible().withTimeout(60000); await expect(element(by.text(data.alternateUser))).toBeVisible(); - await tapBack('Messages'); - await waitFor(element(by.id('room-list-view'))).toBeVisible().withTimeout(2000); + await tapBack(2); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); }); afterEach(async() => { diff --git a/e2e/09-roominfo.spec.js b/e2e/09-roominfo.spec.js index b2c424307b41d4052d84b5f50ea3c8d3c0c05e12..0638d012391f7f2e6cb63a34165e94e5b51ec518 100644 --- a/e2e/09-roominfo.spec.js +++ b/e2e/09-roominfo.spec.js @@ -158,7 +158,7 @@ describe('Room info screen', () => { await expect(element(by.text('Settings succesfully changed!'))).toBeVisible(); await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000); await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible(); - await tapBack('Room Info'); + await tapBack(); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view-name'))).toHaveText(`${ room }new`).withTimeout(60000); await expect(element(by.id('room-info-view-name'))).toHaveText(`${ room }new`); @@ -206,7 +206,7 @@ describe('Room info screen', () => { await expect(element(by.text('Settings succesfully changed!'))).toBeVisible(); await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000); await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible(); - await tapBack('Room Info'); + await tapBack(); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view-description'))).toHaveText('new description').withTimeout(60000); await expect(element(by.id('room-info-view-description'))).toHaveText('new description'); @@ -223,7 +223,7 @@ describe('Room info screen', () => { await expect(element(by.text('Settings succesfully changed!'))).toBeVisible(); await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000); await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible(); - await tapBack('Room Info'); + await tapBack(); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view-topic'))).toHaveText('new topic').withTimeout(60000); await expect(element(by.id('room-info-view-topic'))).toHaveText('new topic'); @@ -240,7 +240,7 @@ describe('Room info screen', () => { await expect(element(by.text('Settings succesfully changed!'))).toBeVisible(); await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000); await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible(); - await tapBack('Room Info'); + await tapBack(); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view-announcement'))).toHaveText('new announcement').withTimeout(60000); await expect(element(by.id('room-info-view-announcement'))).toHaveText('new announcement'); diff --git a/e2e/11-broadcast.spec.js b/e2e/11-broadcast.spec.js index d959e539c0bbda067b78fdabbec5ad4720b51b26..582cb8935bc4310dbfa68341fddb76ee27036535 100644 --- a/e2e/11-broadcast.spec.js +++ b/e2e/11-broadcast.spec.js @@ -12,6 +12,8 @@ describe('Broadcast room', () => { it('should create broadcast room', async() => { await element(by.id('rooms-list-view-create-channel')).tap(); + await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000); + await element(by.id('new-message-view-create-channel')).tap(); await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); await element(by.id(`select-users-view-item-${ data.alternateUser }`)).tap(); await waitFor(element(by.id(`selected-user-${ data.alternateUser }`))).toBeVisible().withTimeout(5000); @@ -30,14 +32,14 @@ describe('Broadcast room', () => { await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view-broadcast'))).toBeVisible().withTimeout(2000); await expect(element(by.id('room-info-view-broadcast'))).toBeVisible(); - await tapBack('Actions'); + await tapBack(1); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000); - await tapBack(`broadcast${ data.random }`); + await tapBack(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000); - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); - await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible(); + await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000); + await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist(); }); it('should send message', async() => { @@ -51,7 +53,7 @@ describe('Broadcast room', () => { }); it('should login as user without write message authorization and enter room', async() => { - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await expect(element(by.id('rooms-list-view'))).toBeVisible(); await logout(); @@ -61,7 +63,8 @@ describe('Broadcast room', () => { await element(by.id('login-view-submit')).tap(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); // await device.reloadReactNative(); // remove after fix logout - await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + // await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-search')).replaceText(`broadcast${ data.random }`); await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible().withTimeout(60000); await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible(); await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap(); @@ -107,7 +110,7 @@ describe('Broadcast room', () => { after(async() => { // log back as main test user and left screen on RoomsListView - await tapBack('Messages'); + await tapBack(2); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await logout(); await navigateToLogin(); diff --git a/e2e/helpers/app.js b/e2e/helpers/app.js index 873dd0be11dd58e7c1cddd7da237558c4a8eb6a4..1da062cab9c4a6bd38948b04b4fefcff9a787ae4 100644 --- a/e2e/helpers/app.js +++ b/e2e/helpers/app.js @@ -36,12 +36,8 @@ async function logout() { await expect(element(by.id('onboarding-view'))).toBeVisible(); } -async function tapBack(label) { - try { - return element(by.traits(['button']).and(by.label(label || 'Back'))).atIndex(0).tap(); - } catch (err) { - return element(by.type('_UIModernBarButton').and(by.label(label || 'Back'))).tap(); - } +async function tapBack(index) { + await element(by.type('_UIModernBarButton')).atIndex(index || 0).tap(); } async function sleep(ms) { diff --git a/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/Contents.json b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..d984b4ae4a2f7688e2f83203fff035013d0b1e89 --- /dev/null +++ b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "plus.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "plus@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "plus@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus.png b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..cce622415373cec9bcd8d00b0abb6808f50dc088 Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus.png differ diff --git a/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@2x.png b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..251c8d181cc70c57bf8c2068c0e8fe695bca9870 Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@2x.png differ diff --git a/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@3x.png b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..71db08c7acda5cadd0b6c5da25b931113c969f01 Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/plus.imageset/plus@3x.png differ diff --git a/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/Contents.json b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..18d7cebfd752bcbb3c156a036f73c5a833021cff --- /dev/null +++ b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "textinput_search.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "textinput_search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "textinput_search@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search.png b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search.png new file mode 100644 index 0000000000000000000000000000000000000000..4eefb38cdf08f3356af850ae8bdf538b1b147dab Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search.png differ diff --git a/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@2x.png b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a8d16731036c8544af8aa30e3f2f98458f8e6e8b Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@2x.png differ diff --git a/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@3x.png b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9db2d094d48f7ad826836ebd8b5a78421251af Binary files /dev/null and b/ios/RocketChatRN/Images.xcassets/Icons/textinput_search.imageset/textinput_search@3x.png differ diff --git a/package-lock.json b/package-lock.json index 7158b588cdecabba07a693410d745dbe05588ef2..398ad21e746d33162bced3aefa6b8b7e03b5a5b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15999,7 +15999,7 @@ } }, "react-native-navigation": { - "version": "git+https://github.com/RocketChat/react-native-navigation.git#1a428f14ddda77511676d0c6d863083ce6225e32", + "version": "git+https://github.com/RocketChat/react-native-navigation.git#024095e7679afa0b4e9475f4ffce45d9e50ca5ad", "from": "git+https://github.com/RocketChat/react-native-navigation.git", "requires": { "lodash": "4.x.x"