diff options
author | Joram Wilander <jwawilander@gmail.com> | 2017-03-02 17:48:56 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-02 17:48:56 -0500 |
commit | 3a91d4e5e419a43ff19a0736ce697f8d611d36e3 (patch) | |
tree | e57ad85d49f8768a575f27c89d338a4ccaeda521 /webapp/components/more_direct_channels.jsx | |
parent | 8c5cee9521656bcffb371aad9dae4bea8fc70e29 (diff) | |
download | chat-3a91d4e5e419a43ff19a0736ce697f8d611d36e3.tar.gz chat-3a91d4e5e419a43ff19a0736ce697f8d611d36e3.tar.bz2 chat-3a91d4e5e419a43ff19a0736ce697f8d611d36e3.zip |
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)
Diffstat (limited to 'webapp/components/more_direct_channels.jsx')
-rw-r--r-- | webapp/components/more_direct_channels.jsx | 308 |
1 files changed, 189 insertions, 119 deletions
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index 13ee50b4d..c4a3a3526 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -1,19 +1,19 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import SearchableUserList from 'components/searchable_user_list.jsx'; -import SpinnerButton from 'components/spinner_button.jsx'; +import MultiSelect from 'components/multiselect/multiselect.jsx'; +import ProfilePicture from 'components/profile_picture.jsx'; import {searchUsers} from 'actions/user_actions.jsx'; -import {openDirectChannelToUser} from 'actions/channel_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 * as UserAgent from 'utils/user_agent.jsx'; -import {localizeMessage} from 'utils/utils.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'; @@ -21,6 +21,7 @@ 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) { @@ -28,21 +29,31 @@ export default class MoreDirectChannels extends React.Component { this.handleHide = this.handleHide.bind(this); this.handleExit = this.handleExit.bind(this); - this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); this.onChange = this.onChange.bind(this); - this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this); - this.toggleList = this.toggleList.bind(this); - this.nextPage = this.nextPage.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, - loadingDMChannel: -1, - listType: 'team', + values, show: true, - search: false + search: false, + loadingChannel: -1 }; } @@ -50,17 +61,18 @@ export default class MoreDirectChannels extends React.Component { UserStore.addChangeListener(this.onChange); UserStore.addInTeamChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); - TeamStore.addChangeListener(this.onChange); - AsyncClient.getProfiles(0, Constants.PROFILE_CHUNK_SIZE); - AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, Constants.PROFILE_CHUNK_SIZE); + 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); - TeamStore.removeChangeListener(this.onChange); } handleHide() { @@ -68,8 +80,8 @@ export default class MoreDirectChannels extends React.Component { } handleExit() { - if (this.exitToDirectChannel) { - browserHistory.push(this.exitToDirectChannel); + if (this.exitToChannel) { + browserHistory.push(this.exitToChannel); } if (this.props.onModalDismissed) { @@ -77,28 +89,49 @@ export default class MoreDirectChannels extends React.Component { } } - handleShowDirectChannel(teammate, e) { - e.preventDefault(); + handleSubmit(e) { + if (e) { + e.preventDefault(); + } - if (this.state.loadingDMChannel !== -1) { + if (this.state.loadingChannel !== -1) { return; } - this.setState({loadingDMChannel: teammate.id}); - openDirectChannelToUser( - teammate, - (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.exitToDirectChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name; - this.setState({loadingDMChannel: -1}); - this.handleHide(); - }, - () => { - this.setState({loadingDMChannel: -1}); - } - ); + 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) { @@ -107,83 +140,69 @@ export default class MoreDirectChannels extends React.Component { } let users; - if (this.state.listType === 'any') { - users = UserStore.getProfileList(true); + if (this.listType === 'any') { + users = Object.assign([], UserStore.getProfileList(true)); } else { - users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true); + users = Object.assign([], UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true)); } - this.setState({ - users - }); - } - - toggleList(e) { - const listType = e.target.value; - let users; - if (listType === 'any') { - users = UserStore.getProfileList(true); - } else { - users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, 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, - listType + users }); } - createJoinDirectChannelButton({user}) { - return ( - <SpinnerButton - className='btn btm-sm btn-primary' - spinning={this.state.loadingDMChannel === user.id} - onClick={this.handleShowDirectChannel.bind(this, user)} - > - <FormattedMessage - id='more_direct_channels.message' - defaultMessage='Message' - /> - </SpinnerButton> - ); - } - - nextPage(page) { - if (this.state.listType === 'any') { + handlePageChange(page, prevPage) { + if (page > prevPage) { AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE); - } else { - AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), (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.state.listType === 'any') { + if (this.listType === 'any') { teamId = ''; } else { teamId = TeamStore.getCurrentId(); } - clearTimeout(this.searchTimeoutId); - - this.searchTimeoutId = setTimeout( + 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()) { - users.splice(i, 1); - break; + 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}); } @@ -191,44 +210,88 @@ export default class MoreDirectChannels extends React.Component { }, Constants.SEARCH_TIMEOUT_MILLISECONDS ); + + this.searchTimeoutId = searchTimeoutId; } - render() { - let teamToggle; - let memberClass = ''; - if (global.window.mm_config.RestrictDirectMessage === 'any') { - memberClass = 'more-system-members'; - teamToggle = ( - <div className='member-select__container'> - <select - className='form-control' - id='restrictList' - ref='restrictList' - defaultValue='team' - onChange={this.toggleList} - > - <option value='any'> - {localizeMessage('filtered_user_list.any_team', 'All Users')} - </option> - <option value='team'> - {localizeMessage('filtered_user_list.team_only', 'Members of this Team')} - </option> - </select> - <span - className='member-show' - > - <FormattedMessage - id='filtered_user_list.show' - defaultMessage='Filter:' - /> - </span> + 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='You’ve 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 you’re 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 ' + memberClass} + dialogClassName={'more-modal more-direct-channels'} show={this.state.show} onHide={this.handleHide} onExited={this.handleExit} @@ -242,15 +305,21 @@ export default class MoreDirectChannels extends React.Component { </Modal.Title> </Modal.Header> <Modal.Body> - {teamToggle} - <SearchableUserList - key={'moreDirectChannelsList_' + this.state.listType} - users={this.state.users} - usersPerPage={USERS_PER_PAGE} - nextPage={this.nextPage} - search={this.search} - actions={[this.createJoinDirectChannelButton]} - focusOnMount={!UserAgent.isMobile()} + <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> @@ -259,5 +328,6 @@ export default class MoreDirectChannels extends React.Component { } MoreDirectChannels.propTypes = { + startingUsers: React.PropTypes.arrayOf(React.PropTypes.object), onModalDismissed: React.PropTypes.func -};
\ No newline at end of file +}; |