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 00000000..238a8cc9 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 00000000..274376fd 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 00000000..cce62241 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 00000000..4eefb38c 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 00000000..251c8d18 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 00000000..a8d16731 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 00000000..71db08c7 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 00000000..1c9db2d0 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 00000000..320adb8b 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 00000000..363ff5db 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 49bd9cea..5b8f16c9 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 51d1baff..a6bab715 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 47c587eb..5a603742 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 00000000..5135ba19 --- /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 }) => ( + + + + + + +); + +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 0cbf8aa3..382939ae 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 44261479..8af6fa33 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 ed2b0289..711d5d99 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 cf8c07de..f4e03dbd 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 }) => ( - + {name} @{username} + {icon ? : null} ); @@ -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 db6d5911..a423033e 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 9f64e3b4..bd5ff2d9 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 ce21d94b..8f3bf1d6 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 12f1b01e..93c1ce86 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 f959fd18..556b66a7 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 9a1e4f24..be132c44 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 9f9a62ca..5ce8e870 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 ( - - {this.props.createChannel.error.reason} - - ); + this.props.removeUser(user); } renderSwitch = ({ - id, value, label, description, onValueChange, disabled = false + id, value, label, onValueChange, disabled = false }) => ( - - - - {label} - - {description} + + {I18n.t(label)} + - ); + ) 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 = () => + + renderFormSeparator = () => + + renderItem = ({ item }) => ( + this.removeUser(item)} + testID={`create-channel-view-item-${ item.name }`} + /> + ) + + renderInvitedList = () => ( + item._id} + style={[styles.list, sharedStyles.separatorVertical]} + renderItem={this.renderItem} + ItemSeparatorComponent={this.renderSeparator} + enableEmptySections + keyboardShouldPersistTaps='always' + /> + ) + render() { + const userCount = this.props.users.length; return ( - - - 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()} - -