More Direct Messages scene (#244)

* More Direct Messages scene

* Adding search_bar.js to mock tests

* Feedback review

* Address feedback review 2
This commit is contained in:
enahum 2017-02-14 13:49:41 -03:00 committed by Harrison Healey
parent f0e1d38a48
commit 51e8ee6ea2
26 changed files with 553 additions and 76 deletions

View file

@ -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({

View file

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

View file

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

View file

@ -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 = (
<TouchableHighlight
style={moreDmStyle}
onPress={this.props.actions.showDirectMessagesModal}
>
<Icon
name='plus-circle'
size={18}
color={theme.sidebarText}
/>
</TouchableHighlight>
);
data.push(
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.direct'
defaultMessage='DIRECT MESSAGES'
/>,
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.direct'
defaultMessage='DIRECT MESSAGES'
/>
{moreDms}
</View>,
...directChannels
);
@ -413,6 +449,10 @@ class ChannelList extends React.Component {
return rowData;
};
setScrollContainer = (ref) => {
this.scrollContainer = ref;
};
render() {
if (!this.props.currentChannel) {
return <Text>{'Loading'}</Text>;
@ -466,7 +506,7 @@ class ChannelList extends React.Component {
</Text>
</View>
<ListView
ref='scrollContainer'
ref={this.setScrollContainer}
style={Styles.scrollContainer}
dataSource={this.state.dataSource}
renderRow={this.renderRow}

View file

@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {showOptionsModal, closeModal} from 'app/actions/navigation';
import {showDirectMessagesModal, showOptionsModal, closeModal} from 'app/actions/navigation';
import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel';
import {viewChannel} from 'service/actions/channels';
@ -25,6 +25,7 @@ function mapDispatchToProps(dispatch) {
markFavorite,
unmarkFavorite,
showOptionsModal,
showDirectMessagesModal,
closeOptionsModal: closeModal
}, dispatch)
};

View file

@ -40,6 +40,7 @@ const style = StyleSheet.create({
export default class MemberList extends PureComponent {
static propTypes = {
members: PropTypes.array.isRequired,
searching: PropTypes.bool,
onRowPress: PropTypes.func,
onListEndReached: PropTypes.func,
onListEndReachedThreshold: PropTypes.number,
@ -52,41 +53,32 @@ export default class MemberList extends PureComponent {
selectable: PropTypes.bool,
onRowSelect: PropTypes.func,
renderRow: PropTypes.func
}
};
static defaultProps = {
searching: false,
onListEndReached: () => 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 {
<Text style={style.sectionText}>{sectionId}</Text>
</View>
);
}
};
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 {
/>
</View>
);
}
};
render() {
const renderRow = this.props.renderRow || this.renderRow;

View file

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

View file

@ -104,6 +104,10 @@ export default class SearchBarAndroid extends PureComponent {
Keyboard.dismiss();
}
blur = () => {
this.onBlur();
};
render() {
const {
height,

View file

@ -60,9 +60,18 @@ export default class SearchBarIos extends PureComponent {
onBlur(event);
};
blur = () => {
this.searchBar.blur();
};
searchBarRef = (ref) => {
this.searchBar = ref;
};
render() {
return (
<SearchBar
ref={this.searchBarRef}
barStyle={this.props.barStyle}
searchBarStyle={this.props.searchBarStyle}
text={this.props.text}

View file

@ -21,7 +21,8 @@ const state = {
profilesInTeam: {},
profilesInChannel: {},
profilesNotInChannel: {},
statuses: {}
statuses: {},
search: {}
},
teams: {
currentId: '',

View file

@ -10,6 +10,7 @@ import {
LoadTeam,
Login,
Mfa,
MoreDirectMessages,
OptionsModal,
RightMenuDrawer,
Root,
@ -79,6 +80,13 @@ export const Routes = {
title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}
}
},
MoreDirectMessages: {
key: 'MoreDirectMessages',
component: MoreDirectMessages,
navigationProps: {
title: {id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}
}
},
OptionsModal: {
key: 'OptionsModal',
component: OptionsModal,
@ -108,7 +116,6 @@ export const Routes = {
},
SelectTeam: {
key: 'SelectTeam',
transition: RouteTransitions.Horizontal,
component: SelectTeam,
navigationProps: {
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}

View file

@ -9,6 +9,7 @@ import ChannelAddMembers from './channel_add_members';
import LoadTeam from './load_team';
import Login from './login/login_container.js';
import Mfa from './mfa';
import MoreDirectMessages from './more_dms';
import OptionsModal from './options_modal';
import RightMenuDrawer from './right_menu_drawer';
import Root from './root/root_container.js';
@ -26,6 +27,7 @@ module.exports = {
LoadTeam,
Login,
Mfa,
MoreDirectMessages,
OptionsModal,
RightMenuDrawer,
Root,

View file

@ -0,0 +1,53 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goBack} from 'app/actions/navigation';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {getProfiles, searchProfiles} from 'service/actions/users';
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
import {searchProfiles as searchSelector} from 'service/selectors/entities/users';
import MoreDirectMessages from './more_dms';
function mapStateToProps(state, ownProps) {
const {getProfiles: requestStatus, searchProfiles: searchRequest} = state.requests.users;
function getUsers() {
const {profiles, currentId} = state.entities.users;
const users = {...profiles};
Reflect.deleteProperty(users, currentId);
return Object.values(users).sort((a, b) => {
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);

View file

@ -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 (
<TouchableOpacity
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
onPress={() => emitter('close')}
>
<FormattedText
id='more_direct_channels.close'
defaultMessage='Close'
style={{color: theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
);
}
};
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 (
<View style={style.container}>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref={(ref) => {
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}
/>
</View>
<MemberList
members={this.state.profiles}
searching={this.state.searching}
onListEndReached={more}
preferences={this.props.preferences}
loadingMembers={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
onRowPress={this.onSelectMember}
/>
</View>
);
}
}
export default injectIntl(MoreDirectMessages);

View file

@ -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 {
</TouchableOpacity>
);
}
}
};
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 = [];

View file

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

View file

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

View file

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

View file

@ -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) => {

View file

@ -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',

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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';

View file

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