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:
parent
f0e1d38a48
commit
51e8ee6ea2
26 changed files with 553 additions and 76 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
31
app/actions/views/more_dms.js
Normal file
31
app/actions/views/more_dms.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ export default class SearchBarAndroid extends PureComponent {
|
|||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
this.onBlur();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
height,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ const state = {
|
|||
profilesInTeam: {},
|
||||
profilesInChannel: {},
|
||||
profilesNotInChannel: {},
|
||||
statuses: {}
|
||||
statuses: {},
|
||||
search: {}
|
||||
},
|
||||
teams: {
|
||||
currentId: '',
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
53
app/scenes/more_dms/index.js
Normal file
53
app/scenes/more_dms/index.js
Normal 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);
|
||||
196
app/scenes/more_dms/more_dms.js
Normal file
196
app/scenes/more_dms/more_dms.js
Normal 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);
|
||||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue