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);
};