diff --git a/app/constants/settings.js b/app/constants/settings.js index 8e8e9b8c8ef55130f314a9b3780149bdefae848d..f22949ea0179076338b1086de6d534943f9d4cc5 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -14,6 +14,9 @@ export default { CROWD_Enable: { type: 'valueAsBoolean' }, + FEDERATION_Enabled: { + type: 'valueAsBoolean' + }, LDAP_Enable: { type: 'valueAsBoolean' }, diff --git a/app/containers/Check.js b/app/containers/Check.js new file mode 100644 index 0000000000000000000000000000000000000000..30c9cbd31cca02ae5ee26d4fbb083060fe130c41 --- /dev/null +++ b/app/containers/Check.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; + +import { CustomIcon } from '../lib/Icons'; +import sharedStyles from '../views/Styles'; + +const styles = StyleSheet.create({ + icon: { + width: 22, + height: 22, + marginHorizontal: 15, + ...sharedStyles.textColorDescription + } +}); + +const Check = React.memo(() => <CustomIcon style={styles.icon} size={22} name='check' />); + +export default Check; diff --git a/app/containers/SearchBox.js b/app/containers/SearchBox.js index 65a41d3ee323a3dd1083173e5ea2dbdcb9a197a9..87a3cd8e3a9f8c2dc586aa09063b6893618ee1d5 100644 --- a/app/containers/SearchBox.js +++ b/app/containers/SearchBox.js @@ -34,7 +34,7 @@ const styles = StyleSheet.create({ } }); -const SearchBox = ({ onChangeText, testID }) => ( +const SearchBox = ({ onChangeText, onSubmitEditing, testID }) => ( <View style={styles.container}> <View style={styles.searchBox}> <CustomIcon name='magnifier' size={14} color='#8E8E93' /> @@ -49,6 +49,7 @@ const SearchBox = ({ onChangeText, testID }) => ( testID={testID} underlineColorAndroid='transparent' onChangeText={onChangeText} + onSubmitEditing={onSubmitEditing} /> </View> </View> @@ -56,6 +57,7 @@ const SearchBox = ({ onChangeText, testID }) => ( SearchBox.propTypes = { onChangeText: PropTypes.func.isRequired, + onSubmitEditing: PropTypes.func, testID: PropTypes.string }; diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 62b4f117a7a829c6e31176f8577b15de551e6248..a426f0ddb4ee6763c68a0a902aeea2931362a62b 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -142,9 +142,10 @@ export default { DELETE: 'DELETE', description: 'description', Description: 'Description', + Directory: 'Directory', + Direct_Messages: 'Direct Messages', Disable_notifications: 'Disable notifications', Discussions: 'Discussions', - Direct_Messages: 'Direct Messages', Dont_Have_An_Account: 'Don\'t have an account?', Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?', edit: 'edit', @@ -294,6 +295,9 @@ export default { saving_settings: 'saving settings', Search_Messages: 'Search Messages', Search: 'Search', + Search_by: 'Search by', + Search_global_users: 'Search for global users', + Search_global_users_description: 'If you turn-on, you can search for any user from others companies or servers.', Select_Avatar: 'Select Avatar', Select_Users: 'Select Users', Send: 'Send', @@ -348,6 +352,7 @@ export default { Updating: 'Updating...', Uploading: 'Uploading', Upload_file_question_mark: 'Upload file?', + Users: 'Users', User_added_by: 'User {{userAdded}} added by {{userBy}}', User_has_been_key: 'User has been {{key}}!', User_is_no_longer_role_by_: '{{user}} is no longer {{role}} by {{userBy}}', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 859b4f2990a79a3ffbe6ee8bfb176bb9e5cef966..ef13d05cfec0803f080c77af1b32177e0ee927c9 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -146,11 +146,12 @@ export default { delete: 'excluir', Delete: 'Excluir', DELETE: 'EXCLUIR', + Direct_Messages: 'Mensagens Diretas', + Directory: 'Diretório', description: 'descrição', Description: 'Descrição', Disable_notifications: 'Desabilitar notificações', Discussions: 'Discussões', - Direct_Messages: 'Mensagens Diretas', Dont_Have_An_Account: 'Não tem uma conta?', Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?', edit: 'editar', @@ -293,6 +294,9 @@ export default { saving_settings: 'salvando configurações', Search_Messages: 'Buscar Mensagens', Search: 'Buscar', + Search_by: 'Buscar por', + Search_global_users: 'Busca por usuários globais', + Search_global_users_description: 'Caso ativado, busca por usuários de outras empresas ou servidores.', Select_Avatar: 'Selecionar Avatar', Select_Users: 'Selecionar Usuários', Send: 'Enviar', @@ -344,6 +348,7 @@ export default { Updating: 'Atualizando...', Uploading: 'Subindo arquivo', Upload_file_question_mark: 'Enviar arquivo?', + Users: 'Usuários', User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}', User_has_been_key: 'Usuário foi {{key}}!', User_is_no_longer_role_by_: '{{user}} não pertence mais à {{role}} por {{userBy}}', diff --git a/app/index.js b/app/index.js index 9ee8cc9f853a8b9ac12ff9076c0e4607cfb934f3..33c4915614006ed98af3edc37cc9ecf389902557 100644 --- a/app/index.js +++ b/app/index.js @@ -16,6 +16,7 @@ import AuthLoadingView from './views/AuthLoadingView'; import RoomsListView from './views/RoomsListView'; import RoomView from './views/RoomView'; import NewMessageView from './views/NewMessageView'; +import DirectoryView from './views/DirectoryView'; import LoginView from './views/LoginView'; import Navigation from './lib/Navigation'; import Sidebar from './views/SidebarView'; @@ -110,7 +111,8 @@ const ChatsStack = createStackNavigator({ SearchMessagesView, SelectedUsersView, ThreadMessagesView, - MessagesView + MessagesView, + DirectoryView }, { defaultNavigationOptions: defaultHeader }); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 9b7e16e2cb7003d12259695d6ec87aa616522b1d..3c7f622e5692ced6048954db7fea0b7b50ff7d6a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -5,7 +5,7 @@ import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import reduxStore from './createStore'; import defaultSettings from '../constants/settings'; import messagesStatus from '../constants/messagesStatus'; -import database, { safeAddListener } from './realm'; +import database from './realm'; import log from '../utils/log'; import { isIOS, getBundleId } from '../utils/deviceInfo'; import EventEmitter from '../utils/events'; @@ -57,23 +57,6 @@ const RocketChat = { // RC 0.51.0 return this.sdk.methodCall(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast }); }, - async createDirectMessageAndWait(username) { - const room = await RocketChat.createDirectMessage(username); - return new Promise((resolve) => { - const data = database.objects('subscriptions') - .filtered('rid = $1', room.rid); - - if (data.length) { - return resolve(data[0]); - } - safeAddListener(data, () => { - if (!data.length) { return; } - data.removeAllListeners(); - resolve(data[0]); - }); - }); - }, - async getUserToken() { try { return await AsyncStorage.getItem(TOKEN_KEY); @@ -849,6 +832,14 @@ const RocketChat = { this.sdk.subscribe('stream-notify-logged', 'user-status'); } } + }, + getDirectory({ + query, count, offset, sort + }) { + // RC 1.0 + return this.sdk.get('directory', { + query, count, offset, sort + }); } }; diff --git a/app/views/DirectoryView/DirectoryItem.js b/app/views/DirectoryView/DirectoryItem.js new file mode 100644 index 0000000000000000000000000000000000000000..620f5dae053e73bcda435b30bb276d4568ae5b45 --- /dev/null +++ b/app/views/DirectoryView/DirectoryItem.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import PropTypes from 'prop-types'; + +import Avatar from '../../containers/Avatar'; +import Touch from '../../utils/touch'; +import RoomTypeIcon from '../../containers/RoomTypeIcon'; +import styles from './styles'; + +const DirectoryItemLabel = React.memo(({ text }) => { + if (!text) { + return null; + } + return <Text style={styles.directoryItemLabel}>{text}</Text>; +}); + +const DirectoryItem = ({ + title, description, avatar, onPress, testID, style, baseUrl, user, rightLabel, type +}) => ( + <Touch onPress={onPress} style={styles.directoryItemButton} testID={testID}> + <View style={[styles.directoryItemContainer, style]}> + <Avatar + text={avatar} + size={30} + type={type} + style={styles.directoryItemAvatar} + baseUrl={baseUrl} + userId={user.id} + token={user.token} + /> + <View style={styles.directoryItemTextContainer}> + <View style={styles.directoryItemTextTitle}> + <RoomTypeIcon type='c' /> + <Text style={styles.directoryItemName} numberOfLines={1}>{title}</Text> + </View> + <Text style={styles.directoryItemUsername} numberOfLines={1}>{description}</Text> + </View> + <DirectoryItemLabel text={rightLabel} /> + </View> + </Touch> +); + +DirectoryItem.propTypes = { + title: PropTypes.string.isRequired, + description: PropTypes.string, + avatar: PropTypes.string, + type: PropTypes.string, + user: PropTypes.shape({ + id: PropTypes.string, + token: PropTypes.string + }), + baseUrl: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired, + testID: PropTypes.string.isRequired, + style: PropTypes.any, + rightLabel: PropTypes.string +}; + +DirectoryItemLabel.propTypes = { + text: PropTypes.string +}; + +export default DirectoryItem; diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js new file mode 100644 index 0000000000000000000000000000000000000000..841484152e7300afc2f3780a59938668574bb51c --- /dev/null +++ b/app/views/DirectoryView/Options.js @@ -0,0 +1,121 @@ +import React, { PureComponent } from 'react'; +import { + View, Text, Animated, Easing, TouchableWithoutFeedback, Switch +} from 'react-native'; +import PropTypes from 'prop-types'; + +import Touch from '../../utils/touch'; +import styles from './styles'; +import { CustomIcon } from '../../lib/Icons'; +import Check from '../../containers/Check'; +import I18n from '../../i18n'; + +const ANIMATION_DURATION = 200; +const ANIMATION_PROPS = { + duration: ANIMATION_DURATION, + easing: Easing.inOut(Easing.quad), + useNativeDriver: true +}; + +export default class DirectoryOptions extends PureComponent { + static propTypes = { + type: PropTypes.string, + globalUsers: PropTypes.bool, + isFederationEnabled: PropTypes.bool, + close: PropTypes.func, + changeType: PropTypes.func, + toggleWorkspace: PropTypes.func + } + + constructor(props) { + super(props); + this.animatedValue = new Animated.Value(0); + } + + componentDidMount() { + Animated.timing( + this.animatedValue, + { + toValue: 1, + ...ANIMATION_PROPS + }, + ).start(); + } + + close = () => { + const { close } = this.props; + Animated.timing( + this.animatedValue, + { + toValue: 0, + ...ANIMATION_PROPS + }, + ).start(() => close()); + } + + renderItem = (itemType) => { + const { changeType, type: propType } = this.props; + let text = 'Users'; + let icon = 'user'; + if (itemType === 'channels') { + text = 'Channels'; + icon = 'hashtag'; + } + + return ( + <Touch style={styles.dropdownItemButton} onPress={() => changeType(itemType)}> + <View style={styles.dropdownItemContainer}> + <CustomIcon style={styles.dropdownItemIcon} size={22} name={icon} /> + <Text style={styles.dropdownItemText}>{I18n.t(text)}</Text> + {propType === itemType ? <Check /> : null} + </View> + </Touch> + ); + } + + render() { + const translateY = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [-326, 0] + }); + const backdropOpacity = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.3] + }); + const { globalUsers, toggleWorkspace, isFederationEnabled } = this.props; + return ( + <React.Fragment> + <TouchableWithoutFeedback onPress={this.close}> + <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} /> + </TouchableWithoutFeedback> + <Animated.View style={[styles.dropdownContainer, { transform: [{ translateY }] }]}> + <Touch + onPress={this.close} + style={styles.dropdownContainerHeader} + > + <View style={styles.dropdownItemContainer}> + <Text style={styles.dropdownToggleText}>{I18n.t('Search_by')}</Text> + <CustomIcon style={[styles.dropdownItemIcon, styles.inverted]} size={22} name='arrow-down' /> + </View> + </Touch> + {this.renderItem('channels')} + {this.renderItem('users')} + {isFederationEnabled + ? ( + <React.Fragment> + <View style={styles.dropdownSeparator} /> + <View style={[styles.dropdownItemContainer, styles.globalUsersContainer]}> + <View style={styles.globalUsersTextContainer}> + <Text style={styles.dropdownItemText}>{I18n.t('Search_global_users')}</Text> + <Text style={styles.dropdownItemDescription}>{I18n.t('Search_global_users_description')}</Text> + </View> + <Switch value={globalUsers} onValueChange={toggleWorkspace} /> + </View> + </React.Fragment> + ) + : null} + </Animated.View> + </React.Fragment> + ); + } +} diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js new file mode 100644 index 0000000000000000000000000000000000000000..60a12932eaed3e7344995a11d46a75d9aa2fddd7 --- /dev/null +++ b/app/views/DirectoryView/index.js @@ -0,0 +1,248 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + View, FlatList, Text +} from 'react-native'; +import { connect } from 'react-redux'; +import { SafeAreaView } from 'react-navigation'; + +import RocketChat from '../../lib/rocketchat'; +import DirectoryItem from './DirectoryItem'; +import sharedStyles from '../Styles'; +import I18n from '../../i18n'; +import Touch from '../../utils/touch'; +import SearchBox from '../../containers/SearchBox'; +import { CustomIcon } from '../../lib/Icons'; +import StatusBar from '../../containers/StatusBar'; +import RCActivityIndicator from '../../containers/ActivityIndicator'; +import debounce from '../../utils/debounce'; +import log from '../../utils/log'; +import Options from './Options'; +import styles from './styles'; + +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', + user: { + id: state.login.user && state.login.user.id, + token: state.login.user && state.login.user.token + }, + isFederationEnabled: state.settings.FEDERATION_Enabled +})) +export default class DirectoryView extends React.Component { + static navigationOptions = () => ({ + title: I18n.t('Directory') + }) + + static propTypes = { + navigation: PropTypes.object, + baseUrl: PropTypes.string, + isFederationEnabled: PropTypes.bool, + user: PropTypes.shape({ + id: PropTypes.string, + token: PropTypes.string + }) + }; + + constructor(props) { + super(props); + this.state = { + data: [], + loading: false, + text: '', + total: -1, + showOptionsDropdown: false, + globalUsers: true, + type: 'channels' + }; + } + + componentDidMount() { + this.load({}); + } + + onSearchChangeText = (text) => { + this.setState({ text }); + } + + onPressItem = (item) => { + const { navigation } = this.props; + try { + const onPressItem = navigation.getParam('onPressItem', () => {}); + onPressItem(item); + } catch (error) { + console.log('DirectoryView -> onPressItem -> error', error); + } + } + + // eslint-disable-next-line react/sort-comp + load = debounce(async({ newSearch = false }) => { + if (newSearch) { + this.setState({ data: [], total: -1, loading: false }); + } + + const { + loading, text, total, data: { length } + } = this.state; + if (loading || length === total) { + return; + } + + this.setState({ loading: true }); + + try { + const { data, type, globalUsers } = this.state; + const query = { text, type, workspace: globalUsers ? 'all' : 'local' }; + const directories = await RocketChat.getDirectory({ + query, + offset: data.length, + count: 50, + sort: (type === 'users') ? { username: 1 } : { usersCount: -1 } + }); + if (directories.success) { + this.setState({ + data: [...data, ...directories.result], + loading: false, + total: directories.total + }); + } else { + this.setState({ loading: false }); + } + } catch (error) { + log('err_load_directory', error); + this.setState({ loading: false }); + } + }, 200) + + search = () => { + this.load({ newSearch: true }); + } + + changeType = (type) => { + this.setState({ type, data: [] }, () => this.search()); + } + + toggleWorkspace = () => { + this.setState(({ globalUsers }) => ({ globalUsers: !globalUsers, data: [] }), () => this.search()); + } + + toggleDropdown = () => { + this.setState(({ showOptionsDropdown }) => ({ showOptionsDropdown: !showOptionsDropdown })); + } + + goRoom = async({ rid, name, t }) => { + const { navigation } = this.props; + await navigation.navigate('RoomsListView'); + navigation.navigate('RoomView', { rid, name, t }); + } + + onPressItem = async(item) => { + const { type } = this.state; + if (type === 'users') { + const result = await RocketChat.createDirectMessage(item.username); + if (result.success) { + this.goRoom({ rid: result.room._id, name: item.username, t: 'd' }); + } + } else { + this.goRoom({ rid: item._id, name: item.name, t: 'c' }); + } + } + + renderHeader = () => { + const { type } = this.state; + return ( + <React.Fragment> + <SearchBox + onChangeText={this.onSearchChangeText} + onSubmitEditing={this.search} + testID='federation-view-search' + /> + <Touch onPress={this.toggleDropdown} testID='federation-view-create-channel'> + <View style={[sharedStyles.separatorVertical, styles.toggleDropdownContainer]}> + <CustomIcon style={styles.toggleDropdownIcon} size={20} name={type === 'users' ? 'user' : 'hashtag'} /> + <Text style={styles.toggleDropdownText}>{type === 'users' ? I18n.t('Users') : I18n.t('Channels')}</Text> + <CustomIcon name='arrow-down' size={20} style={styles.toggleDropdownArrow} /> + </View> + </Touch> + </React.Fragment> + ); + } + + renderSeparator = () => <View style={[sharedStyles.separator, styles.separator]} />; + + renderItem = ({ item, index }) => { + const { data, type } = this.state; + const { baseUrl, user } = this.props; + + let style; + if (index === data.length - 1) { + style = sharedStyles.separatorBottom; + } + + const commonProps = { + title: item.name, + onPress: () => this.onPressItem(item), + baseUrl, + testID: `federation-view-item-${ item.name }`, + style, + user + }; + + if (type === 'users') { + return ( + <DirectoryItem + avatar={item.username} + description={item.username} + rightLabel={item.federation && item.federation.peer} + type='d' + {...commonProps} + /> + ); + } + return ( + <DirectoryItem + avatar={item.name} + description={item.topic} + rightLabel={I18n.t('N_users', { n: item.usersCount })} + type='c' + {...commonProps} + /> + ); + } + + render = () => { + const { + data, loading, showOptionsDropdown, type, globalUsers + } = this.state; + const { isFederationEnabled } = this.props; + return ( + <SafeAreaView style={styles.safeAreaView} testID='directory-view' forceInset={{ bottom: 'never' }}> + <StatusBar /> + <FlatList + data={data} + style={styles.list} + contentContainerStyle={styles.listContainer} + extraData={this.state} + keyExtractor={item => item._id} + ListHeaderComponent={this.renderHeader} + renderItem={this.renderItem} + ItemSeparatorComponent={this.renderSeparator} + keyboardShouldPersistTaps='always' + ListFooterComponent={loading ? <RCActivityIndicator /> : null} + onEndReached={() => this.load({})} + /> + {showOptionsDropdown + ? ( + <Options + type={type} + globalUsers={globalUsers} + close={this.toggleDropdown} + changeType={this.changeType} + toggleWorkspace={this.toggleWorkspace} + isFederationEnabled={isFederationEnabled} + /> + ) + : null} + </SafeAreaView> + ); + } +} diff --git a/app/views/DirectoryView/styles.js b/app/views/DirectoryView/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..59e60da2b0f82fe8e4ee6e4b7a76af4a1e3b36a8 --- /dev/null +++ b/app/views/DirectoryView/styles.js @@ -0,0 +1,151 @@ +import { StyleSheet } from 'react-native'; + +import { COLOR_WHITE, COLOR_SEPARATOR, COLOR_PRIMARY } from '../../constants/colors'; +import { isIOS } from '../../utils/deviceInfo'; +import sharedStyles from '../Styles'; + +export default StyleSheet.create({ + safeAreaView: { + flex: 1, + backgroundColor: isIOS ? '#F7F8FA' : '#E1E5E8' + }, + list: { + flex: 1 + }, + listContainer: { + paddingBottom: 30 + }, + separator: { + marginLeft: 60 + }, + toggleDropdownContainer: { + height: 47, + backgroundColor: COLOR_WHITE, + flexDirection: 'row', + alignItems: 'center' + }, + toggleDropdownIcon: { + color: COLOR_PRIMARY, + marginLeft: 20, + marginRight: 17 + }, + toggleDropdownText: { + flex: 1, + color: COLOR_PRIMARY, + fontSize: 17, + ...sharedStyles.textRegular + }, + toggleDropdownArrow: { + ...sharedStyles.textColorDescription, + marginRight: 15 + }, + dropdownContainer: { + backgroundColor: COLOR_WHITE, + width: '100%', + position: 'absolute', + top: 0 + }, + backdrop: { + ...StyleSheet.absoluteFill, + backgroundColor: '#000000' + }, + dropdownContainerHeader: { + height: 47, + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: COLOR_SEPARATOR, + alignItems: 'center', + backgroundColor: isIOS ? COLOR_WHITE : '#54585E', + flexDirection: 'row' + }, + dropdownItemButton: { + height: 57, + justifyContent: 'center' + }, + dropdownItemContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center' + }, + dropdownItemText: { + fontSize: 18, + flex: 1, + ...sharedStyles.textColorNormal, + ...sharedStyles.textRegular + }, + dropdownItemDescription: { + fontSize: 14, + flex: 1, + marginTop: 2, + ...sharedStyles.textColorDescription, + ...sharedStyles.textRegular + }, + dropdownToggleText: { + fontSize: 15, + flex: 1, + marginLeft: 15, + ...sharedStyles.textColorDescription, + ...sharedStyles.textRegular + }, + dropdownItemIcon: { + width: 22, + height: 22, + marginHorizontal: 15, + ...sharedStyles.textColorDescription + }, + dropdownSeparator: { + height: StyleSheet.hairlineWidth, + backgroundColor: COLOR_SEPARATOR, + marginHorizontal: 15, + flex: 1 + }, + directoryItemButton: { + height: 54, + backgroundColor: COLOR_WHITE + }, + directoryItemContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 15 + }, + directoryItemAvatar: { + marginRight: 12 + }, + directoryItemTextTitle: { + flexDirection: 'row', + alignItems: 'center' + }, + directoryItemTextContainer: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center' + }, + directoryItemName: { + flex: 1, + fontSize: 17, + ...sharedStyles.textMedium, + ...sharedStyles.textColorNormal + }, + directoryItemUsername: { + fontSize: 14, + ...sharedStyles.textRegular, + ...sharedStyles.textColorDescription + }, + directoryItemLabel: { + fontSize: 14, + paddingLeft: 10, + ...sharedStyles.textRegular, + ...sharedStyles.textColorDescription + }, + inverted: { + transform: [{ scaleY: -1 }] + }, + globalUsersContainer: { + padding: 15 + }, + globalUsersTextContainer: { + flex: 1, + flexDirection: 'column' + } +}); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index c2f495ea03863c0c121839396960c42f0dbf0c17..cfadd6094e128e44bf160e5aee76177854cd234d 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -40,7 +40,8 @@ const styles = StyleSheet.create({ }, createChannelIcon: { color: COLOR_PRIMARY, - marginHorizontal: 18 + marginLeft: 18, + marginRight: 15 }, createChannelText: { color: COLOR_PRIMARY, diff --git a/app/views/RoomsListView/Check.js b/app/views/RoomsListView/Check.js deleted file mode 100644 index 42685ba0a114b4ed85e4cf71368f8935fca50811..0000000000000000000000000000000000000000 --- a/app/views/RoomsListView/Check.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -import { CustomIcon } from '../../lib/Icons'; -import styles from './styles'; - -const Check = React.memo(() => <CustomIcon style={styles.sortIcon} size={22} name='check' />); - -export default Check; diff --git a/app/views/RoomsListView/ListHeader/Directory.js b/app/views/RoomsListView/ListHeader/Directory.js new file mode 100644 index 0000000000000000000000000000000000000000..0e83ec1753305f4bd48aa6523609812959f8d97e --- /dev/null +++ b/app/views/RoomsListView/ListHeader/Directory.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import PropTypes from 'prop-types'; + +import { CustomIcon } from '../../../lib/Icons'; +import I18n from '../../../i18n'; +import Touch from '../../../utils/touch'; +import styles from '../styles'; +import DisclosureIndicator from '../../../containers/DisclosureIndicator'; + + +const Directory = React.memo(({ goDirectory }) => ( + <Touch + key='rooms-list-view-sort' + onPress={goDirectory} + style={styles.dropdownContainerHeader} + > + <View style={styles.sortItemContainer}> + <CustomIcon style={styles.directoryIcon} size={22} name='discover' /> + <Text style={styles.directoryText}>{I18n.t('Directory')}</Text> + <DisclosureIndicator /> + </View> + </Touch> +)); + +Directory.propTypes = { + goDirectory: PropTypes.func +}; + +export default Directory; diff --git a/app/views/RoomsListView/ListHeader/index.js b/app/views/RoomsListView/ListHeader/index.js index 92743b39d981e96885693f354eaf8983dd6471b9..fd35f0b571e6cbbebe3043bcf27179163d34db0c 100644 --- a/app/views/RoomsListView/ListHeader/index.js +++ b/app/views/RoomsListView/ListHeader/index.js @@ -2,13 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import SearchBar from './SearchBar'; +import Directory from './Directory'; import Sort from './Sort'; const ListHeader = React.memo(({ - searchLength, sortBy, onChangeSearchText, toggleSort + searchLength, sortBy, onChangeSearchText, toggleSort, goDirectory }) => ( <React.Fragment> <SearchBar onChangeSearchText={onChangeSearchText} /> + <Directory goDirectory={goDirectory} /> <Sort searchLength={searchLength} sortBy={sortBy} toggleSort={toggleSort} /> </React.Fragment> )); @@ -17,7 +19,8 @@ ListHeader.propTypes = { searchLength: PropTypes.number, sortBy: PropTypes.string, onChangeSearchText: PropTypes.func, - toggleSort: PropTypes.func + toggleSort: PropTypes.func, + goDirectory: PropTypes.func }; export default ListHeader; diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index 7953446099fc3a295977cb7d0f100997ad563a38..4bc8db9d11b23315388c0e9d45772952df01f026 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -16,7 +16,7 @@ import Touch from '../../utils/touch'; import RocketChat from '../../lib/rocketchat'; import I18n from '../../i18n'; import EventEmitter from '../../utils/events'; -import Check from './Check'; +import Check from '../../containers/Check'; const ROW_HEIGHT = 68; const ANIMATION_DURATION = 200; diff --git a/app/views/RoomsListView/SortDropdown.js b/app/views/RoomsListView/SortDropdown.js index ea7efaefebe5f14ec341fe7465a46babc2f00ada..163b4d1e732212a57b151727b218a593a8fe2cb5 100644 --- a/app/views/RoomsListView/SortDropdown.js +++ b/app/views/RoomsListView/SortDropdown.js @@ -12,7 +12,7 @@ import { setPreference } from '../../actions/sortPreferences'; import log from '../../utils/log'; import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; -import Check from './Check'; +import Check from '../../containers/Check'; const ANIMATION_DURATION = 200; @@ -106,7 +106,7 @@ export default class Sort extends PureComponent { render() { const translateY = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [-245, 41] + outputRange: [-326, 0] }); const backdropOpacity = this.animatedValue.interpolate({ inputRange: [0, 1], @@ -117,14 +117,24 @@ export default class Sort extends PureComponent { } = this.props; return ( - [ + <React.Fragment> <TouchableWithoutFeedback key='sort-backdrop' onPress={this.close}> <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]} /> - </TouchableWithoutFeedback>, + </TouchableWithoutFeedback> <Animated.View key='sort-container' style={[styles.dropdownContainer, { transform: [{ translateY }] }]} > + <Touch + key='sort-toggle' + onPress={this.close} + style={styles.dropdownContainerHeader} + > + <View style={styles.sortItemContainer}> + <Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> + <CustomIcon style={styles.sortIcon} size={22} name='sort1' /> + </View> + </Touch> <Touch key='sort-alphabetical' style={styles.sortItemButton} onPress={this.sortByName}> <View style={styles.sortItemContainer}> <CustomIcon style={styles.sortIcon} size={22} name='sort' /> @@ -161,18 +171,8 @@ export default class Sort extends PureComponent { {showUnread ? <Check /> : null} </View> </Touch> - </Animated.View>, - <Touch - key='sort-toggle' - onPress={this.close} - style={[styles.dropdownContainerHeader, styles.sortToggleContainerClose]} - > - <View style={styles.sortItemContainer}> - <Text style={styles.sortToggleText}>{I18n.t('Sorting_by', { key: I18n.t(sortBy === 'alphabetical' ? 'name' : 'activity') })}</Text> - <CustomIcon style={styles.sortIcon} size={22} name='sort1' /> - </View> - </Touch> - ] + </Animated.View> + </React.Fragment> ); } } diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index d3574c33996b67e726e3542be8ea61536dc8b706..f29d62348f15b3b8290e0791d12829fa73230b37 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -379,6 +379,11 @@ export default class RoomsListView extends React.Component { }, 100); } + goDirectory = () => { + const { navigation } = this.props; + navigation.navigate('DirectoryView'); + } + getScrollRef = ref => this.scroll = ref renderListHeader = () => { @@ -390,6 +395,7 @@ export default class RoomsListView extends React.Component { sortBy={sortBy} onChangeSearchText={this.search} toggleSort={this.toggleSort} + goDirectory={this.goDirectory} /> ); } diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 95c1116647af0ea63dd53a2f619c23b4dd3c4601..0c19c11ee48fcdba422cbbfade2d94b7bcaf56c7 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -1,7 +1,7 @@ import { StyleSheet } from 'react-native'; import { isIOS } from '../../utils/deviceInfo'; import { - COLOR_SEPARATOR, COLOR_TEXT, COLOR_PRIMARY, COLOR_WHITE + COLOR_SEPARATOR, COLOR_TEXT, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION } from '../../constants/colors'; import sharedStyles from '../Styles'; @@ -147,5 +147,17 @@ export default StyleSheet.create({ height: StyleSheet.hairlineWidth, backgroundColor: COLOR_SEPARATOR, marginLeft: 72 + }, + directoryIcon: { + width: 22, + height: 22, + marginHorizontal: 15, + color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION + }, + directoryText: { + fontSize: 15, + flex: 1, + color: isIOS ? COLOR_PRIMARY : COLOR_TEXT_DESCRIPTION, + ...sharedStyles.textRegular } });