diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js index 58ee2eb96..4be2ab50d 100644 --- a/app/actions/navigation/index.js +++ b/app/actions/navigation/index.js @@ -130,6 +130,15 @@ export function showOptionsModal(title, options) { }; } +export function showDirectMessagesModal() { + return async (dispatch, getState) => { + dispatch({ + type: NavigationTypes.NAVIGATION_MODAL, + route: Routes.MoreDirectMessages + }, getState); + }; +} + export function closeModal() { return async (dispatch, getState) => { dispatch({ diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index aa7a551fa..91aef4c39 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -16,7 +16,7 @@ import {getPosts} from 'service/actions/posts'; import {savePreferences, deletePreferences} from 'service/actions/preferences'; import {getTeamMembersByIds} from 'service/actions/teams'; import {Constants, UsersTypes} from 'service/constants'; -import {getChannelByName, getDirectChannelName} from 'service/utils/channel_utils'; +import {getChannelByName, getDirectChannelName, isDirectChannelVisible} from 'service/utils/channel_utils'; import {getPreferencesByCategory} from 'service/utils/preference_utils'; export function loadChannelsIfNecessary(teamId) { @@ -103,10 +103,13 @@ export function selectInitialChannel(teamId) { const state = getState(); const {channels, myMembers} = state.entities.channels; const currentChannelId = state.entities.channels.currentId; + const currentUserId = state.entities.users.currentId; const currentChannel = channels[currentChannelId]; + const {myPreferences} = state.entities.preferences; if (currentChannel && myMembers[currentChannelId] && - (currentChannel.team_id === teamId || currentChannel.type === Constants.DM_CHANNEL)) { + (currentChannel.team_id === teamId || (currentChannel.type === Constants.DM_CHANNEL && + isDirectChannelVisible(currentUserId, myPreferences, currentChannel)))) { await selectChannel(currentChannelId)(dispatch, getState); return; } @@ -146,22 +149,31 @@ export function handlePostDraftChanged(channelId, postDraft) { }; } -export function closeDMChannel(channel) { - return async(dispatch, getState) => { +export function toggleDMChannel(otherUserId, visible) { + return async (dispatch, getState) => { const state = getState(); const userId = state.entities.users.currentId; const dm = [{ user_id: userId, category: Constants.CATEGORY_DIRECT_CHANNEL_SHOW, - name: channel.teammate_id, - value: 'false' + name: otherUserId, + value: visible }]; + return savePreferences(dm)(dispatch, getState); + }; +} + +export function closeDMChannel(channel) { + return async(dispatch, getState) => { + const state = getState(); + if (channel.isFavorite) { unmarkFavorite(channel.id)(dispatch, getState); } - savePreferences(dm)(dispatch, getState).then(() => { + + toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState).then(() => { if (channel.isCurrent) { selectInitialChannel(state.entities.teams.currentId)(dispatch, getState); } diff --git a/app/actions/views/more_dms.js b/app/actions/views/more_dms.js new file mode 100644 index 000000000..8a218c1f4 --- /dev/null +++ b/app/actions/views/more_dms.js @@ -0,0 +1,31 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {getDirectChannelName} from 'service/utils/channel_utils'; +import {createDirectChannel} from 'service/actions/channels'; +import {getTeamMember} from 'service/actions/teams'; +import {handleSelectChannel, toggleDMChannel} from 'app/actions/views/channel'; + +export function makeDirectChannel(otherUserId) { + return async (dispatch, getState) => { + const state = getState(); + const {currentId} = state.entities.users; + const channelName = getDirectChannelName(currentId, otherUserId); + const {channels, myMembers} = state.entities.channels; + const channel = Object.values(channels).find((c) => c.name === channelName); + const teamId = state.entities.teams.currentId; + + await getTeamMember(teamId, otherUserId)(dispatch, getState); + + if (channel && myMembers[channel.id]) { + await toggleDMChannel(otherUserId, 'true')(dispatch, getState); + handleSelectChannel(channel.id)(dispatch, getState); + } else { + const created = await createDirectChannel(teamId, currentId, otherUserId)(dispatch, getState); + if (created) { + await toggleDMChannel(otherUserId, 'true')(dispatch, getState); + handleSelectChannel(created.id)(dispatch, getState); + } + } + }; +} diff --git a/app/components/channel_list/channel_list.js b/app/components/channel_list/channel_list.js index f1cae858a..d97eb16e5 100644 --- a/app/components/channel_list/channel_list.js +++ b/app/components/channel_list/channel_list.js @@ -1,9 +1,17 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; +import React, {PropTypes, Component} from 'react'; -import {Alert, ListView, Platform, StyleSheet, Text, View} from 'react-native'; +import { + Alert, + ListView, + Platform, + StyleSheet, + Text, + TouchableHighlight, + View +} from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; import {Constants} from 'service/constants'; import LineDivider from 'app/components/line_divider'; @@ -11,6 +19,7 @@ import ChannelItem from './channel_item'; import FormattedText from 'app/components/formatted_text'; import UnreadIndicator from './unread_indicator'; import deepEqual from 'deep-equal'; +import Icon from 'react-native-vector-icons/FontAwesome'; const Styles = StyleSheet.create({ container: { @@ -58,23 +67,24 @@ const Styles = StyleSheet.create({ } }); -class ChannelList extends React.Component { +class ChannelList extends Component { static propTypes = { intl: intlShape.isRequired, - currentTeam: React.PropTypes.object.isRequired, - currentChannel: React.PropTypes.object, - channels: React.PropTypes.object.isRequired, - channelMembers: React.PropTypes.object, - theme: React.PropTypes.object.isRequired, - onSelectChannel: React.PropTypes.func.isRequired, - actions: React.PropTypes.shape({ - viewChannel: React.PropTypes.func.isRequired, - closeDMChannel: React.PropTypes.func.isRequired, - leaveChannel: React.PropTypes.func.isRequired, - markFavorite: React.PropTypes.func.isRequired, - unmarkFavorite: React.PropTypes.func.isRequired, - showOptionsModal: React.PropTypes.func.isRequired, - closeOptionsModal: React.PropTypes.func.isRequired + currentTeam: PropTypes.object.isRequired, + currentChannel: PropTypes.object, + channels: PropTypes.object.isRequired, + channelMembers: PropTypes.object, + theme: PropTypes.object.isRequired, + onSelectChannel: PropTypes.func.isRequired, + actions: PropTypes.shape({ + viewChannel: PropTypes.func.isRequired, + closeDMChannel: PropTypes.func.isRequired, + leaveChannel: PropTypes.func.isRequired, + markFavorite: PropTypes.func.isRequired, + unmarkFavorite: PropTypes.func.isRequired, + showOptionsModal: PropTypes.func.isRequired, + showDirectMessagesModal: PropTypes.func.isRequired, + closeOptionsModal: PropTypes.func.isRequired }).isRequired }; @@ -379,12 +389,38 @@ class ChannelList extends React.Component { />, ...privateChannels ); + + const moreDmStyle = { + position: 'absolute', + opacity: 0.6, + right: 0, + width: 50, + height: 30, + alignItems: 'center', + justifyContent: 'center' + }; + + const moreDms = ( + + + + ); data.push( - , + + + {moreDms} + , ...directChannels ); @@ -413,6 +449,10 @@ class ChannelList extends React.Component { return rowData; }; + setScrollContainer = (ref) => { + this.scrollContainer = ref; + }; + render() { if (!this.props.currentChannel) { return {'Loading'}; @@ -466,7 +506,7 @@ class ChannelList extends React.Component { true, - onListEndThreshold: 50, + onListEndReachedThreshold: 50, listPageSize: 10, listInitialSize: 10, listScrollRenderAheadDistance: 200, selectable: false, onRowSelect: () => true, showSections: true - } + }; constructor(props) { super(props); - const ds = new ListView.DataSource({ - rowHasChanged: (r1, r2) => r1 !== r2, - sectionHeaderHasChanged: (s1, s2) => s1 !== s2 - }); - let data = props.members; - if (props.showSections) { - data = this.createSections(props.members); - } - const dataSource = ds.cloneWithRowsAndSections(data); - this.state = { - data, - dataSource - }; + this.state = this.buildDataSource(props); } componentWillReceiveProps(nextProps) { - const {members, showSections} = nextProps; + const {members, showSections, searching} = nextProps; - if (members !== this.props.members || showSections !== this.props.showSections) { + if (searching || searching !== this.props.searching) { + this.setState(this.buildDataSource(nextProps)); + } else if (members !== this.props.members || showSections !== this.props.showSections) { let data = members; if (showSections) { data = this.createSections(members); @@ -101,6 +93,22 @@ export default class MemberList extends PureComponent { } } + buildDataSource = (props) => { + const ds = new ListView.DataSource({ + rowHasChanged: (r1, r2) => r1 !== r2, + sectionHeaderHasChanged: (s1, s2) => s1 !== s2 + }); + let data = props.members; + if (props.showSections) { + data = this.createSections(props.members); + } + const dataSource = ds.cloneWithRowsAndSections(data); + return { + data, + dataSource + }; + }; + createSections = (data) => { const sections = {}; data.forEach((d) => { @@ -115,7 +123,7 @@ export default class MemberList extends PureComponent { }); return sections; - } + }; handleRowSelect = (sectionId, rowId) => { const data = this.state.data; @@ -131,7 +139,7 @@ export default class MemberList extends PureComponent { data: mergedData, dataSource }, () => this.props.onRowSelect(id)); - } + }; renderSectionHeader = (sectionData, sectionId) => { return ( @@ -139,7 +147,7 @@ export default class MemberList extends PureComponent { {sectionId} ); - } + }; renderRow = (user, sectionId, rowId) => { const {id, username} = user; @@ -161,7 +169,7 @@ export default class MemberList extends PureComponent { onRowSelect={onRowSelect} /> ); - } + }; renderSeparator = (sectionId, rowId) => { return ( @@ -170,7 +178,7 @@ export default class MemberList extends PureComponent { style={style.separator} /> ); - } + }; renderFooter = () => { if (!this.props.loadingMembers) { @@ -188,7 +196,7 @@ export default class MemberList extends PureComponent { /> ); - } + }; render() { const renderRow = this.props.renderRow || this.renderRow; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index ec6366b05..97471ca8c 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -53,12 +53,16 @@ const statusToIcon = { export default class ProfilePicture extends React.PureComponent { static propTypes = { - size: React.PropTypes.number.isRequired, + size: React.PropTypes.number, user: React.PropTypes.object, status: React.PropTypes.string, theme: React.PropTypes.object.isRequired }; + static defaultProps = { + size: 128 + }; + render() { const style = getStyleSheet(this.props.theme); diff --git a/app/components/search_bar/search_bar.android.js b/app/components/search_bar/search_bar.android.js index 3c5142548..5a91caae2 100644 --- a/app/components/search_bar/search_bar.android.js +++ b/app/components/search_bar/search_bar.android.js @@ -104,6 +104,10 @@ export default class SearchBarAndroid extends PureComponent { Keyboard.dismiss(); } + blur = () => { + this.onBlur(); + }; + render() { const { height, diff --git a/app/components/search_bar/search_bar.ios.js b/app/components/search_bar/search_bar.ios.js index f55c0c66a..2307410a8 100644 --- a/app/components/search_bar/search_bar.ios.js +++ b/app/components/search_bar/search_bar.ios.js @@ -60,9 +60,18 @@ export default class SearchBarIos extends PureComponent { onBlur(event); }; + blur = () => { + this.searchBar.blur(); + }; + + searchBarRef = (ref) => { + this.searchBar = ref; + }; + render() { return ( { + const nameA = a.username; + const nameB = b.username; + + return nameA.localeCompare(nameB); + }); + } + + return { + ...ownProps, + preferences: getMyPreferences(state), + profiles: getUsers(), + search: searchSelector(state), + theme: getTheme(state), + requestStatus, + searchRequest + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + goBack, + makeDirectChannel, + getProfiles, + searchProfiles + }, dispatch) + }; +} + +export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(MoreDirectMessages); diff --git a/app/scenes/more_dms/more_dms.js b/app/scenes/more_dms/more_dms.js new file mode 100644 index 000000000..444bded73 --- /dev/null +++ b/app/scenes/more_dms/more_dms.js @@ -0,0 +1,196 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; +import { + InteractionManager, + StyleSheet, + TouchableOpacity, + View +} from 'react-native'; + +import FormattedText from 'app/components/formatted_text'; +import MemberList from 'app/components/member_list'; +import SearchBar from 'app/components/search_bar'; + +import {Constants, RequestStatus} from 'service/constants'; +import {changeOpacity} from 'app/utils/theme'; + +const style = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff' + } +}); + +class MoreDirectMessages extends PureComponent { + static propTypes = { + intl: intlShape.isRequired, + preferences: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + profiles: PropTypes.array, + search: PropTypes.array, + requestStatus: PropTypes.object.isRequired, + searchRequest: PropTypes.object.isRequired, + subscribeToHeaderEvent: React.PropTypes.func.isRequired, + unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired, + actions: PropTypes.shape({ + goBack: PropTypes.func.isRequired, + makeDirectChannel: PropTypes.func.isRequired, + getProfiles: PropTypes.func.isRequired, + searchProfiles: PropTypes.func.isRequired + }).isRequired + }; + + static navigationProps = { + renderLeftComponent: (props, emitter, theme) => { + return ( + emitter('close')} + > + + + ); + } + }; + + constructor(props) { + super(props); + + this.searchTimeoutId = 0; + + this.state = { + profiles: [], + page: 0, + next: true, + searching: false + }; + } + + componentWillMount() { + this.props.subscribeToHeaderEvent('close', this.props.actions.goBack); + } + + componentWillReceiveProps(nextProps) { + const {requestStatus} = this.props; + if (requestStatus.status === RequestStatus.STARTED && + nextProps.requestStatus.status === RequestStatus.SUCCESS) { + const {page} = this.state; + const profiles = nextProps.profiles.splice(0, (page + 1) * Constants.PROFILE_CHUNK_SIZE); + this.setState({profiles}); + } else if (this.state.searching && + nextProps.searchRequest.status === RequestStatus.SUCCESS) { + this.setState({profiles: nextProps.search}); + } + } + + componentDidMount() { + InteractionManager.runAfterInteractions(() => { + this.props.actions.getProfiles(0); + }); + } + + componentWillUnmount() { + this.props.unsubscribeFromHeaderEvent('close'); + } + + searchProfiles = (event) => { + const term = event.nativeEvent.text; + + if (term) { + this.setState({searching: true}); + clearTimeout(this.searchTimeoutId); + + this.searchTimeoutId = setTimeout(() => { + this.props.actions.searchProfiles(term); + }, Constants.SEARCH_TIMEOUT_MILLISECONDS); + } else { + this.cancelSearch(); + } + }; + + cancelSearch = () => { + this.setState({ + searching: false, + page: 0, + profiles: [] + }); + }; + + loadMoreProfiles = () => { + let {page} = this.state; + if (this.props.requestStatus.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) { + page = page + 1; + this.props.actions.getProfiles(page, Constants.PROFILE_CHUNK_SIZE). + then((data) => { + if (Object.keys(data).length) { + this.setState({ + page + }); + } else { + this.setState({next: false}); + } + }); + } + }; + + onSelectMember = (id) => { + this.searchBar.blur(); + this.props.actions.makeDirectChannel(id). + then(() => { + InteractionManager.runAfterInteractions(() => { + this.props.actions.goBack(); + }); + }); + }; + + render() { + const {formatMessage} = this.props.intl; + const isLoading = (this.props.requestStatus.status === RequestStatus.STARTED) || + (this.props.searchRequest.status === RequestStatus.STARTED); + + const more = this.state.searching ? () => true : this.loadMoreProfiles; + + return ( + + + { + this.searchBar = ref; + }} + placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})} + height={27} + fontSize={14} + hideBackground={true} + textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.4)} + onChange={this.searchProfiles} + onSearchButtonPress={() => { + this.searchBar.blur(); + }} + onCancelButtonPress={this.cancelSearch} + /> + + + + ); + } +} + +export default injectIntl(MoreDirectMessages); diff --git a/app/scenes/select_team/select_team.js b/app/scenes/select_team/select_team.js index a9ef66b9f..f714db81f 100644 --- a/app/scenes/select_team/select_team.js +++ b/app/scenes/select_team/select_team.js @@ -4,6 +4,7 @@ import React from 'react'; import { Image, + InteractionManager, Text, TouchableOpacity, View @@ -25,6 +26,7 @@ export default class SelectTeam extends React.Component { unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired, actions: React.PropTypes.shape({ goBackToChannelView: React.PropTypes.func.isRequired, + closeDrawers: React.PropTypes.func.isRequired, handleTeamChange: React.PropTypes.func.isRequired }).isRequired }; @@ -44,7 +46,7 @@ export default class SelectTeam extends React.Component { ); } - } + }; componentWillMount() { this.props.subscribeToHeaderEvent('close', this.props.actions.goBackToChannelView); @@ -54,9 +56,18 @@ export default class SelectTeam extends React.Component { this.props.unsubscribeFromHeaderEvent('close'); } - onSelectTeam(team) { - this.props.actions.handleTeamChange(team).then(this.props.actions.goBackToChannelView); - } + onSelectTeam = async (team) => { + const { + goBackToChannelView, + closeDrawers, + handleTeamChange + + } = this.props.actions; + + await handleTeamChange(team); + await InteractionManager.runAfterInteractions(goBackToChannelView); + InteractionManager.runAfterInteractions(() => setTimeout(closeDrawers, 200)); + }; render() { const teams = []; diff --git a/app/scenes/select_team/select_team_container.js b/app/scenes/select_team/select_team_container.js index 06f10e629..e21eed828 100644 --- a/app/scenes/select_team/select_team_container.js +++ b/app/scenes/select_team/select_team_container.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import navigationSceneConnect from '../navigationSceneConnect'; -import {goBack} from 'app/actions/navigation'; +import {goBack, closeDrawers} from 'app/actions/navigation'; import {handleTeamChange} from 'app/actions/views/select_team'; import {getCurrentTeam} from 'service/selectors/entities/teams'; @@ -26,6 +26,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ goBackToChannelView: goBack, + closeDrawers, handleTeamChange }, dispatch) }; diff --git a/service/actions/channels.js b/service/actions/channels.js index fe909bfe0..cd7e951b0 100644 --- a/service/actions/channels.js +++ b/service/actions/channels.js @@ -99,7 +99,7 @@ export function createDirectChannel(teamId, userId, otherUserId) { error } ]), getState); - return; + return null; } const member = { @@ -130,6 +130,8 @@ export function createDirectChannel(teamId, userId, otherUserId) { type: ChannelTypes.CREATE_CHANNEL_SUCCESS } ]), getState); + + return created; }; } diff --git a/service/actions/users.js b/service/actions/users.js index ac840b7da..a910733d3 100644 --- a/service/actions/users.js +++ b/service/actions/users.js @@ -149,14 +149,30 @@ export function logout() { } export function getProfiles(offset, limit = Constants.PROFILE_CHUNK_SIZE) { - return bindClientFunc( - Client.getProfiles, - UsersTypes.PROFILES_REQUEST, - [UsersTypes.RECEIVED_PROFILES, UsersTypes.PROFILES_SUCCESS], - UsersTypes.PROFILES_FAILURE, - offset, - limit - ); + return async (dispatch, getState) => { + dispatch({type: UsersTypes.PROFILES_REQUEST}, getState); + + let profiles; + try { + profiles = await Client.getProfiles(offset, limit); + } catch (error) { + forceLogoutIfNecessary(error, dispatch); + dispatch({type: UsersTypes.PROFILES_FAILURE, error}, getState); + return null; + } + + dispatch(batchActions([ + { + type: UsersTypes.RECEIVED_PROFILES, + data: profiles + }, + { + type: UsersTypes.PROFILES_SUCCESS + } + ]), getState); + + return profiles; + }; } export function getProfilesByIds(userIds) { @@ -325,6 +341,17 @@ export function autocompleteUsersInChannel(teamId, channelId, term) { }; } +export function searchProfiles(term, options) { + return bindClientFunc( + Client.searchProfiles, + UsersTypes.SEARCH_PROFILES_REQUEST, + [UsersTypes.RECEIVED_SEARCH_PROFILES, UsersTypes.SEARCH_PROFILES_SUCCESS], + UsersTypes.SEARCH_PROFILES_FAILURE, + term, + options + ); +} + export default { checkMfa, login, @@ -337,5 +364,6 @@ export default { getStatusesByIds, getSessions, revokeSession, - getAudits + getAudits, + searchProfiles }; diff --git a/service/client/client.js b/service/client/client.js index a1583429a..1e7cd3413 100644 --- a/service/client/client.js +++ b/service/client/client.js @@ -358,6 +358,13 @@ export default class Client { ); } + searchProfiles = (term, options) => { + return this.doFetch( + `${this.getUsersRoute()}/search`, + {method: 'post', body: JSON.stringify({term, ...options})} + ); + }; + // Team routes createTeam = async (team) => { diff --git a/service/constants/constants.js b/service/constants/constants.js index 01cb918d7..0af9ef1e9 100644 --- a/service/constants/constants.js +++ b/service/constants/constants.js @@ -5,6 +5,7 @@ const Constants = { POST_CHUNK_SIZE: 60, PROFILE_CHUNK_SIZE: 100, CHANNELS_CHUNK_SIZE: 50, + SEARCH_TIMEOUT_MILLISECONDS: 100, OFFLINE: 'offline', AWAY: 'away', diff --git a/service/constants/users.js b/service/constants/users.js index b16f58131..37dfe153f 100644 --- a/service/constants/users.js +++ b/service/constants/users.js @@ -52,8 +52,13 @@ const UserTypes = keyMirror({ AUTOCOMPLETE_IN_CHANNEL_SUCCESS: null, AUTOCOMPLETE_IN_CHANNEL_FAILURE: null, + SEARCH_PROFILES_REQUEST: null, + SEARCH_PROFILES_SUCCESS: null, + SEARCH_PROFILES_FAILURE: null, + RECEIVED_ME: null, RECEIVED_PROFILES: null, + RECEIVED_SEARCH_PROFILES: null, RECEIVED_PROFILES_IN_TEAM: null, RECEIVED_PROFILES_IN_CHANNEL: null, RECEIVED_PROFILE_IN_CHANNEL: null, diff --git a/service/reducers/entities/users.js b/service/reducers/entities/users.js index 52e3c579c..59f9f50b6 100644 --- a/service/reducers/entities/users.js +++ b/service/reducers/entities/users.js @@ -183,6 +183,19 @@ function autocompleteUsersInChannel(state = {}, action) { } } +function search(state = {}, action) { + switch (action.type) { + case UsersTypes.RECEIVED_SEARCH_PROFILES: + return action.data; + + case UsersTypes.LOGOUT_SUCCESS: + return {}; + + default: + return state; + } +} + export default combineReducers({ // the current selected user @@ -210,5 +223,8 @@ export default combineReducers({ statuses, // object where every key is a channel id and has a [channelId] object that contains members that are in and out of the current channel - autocompleteUsersInChannel + autocompleteUsersInChannel, + + // object where every key is a user id and has an object with the users details + search }); diff --git a/service/reducers/requests/users.js b/service/reducers/requests/users.js index 7e5c937b9..492752c58 100644 --- a/service/reducers/requests/users.js +++ b/service/reducers/requests/users.js @@ -153,6 +153,16 @@ function autocompleteUsersInChannel(state = initialRequestState(), action) { ); } +function searchProfiles(state = initialRequestState(), action) { + return handleRequest( + UsersTypes.SEARCH_PROFILES_REQUEST, + UsersTypes.SEARCH_PROFILES_SUCCESS, + UsersTypes.SEARCH_PROFILES_FAILURE, + state, + action + ); +} + export default combineReducers({ checkMfa, login, @@ -165,5 +175,6 @@ export default combineReducers({ getSessions, revokeSession, getAudits, - autocompleteUsersInChannel + autocompleteUsersInChannel, + searchProfiles }); diff --git a/service/selectors/entities/users.js b/service/selectors/entities/users.js index b2a4fa4c7..3c679601b 100644 --- a/service/selectors/entities/users.js +++ b/service/selectors/entities/users.js @@ -110,3 +110,17 @@ export const getAutocompleteUsersInCurrentChannel = createSelector( return autocompleteUsersInChannel[currentChannelId] || {}; } ); + +export const searchProfiles = createSelector( + (state) => state.entities.users.search, + getCurrentUserId, + (users, currentId) => { + const profiles = {...users}; + return Object.values(profiles).sort((a, b) => { + const nameA = a.username; + const nameB = b.username; + + return nameA.localeCompare(nameB); + }).filter((p) => p.id !== currentId); + } +); diff --git a/service/utils/channel_utils.js b/service/utils/channel_utils.js index 9a57a7751..8bb9268a3 100644 --- a/service/utils/channel_utils.js +++ b/service/utils/channel_utils.js @@ -90,7 +90,7 @@ function isDirectChannel(channel) { return channel.type === Constants.DM_CHANNEL; } -function isDirectChannelVisible(userId, myPreferences, channel) { +export function isDirectChannelVisible(userId, myPreferences, channel) { const channelId = getUserIdFromChannelName(userId, channel); const dm = myPreferences[`${Constants.CATEGORY_DIRECT_CHANNEL_SHOW}--${channelId}`]; return dm && dm.value === 'true'; diff --git a/test/setup.js b/test/setup.js index 0cda813a5..7d1c7aed3 100644 --- a/test/setup.js +++ b/test/setup.js @@ -22,6 +22,10 @@ m._load = function hookedLoader(request, parent, isMain) { return {uri: request}; } + if (request === './search_bar') { + request = './search_bar.ios'; + } + return originalLoader(request, parent, isMain); };