Files
mattermost/webapp/components/more_direct_channels.jsx
Joram Wilander 3a91d4e5e4 PLT-3077 Add group messaging (#5489)
* Implement server changes for group messaging

* Majority of client-side implementation

* Some server updates

* Added new React multiselect component

* Fix style issues

* Add custom renderer for options

* Fix model test

* Update ENTER functionality for multiselect control

* Remove buttons from multiselect UI control

* Updating group messaging UI (#5524)

* Move filter controls up a component level

* Scroll with arrow keys

* Updating mobile layout for multiselect (#5534)

* Fix race condition when backspacing quickly

* Hidden or new GMs show up for regular messages

* Add overriding of number remaining text

* Add UI filtering for team if config setting set

* Add icon to channel switcher and class prop to status icon

* Minor updates per feedback

* Improving group messaging UI (#5563)

* UX changes per feedback

* Update email for group messages

* UI fixes for group messaging (#5587)

* Fix missing localization string

* Add maximum users message when adding members to GM

* Fix input clearing on Android

* Updating group messaging UI (#5603)

* Updating UI for group messaging (#5604)
2017-03-02 17:48:56 -05:00

334 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import MultiSelect from 'components/multiselect/multiselect.jsx';
import ProfilePicture from 'components/profile_picture.jsx';
import {searchUsers} from 'actions/user_actions.jsx';
import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
import {displayUsernameForUser} from 'utils/utils.jsx';
import Client from 'client/web_client.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const USERS_PER_PAGE = 50;
const MAX_SELECTABLE_VALUES = Constants.MAX_USERS_IN_GM - 1;
export default class MoreDirectChannels extends React.Component {
constructor(props) {
super(props);
this.handleHide = this.handleHide.bind(this);
this.handleExit = this.handleExit.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.onChange = this.onChange.bind(this);
this.search = this.search.bind(this);
this.addValue = this.addValue.bind(this);
this.searchTimeoutId = 0;
this.listType = global.window.mm_config.RestrictDirectMessage;
const values = [];
if (props.startingUsers) {
for (let i = 0; i < props.startingUsers.length; i++) {
const user = Object.assign({}, props.startingUsers[i]);
user.value = user.id;
user.label = '@' + user.username;
values.push(user);
}
}
this.state = {
users: null,
values,
show: true,
search: false,
loadingChannel: -1
};
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
UserStore.addInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
if (this.listType === 'any') {
AsyncClient.getProfiles(0, USERS_PER_PAGE * 2);
} else {
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
}
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
UserStore.removeInTeamChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
}
handleHide() {
this.setState({show: false});
}
handleExit() {
if (this.exitToChannel) {
browserHistory.push(this.exitToChannel);
}
if (this.props.onModalDismissed) {
this.props.onModalDismissed();
}
}
handleSubmit(e) {
if (e) {
e.preventDefault();
}
if (this.state.loadingChannel !== -1) {
return;
}
const userIds = this.state.values.map((v) => v.id);
if (userIds.length === 0) {
return;
}
this.setState({loadingChannel: 1});
const success = (channel) => {
// Due to how react-overlays Modal handles focus, we delay pushing
// the new channel information until the modal is fully exited.
// The channel information will be pushed in `handleExit`
this.exitToChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name;
this.setState({loadingChannel: -1});
this.handleHide();
};
const error = () => {
this.setState({loadingChannel: -1});
};
if (userIds.length === 1) {
openDirectChannelToUser(userIds[0], success, error);
} else {
openGroupChannelToUsers(userIds, success, error);
}
}
addValue(value) {
const values = Object.assign([], this.state.values);
if (values.indexOf(value) === -1) {
values.push(value);
}
this.setState({values});
}
onChange(force) {
if (this.state.search && !force) {
return;
}
let users;
if (this.listType === 'any') {
users = Object.assign([], UserStore.getProfileList(true));
} else {
users = Object.assign([], UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true));
}
for (let i = 0; i < users.length; i++) {
const user = Object.assign({}, users[i]);
user.value = user.id;
user.label = '@' + user.username;
users[i] = user;
}
this.setState({
users
});
}
handlePageChange(page, prevPage) {
if (page > prevPage) {
AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
}
}
search(term) {
clearTimeout(this.searchTimeoutId);
if (term === '') {
this.onChange(true);
this.setState({search: false});
this.searchTimeoutId = '';
return;
}
let teamId;
if (this.listType === 'any') {
teamId = '';
} else {
teamId = TeamStore.getCurrentId();
}
const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
teamId,
{},
(users) => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
let indexToDelete = -1;
for (let i = 0; i < users.length; i++) {
if (users[i].id === UserStore.getCurrentId()) {
indexToDelete = i;
}
users[i].value = users[i].id;
users[i].label = '@' + users[i].username;
}
if (indexToDelete !== -1) {
users.splice(indexToDelete, 1);
}
this.setState({search: true, users});
}
);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
handleDelete(values) {
this.setState({values});
}
renderOption(option, isSelected, onAdd) {
var rowSelected = '';
if (isSelected) {
rowSelected = 'more-modal__row--selected';
}
return (
<div
key={option.id}
ref={isSelected ? 'selected' : option.id}
className={'more-modal__row clickable ' + rowSelected}
onClick={() => onAdd(option)}
>
<ProfilePicture
src={`${Client.getUsersRoute()}/${option.id}/image?time=${option.last_picture_update}`}
width='32'
height='32'
/>
<div
className='more-modal__details'
>
<div className='more-modal__name'>
{displayUsernameForUser(option)}
</div>
<div className='more-modal__description'>
{option.email}
</div>
</div>
<div className='more-modal__actions'>
<div className='more-modal__actions--round'>
<i className='fa fa-plus'/>
</div>
</div>
</div>
);
}
renderValue(user) {
return user.username;
}
render() {
let note;
if (this.props.startingUsers) {
if (this.state.values && this.state.values.length >= MAX_SELECTABLE_VALUES) {
note = (
<FormattedMessage
id='more_direct_channels.new_convo_note.full'
defaultMessage='Youve reached the maximum number of people for this conversation. Consider creating a private group instead.'
/>
);
} else {
note = (
<FormattedMessage
id='more_direct_channels.new_convo_note'
defaultMessage='This will start a new conversation. If youre adding a lot of people, consider creating a private group instead.'
/>
);
}
}
const numRemainingText = (
<FormattedMessage
id='multiselect.numPeopleRemaining'
defaultMessage='You can add {num, number} more {num, plural, =0 {people} one {person} other {people}}. '
values={{
num: MAX_SELECTABLE_VALUES - this.state.values.length
}}
/>
);
return (
<Modal
dialogClassName={'more-modal more-direct-channels'}
show={this.state.show}
onHide={this.handleHide}
onExited={this.handleExit}
>
<Modal.Header closeButton={true}>
<Modal.Title>
<FormattedMessage
id='more_direct_channels.title'
defaultMessage='Direct Messages'
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<MultiSelect
key='moreDirectChannelsList'
options={this.state.users}
optionRenderer={this.renderOption}
values={this.state.values}
valueRenderer={this.renderValue}
perPage={USERS_PER_PAGE}
handlePageChange={this.handlePageChange}
handleInput={this.search}
handleDelete={this.handleDelete}
handleAdd={this.addValue}
handleSubmit={this.handleSubmit}
noteText={note}
maxValues={MAX_SELECTABLE_VALUES}
numRemainingText={numRemainingText}
/>
</Modal.Body>
</Modal>
);
}
}
MoreDirectChannels.propTypes = {
startingUsers: React.PropTypes.arrayOf(React.PropTypes.object),
onModalDismissed: React.PropTypes.func
};