summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/multiselect_settings.jsx2
-rw-r--r--webapp/components/channel_header.jsx78
-rw-r--r--webapp/components/channel_switch_modal.jsx4
-rw-r--r--webapp/components/more_direct_channels.jsx308
-rw-r--r--webapp/components/multiselect/multiselect.jsx257
-rw-r--r--webapp/components/multiselect/multiselect_list.jsx169
-rw-r--r--webapp/components/navbar.jsx69
-rw-r--r--webapp/components/popover_list_members.jsx2
-rw-r--r--webapp/components/profile_popover.jsx2
-rw-r--r--webapp/components/sidebar.jsx77
-rw-r--r--webapp/components/status_icon.jsx7
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx13
12 files changed, 825 insertions, 163 deletions
diff --git a/webapp/components/admin_console/multiselect_settings.jsx b/webapp/components/admin_console/multiselect_settings.jsx
index 8aad5d6eb..2beebb337 100644
--- a/webapp/components/admin_console/multiselect_settings.jsx
+++ b/webapp/components/admin_console/multiselect_settings.jsx
@@ -76,4 +76,4 @@ MultiSelectSetting.propTypes = {
noResultText: React.PropTypes.node,
errorText: React.PropTypes.node,
notPresent: React.PropTypes.node
-}; \ No newline at end of file
+};
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 341c9be1b..9be2d5b58 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -32,7 +32,9 @@ import {getSiteURL} from 'utils/url.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import {getFlaggedPosts} from 'actions/post_actions.jsx';
-import {Constants, Preferences, UserStatuses} from 'utils/constants.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+
+import {Constants, Preferences, UserStatuses, ActionTypes} from 'utils/constants.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
@@ -53,6 +55,7 @@ export default class ChannelHeader extends React.Component {
this.getFlagged = this.getFlagged.bind(this);
this.initWebrtc = this.initWebrtc.bind(this);
this.onBusy = this.onBusy.bind(this);
+ this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
@@ -198,6 +201,14 @@ export default class ChannelHeader extends React.Component {
this.setState({isBusy});
}
+ openDirectMessageModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DM_MODAL,
+ value: true,
+ startingUsers: UserStore.getProfileListInChannel(this.props.channelId, true)
+ });
+ }
+
render() {
const flagIcon = Constants.FLAG_ICON_SVG;
@@ -246,7 +257,8 @@ export default class ChannelHeader extends React.Component {
const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel();
- const isDirect = (this.state.channel.type === 'D');
+ const isDirect = (this.state.channel.type === Constants.DM_CHANNEL);
+ const isGroup = (this.state.channel.type === Constants.GM_CHANNEL);
let webrtc;
if (isDirect) {
@@ -319,6 +331,10 @@ export default class ChannelHeader extends React.Component {
}
}
+ if (isGroup) {
+ channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
+ }
+
let channelTerm = (
<FormattedMessage
id='channel_header.channel'
@@ -364,6 +380,64 @@ export default class ChannelHeader extends React.Component {
</ToggleModalButton>
</li>
);
+ } else if (isGroup) {
+ dropdownContents.push(
+ <li
+ key='edit_header_direct'
+ role='presentation'
+ >
+ <ToggleModalButton
+ role='menuitem'
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
+ >
+ <FormattedMessage
+ id='channel_header.channelHeader'
+ defaultMessage='Edit Channel Header'
+ />
+ </ToggleModalButton>
+ </li>
+ );
+
+ dropdownContents.push(
+ <li
+ key='notification_preferences'
+ role='presentation'
+ >
+ <ToggleModalButton
+ role='menuitem'
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{
+ channel,
+ channelMember: this.state.memberChannel,
+ currentUser: this.state.currentUser
+ }}
+ >
+ <FormattedMessage
+ id='channel_header.notificationPreferences'
+ defaultMessage='Notification Preferences'
+ />
+ </ToggleModalButton>
+ </li>
+ );
+
+ dropdownContents.push(
+ <li
+ key='add_members'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.openDirectMessageModal}
+ >
+ <FormattedMessage
+ id='channel_header.addMembers'
+ defaultMessage='Add Members'
+ />
+ </a>
+ </li>
+ );
} else {
dropdownContents.push(
<li
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
index fc66e06b1..2b08ee239 100644
--- a/webapp/components/channel_switch_modal.jsx
+++ b/webapp/components/channel_switch_modal.jsx
@@ -105,7 +105,7 @@ export default class SwitchChannelModal extends React.Component {
if (user) {
openDirectChannelToUser(
- user,
+ user.id,
(ch) => {
channel = ch;
this.switchToChannel(channel);
@@ -117,7 +117,7 @@ export default class SwitchChannelModal extends React.Component {
);
}
} else {
- channel = ChannelStore.getByName(this.selected.name);
+ channel = ChannelStore.get(this.selected.id);
this.switchToChannel(channel);
}
}
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
+};
diff --git a/webapp/components/multiselect/multiselect.jsx b/webapp/components/multiselect/multiselect.jsx
new file mode 100644
index 000000000..a3e32dccf
--- /dev/null
+++ b/webapp/components/multiselect/multiselect.jsx
@@ -0,0 +1,257 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import MultiSelectList from './multiselect_list.jsx';
+
+import {localizeMessage} from 'utils/utils.jsx';
+import Constants from 'utils/constants.jsx';
+const KeyCodes = Constants.KeyCodes;
+
+import React from 'react';
+import ReactSelect from 'react-select';
+import {FormattedMessage} from 'react-intl';
+
+export default class MultiSelect extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.onSelect = this.onSelect.bind(this);
+ this.onAdd = this.onAdd.bind(this);
+ this.onInput = this.onInput.bind(this);
+ this.handleEnterPress = this.handleEnterPress.bind(this);
+ this.nextPage = this.nextPage.bind(this);
+ this.prevPage = this.prevPage.bind(this);
+
+ this.selected = null;
+
+ this.state = {
+ page: 0
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleEnterPress);
+ this.refs.select.focus();
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleEnterPress);
+ }
+
+ nextPage() {
+ if (this.props.handlePageChange) {
+ this.props.handlePageChange(this.state.page + 1, this.state.page);
+ }
+ this.refs.list.setSelected(0);
+ this.setState({page: this.state.page + 1});
+ }
+
+ prevPage() {
+ if (this.state.page === 0) {
+ return;
+ }
+
+ if (this.props.handlePageChange) {
+ this.props.handlePageChange(this.state.page - 1, this.state.page);
+ }
+ this.refs.list.setSelected(0);
+ this.setState({page: this.state.page - 1});
+ }
+
+ onSelect(selected) {
+ this.selected = selected;
+ }
+
+ onAdd(value) {
+ if (this.props.maxValues && this.props.values.length >= this.props.maxValues) {
+ return;
+ }
+
+ for (let i = 0; i < this.props.values.length; i++) {
+ if (this.props.values[i].value === value.value) {
+ return;
+ }
+ }
+
+ this.props.handleAdd(value);
+ this.selected = null;
+ this.refs.select.handleInputChange({target: {value: ''}});
+ this.onInput('');
+ this.refs.select.focus();
+ }
+
+ onInput(input) {
+ if (input === '') {
+ this.refs.list.setSelected(-1);
+ } else {
+ this.refs.list.setSelected(0);
+ }
+ this.selected = null;
+
+ this.props.handleInput(input);
+ }
+
+ handleEnterPress(e) {
+ switch (e.keyCode) {
+ case KeyCodes.ENTER:
+ if (this.selected == null) {
+ this.props.handleSubmit();
+ return;
+ }
+ this.onAdd(this.selected);
+ break;
+ }
+ }
+
+ onChange(values) {
+ if (values.length < this.props.values.length) {
+ this.props.handleDelete(values);
+ }
+ }
+
+ render() {
+ const options = this.props.options;
+
+ let numRemainingText;
+ if (this.props.numRemainingText) {
+ numRemainingText = this.props.numRemainingText;
+ } else if (this.props.maxValues != null) {
+ numRemainingText = (
+ <FormattedMessage
+ id='multiselect.numRemaining'
+ defaultMessage='You can add {num, number} more. '
+ values={{
+ num: this.props.maxValues - this.props.values.length
+ }}
+ />
+ );
+ }
+
+ let optionsToDisplay = [];
+ let nextButton;
+ let previousButton;
+ let noteTextContainer;
+
+ if (this.props.noteText) {
+ noteTextContainer = (
+ <div className='multi-select__note'>
+ <div className='note__icon'><span className='fa fa-info'/></div>
+ <div>{this.props.noteText}</div>
+ </div>
+ );
+ }
+
+ if (options && options.length > this.props.perPage) {
+ const pageStart = this.state.page * this.props.perPage;
+ const pageEnd = pageStart + this.props.perPage;
+ optionsToDisplay = options.slice(pageStart, pageEnd);
+
+ if (options.length > pageEnd) {
+ nextButton = (
+ <button
+ className='btn btn-default filter-control filter-control__next'
+ onClick={this.nextPage}
+ >
+ <FormattedMessage
+ id='filtered_user_list.next'
+ defaultMessage='Next'
+ />
+ </button>
+ );
+ }
+
+ if (this.state.page > 0) {
+ previousButton = (
+ <button
+ className='btn btn-default filter-control filter-control__prev'
+ onClick={this.prevPage}
+ >
+ <FormattedMessage
+ id='filtered_user_list.prev'
+ defaultMessage='Previous'
+ />
+ </button>
+ );
+ }
+ } else {
+ optionsToDisplay = options;
+ }
+
+ return (
+ <div className='filtered-user-list'>
+ <div className='filter-row filter-row--full'>
+ <div className='multi-select__container'>
+ <ReactSelect
+ ref='select'
+ multi={true}
+ options={this.props.options}
+ joinValues={true}
+ clearable={false}
+ openOnFocus={true}
+ onInputChange={this.onInput}
+ onBlurResetsInput={false}
+ onCloseResetsInput={false}
+ onChange={this.onChange}
+ value={this.props.values}
+ valueRenderer={this.props.valueRenderer}
+ menuRenderer={() => null}
+ arrowRenderer={() => null}
+ noResultsText={null}
+ placeholder={localizeMessage('multiselect.placeholder', 'Search and add members')}
+ />
+ <button
+ className='btn btn-primary btn-sm'
+ onClick={this.props.handleSubmit}
+ >
+ <FormattedMessage
+ id='multiselect.go'
+ defaultMessage='Go'
+ />
+ </button>
+ </div>
+ <div className='multi-select__help'>
+ <div className='hidden-xs'>
+ <FormattedMessage
+ id='multiselect.instructions'
+ defaultMessage='Use up/down arrows to navigate and enter to select'
+ />
+ </div>
+ {numRemainingText}
+ {noteTextContainer}
+ </div>
+ </div>
+ <MultiSelectList
+ ref='list'
+ options={optionsToDisplay}
+ optionRenderer={this.props.optionRenderer}
+ page={this.state.page}
+ perPage={this.props.perPage}
+ onPageChange={this.props.handlePageChange}
+ onAdd={this.onAdd}
+ onSelect={this.onSelect}
+ />
+ <div className='filter-controls'>
+ {previousButton}
+ {nextButton}
+ </div>
+ </div>
+ );
+ }
+}
+
+MultiSelect.propTypes = {
+ options: React.PropTypes.arrayOf(React.PropTypes.object),
+ optionRenderer: React.PropTypes.func,
+ values: React.PropTypes.arrayOf(React.PropTypes.object),
+ valueRenderer: React.PropTypes.func,
+ handleInput: React.PropTypes.func,
+ handleDelete: React.PropTypes.func,
+ perPage: React.PropTypes.number,
+ handlePageChange: React.PropTypes.func,
+ handleAdd: React.PropTypes.func,
+ handleSubmit: React.PropTypes.func,
+ noteText: React.PropTypes.node,
+ maxValues: React.PropTypes.number,
+ numRemainingText: React.PropTypes.node
+};
diff --git a/webapp/components/multiselect/multiselect_list.jsx b/webapp/components/multiselect/multiselect_list.jsx
new file mode 100644
index 000000000..ff9f68bf8
--- /dev/null
+++ b/webapp/components/multiselect/multiselect_list.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {cmdOrCtrlPressed} from 'utils/utils.jsx';
+import Constants from 'utils/constants.jsx';
+const KeyCodes = Constants.KeyCodes;
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+export default class MultiSelectList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.defaultOptionRenderer = this.defaultOptionRenderer.bind(this);
+ this.handleArrowPress = this.handleArrowPress.bind(this);
+ this.setSelected = this.setSelected.bind(this);
+
+ this.toSelect = -1;
+
+ this.state = {
+ selected: -1
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleArrowPress);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleArrowPress);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({selected: this.toSelect});
+
+ const options = nextProps.options;
+
+ if (options && options.length > 0 && this.toSelect >= 0) {
+ this.props.onSelect(options[this.toSelect]);
+ }
+ }
+
+ componentDidUpdate() {
+ if (this.refs.list && this.refs.selected) {
+ const elemTop = this.refs.selected.getBoundingClientRect().top;
+ const elemBottom = this.refs.selected.getBoundingClientRect().bottom;
+ const listTop = this.refs.list.getBoundingClientRect().top;
+ const listBottom = this.refs.list.getBoundingClientRect().bottom;
+ if (elemBottom > listBottom) {
+ this.refs.selected.scrollIntoView(false);
+ } else if (elemTop < listTop) {
+ this.refs.selected.scrollIntoView(true);
+ }
+ }
+ }
+
+ setSelected(selected) {
+ this.toSelect = selected;
+ }
+
+ handleArrowPress(e) {
+ if (cmdOrCtrlPressed(e) && e.shiftKey) {
+ return;
+ }
+
+ const options = this.props.options;
+ if (options.length === 0) {
+ return;
+ }
+
+ let selected;
+ switch (e.keyCode) {
+ case KeyCodes.DOWN:
+ if (this.state.selected === -1) {
+ selected = 0;
+ break;
+ }
+ selected = Math.min(this.state.selected + 1, options.length - 1);
+ break;
+ case KeyCodes.UP:
+ if (this.state.selected === -1) {
+ selected = 0;
+ break;
+ }
+ selected = Math.max(this.state.selected - 1, 0);
+ break;
+ default:
+ return;
+ }
+
+ e.preventDefault();
+ this.setState({selected});
+ this.props.onSelect(options[selected]);
+ }
+
+ defaultOptionRenderer(option, isSelected, onAdd) {
+ var rowSelected = '';
+ if (isSelected) {
+ rowSelected = 'more-modal__row--selected';
+ }
+
+ return (
+ <div
+ ref={isSelected ? 'selected' : option.value}
+ className={rowSelected}
+ key={'multiselectoption' + option.value}
+ onClick={() => onAdd(option)}
+ >
+ {option.label}
+ </div>
+ );
+ }
+
+ render() {
+ const options = this.props.options;
+
+ if (options == null || options.length === 0) {
+ return (
+ <div
+ key='no-users-found'
+ className='no-channel-message'
+ >
+ <p className='primary-message'>
+ <FormattedMessage
+ id='multiselect.list.notFound'
+ defaultMessage='No items found'
+ />
+ </p>
+ </div>
+ );
+ }
+
+ let renderer;
+ if (this.props.optionRenderer) {
+ renderer = this.props.optionRenderer;
+ } else {
+ renderer = this.defaultOptionRenderer;
+ }
+
+ const optionControls = options.map((o, i) => renderer(o, this.state.selected === i, this.props.onAdd));
+
+ return (
+ <div className='more-modal__list'>
+ <div
+ ref='list'
+ >
+ {optionControls}
+ </div>
+ </div>
+ );
+ }
+}
+
+MultiSelectList.defaultProps = {
+ options: [],
+ perPage: 50,
+ onAction: () => null
+};
+
+MultiSelectList.propTypes = {
+ options: React.PropTypes.arrayOf(React.PropTypes.object),
+ optionRenderer: React.PropTypes.func,
+ page: React.PropTypes.number,
+ perPage: React.PropTypes.number,
+ onPageChange: React.PropTypes.func,
+ onAdd: React.PropTypes.func,
+ onSelect: React.PropTypes.func
+};
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index a805a9de4..dee32416b 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -61,6 +61,8 @@ export default class Navbar extends React.Component {
this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
+ this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
+
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
@@ -206,6 +208,14 @@ export default class Navbar extends React.Component {
});
}
+ openDirectMessageModal() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_DM_MODAL,
+ value: true,
+ startingUsers: UserStore.getProfileListInChannel(this.state.channel.id, true)
+ });
+ }
+
toggleFavorite = (e) => {
e.preventDefault();
@@ -216,7 +226,7 @@ export default class Navbar extends React.Component {
}
};
- createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, popoverContent) {
+ createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, isGroup, popoverContent) {
if (channel) {
let channelTerm = (
<FormattedMessage
@@ -258,6 +268,57 @@ export default class Navbar extends React.Component {
</a>
</li>
);
+ } else if (isGroup) {
+ setChannelHeaderOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.showEditChannelHeaderModal}
+ >
+ <FormattedMessage
+ id='channel_header.channelHeader'
+ defaultMessage='Set Channel Header...'
+ />
+ </a>
+ </li>
+ );
+
+ notificationPreferenceOption = (
+ <li role='presentation'>
+ <ToggleModalButton
+ role='menuitem'
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{
+ channel,
+ channelMember: this.state.member,
+ currentUser: this.state.currentUser
+ }}
+ >
+ <FormattedMessage
+ id='navbar.preferences'
+ defaultMessage='Notification Preferences'
+ />
+ </ToggleModalButton>
+ </li>
+ );
+
+ addMembersOption = (
+ <li
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.openDirectMessageModal}
+ >
+ <FormattedMessage
+ id='navbar.addMembers'
+ defaultMessage='Add Members'
+ />
+ </a>
+ </li>
+ );
} else {
viewInfoOption = (
<li role='presentation'>
@@ -621,6 +682,7 @@ export default class Navbar extends React.Component {
var isSystemAdmin = false;
var isChannelAdmin = false;
var isDirect = false;
+ let isGroup = false;
var editChannelHeaderModal = null;
var editChannelPurposeModal = null;
@@ -660,6 +722,9 @@ export default class Navbar extends React.Component {
isDirect = true;
const teammateId = Utils.getUserIdFromChannelName(channel);
channelTitle = Utils.displayUsername(teammateId);
+ } else if (channel.type === Constants.GM_CHANNEL) {
+ isGroup = true;
+ channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
}
if (channel.header.length === 0) {
@@ -757,7 +822,7 @@ export default class Navbar extends React.Component {
</button>
);
- var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, popoverContent);
+ var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, isGroup, popoverContent);
return (
<div>
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 5ffcb687a..6d4ed056c 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -48,7 +48,7 @@ export default class PopoverListMembers extends React.Component {
e.preventDefault();
openDirectChannelToUser(
- teammate,
+ teammate.id,
(channel, channelAlreadyExisted) => {
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
if (channelAlreadyExisted) {
diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx
index fc22c1314..c7d45474f 100644
--- a/webapp/components/profile_popover.jsx
+++ b/webapp/components/profile_popover.jsx
@@ -81,7 +81,7 @@ export default class ProfilePopover extends React.Component {
this.setState({loadingDMChannel: user.id});
openDirectChannelToUser(
- user,
+ user.id,
(channel) => {
if (Utils.isMobile()) {
GlobalActions.emitCloseRightHandSide();
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index c4c5f0517..ce584d477 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -15,6 +15,7 @@ import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import ModalStore from 'stores/modal_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -22,7 +23,7 @@ import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
-import Constants from 'utils/constants.jsx';
+import {ActionTypes, Constants} from 'utils/constants.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -49,6 +50,8 @@ export default class Sidebar extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this);
+ this.onModalChange = this.onModalChange.bind(this);
+ this.onInChannelChange = this.onInChannelChange.bind(this);
this.onScroll = this.onScroll.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
@@ -77,6 +80,7 @@ export default class Sidebar extends React.Component {
state.showDirectChannelsModal = false;
state.showMoreChannelsModal = false;
state.loadingDMChannel = -1;
+ state.inChannelChange = false;
this.state = state;
}
@@ -96,7 +100,7 @@ export default class Sidebar extends React.Component {
Object.keys(unreadCounts).forEach((chId) => {
const channel = ChannelStore.get(chId);
- if (channel && (channel.type === 'D' || channel.team_id === this.state.currentTeam.id)) {
+ if (channel && (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL || channel.team_id === this.state.currentTeam.id)) {
msgs += unreadCounts[chId].msgs;
mentions += unreadCounts[chId].mentions;
}
@@ -128,13 +132,19 @@ export default class Sidebar extends React.Component {
};
}
+ onInChannelChange() {
+ this.setState({inChannelChange: !this.state.inChannelChange});
+ }
+
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
UserStore.addChangeListener(this.onChange);
UserStore.addInTeamChangeListener(this.onChange);
+ UserStore.addInChannelChangeListener(this.onInChannelChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onChange);
+ ModalStore.addModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
this.updateTitle();
this.updateUnreadIndicators();
@@ -179,13 +189,19 @@ export default class Sidebar extends React.Component {
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeChangeListener(this.onChange);
UserStore.removeInTeamChangeListener(this.onChange);
+ UserStore.removeInChannelChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onChange);
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
document.removeEventListener('keydown', this.navigateChannelShortcut);
document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
}
+ onModalChange(value, args) {
+ this.showMoreDirectChannelsModal(args.startingUsers);
+ }
+
onChange() {
if (this.state.currentTeam.id !== TeamStore.getCurrentId()) {
ChannelStore.clear();
@@ -203,11 +219,13 @@ export default class Sidebar extends React.Component {
}
let currentChannelName = channel.display_name;
- if (channel.type === 'D') {
+ if (channel.type === Constants.DM_CHANNEL) {
const teammate = Utils.getDirectTeammate(channel.id);
if (teammate != null) {
currentChannelName = teammate.username;
}
+ } else if (channel.type === Constants.GM_CHANNEL) {
+ currentChannelName = ChannelUtils.buildGroupChannelName(channel.id);
}
const unread = this.getTotalUnreadCount();
@@ -331,7 +349,7 @@ export default class Sidebar extends React.Component {
}
getDisplayedChannels() {
- return this.state.favoriteChannels.concat(this.state.publicChannels).concat(this.state.privateChannels).concat(this.state.directChannels).concat(this.state.directNonTeamChannels);
+ return this.state.favoriteChannels.concat(this.state.publicChannels).concat(this.state.privateChannels).concat(this.state.directAndGroupChannels).concat(this.state.directNonTeamChannels);
}
handleLeaveDirectChannel(e, channel) {
@@ -340,9 +358,19 @@ export default class Sidebar extends React.Component {
if (!this.isLeaving.get(channel.id)) {
this.isLeaving.set(channel.id, true);
+ let id;
+ let category;
+ if (channel.type === Constants.DM_CHANNEL) {
+ id = channel.teammate_id;
+ category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
+ } else {
+ id = channel.id;
+ category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
+ }
+
AsyncClient.savePreference(
- Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
- channel.teammate_id,
+ category,
+ id,
'false',
() => {
this.isLeaving.set(channel.id, false);
@@ -382,13 +410,13 @@ export default class Sidebar extends React.Component {
this.setState({newChannelModalType: ''});
}
- showMoreDirectChannelsModal() {
+ showMoreDirectChannelsModal(startingUsers) {
trackEvent('ui', 'ui_channels_more_direct');
- this.setState({showDirectChannelsModal: true});
+ this.setState({showDirectChannelsModal: true, startingUsers});
}
hideMoreDirectChannelsModal() {
- this.setState({showDirectChannelsModal: false});
+ this.setState({showDirectChannelsModal: false, startingUsers: null});
}
openLeftSidebar() {
@@ -509,11 +537,16 @@ export default class Sidebar extends React.Component {
rowClass += ' has-badge';
}
+ let displayName = channel.display_name;
+
var icon = null;
if (channel.type === Constants.OPEN_CHANNEL) {
icon = <div className='status'><i className='fa fa-globe'/></div>;
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = <div className='status'><i className='fa fa-lock'/></div>;
+ } else if (channel.type === Constants.GM_CHANNEL) {
+ displayName = ChannelUtils.buildGroupChannelName(channel.id);
+ icon = <div className='status status--group'>{UserStore.getProfileListInChannel(channel.id, true).length}</div>;
} else {
// set up status icon for direct message channels (status is null for other channel types)
icon = (
@@ -576,7 +609,7 @@ export default class Sidebar extends React.Component {
onClick={this.trackChannelSelectedEvent}
>
{icon}
- {channel.display_name}
+ {displayName}
{badge}
{closeButton}
</Link>
@@ -615,27 +648,10 @@ export default class Sidebar extends React.Component {
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
- const directMessageItems = this.state.directChannels.map((channel, index, arr) => {
+ const directMessageItems = this.state.directAndGroupChannels.map((channel, index, arr) => {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
});
- const directMessageNonTeamItems = this.state.directNonTeamChannels.map((channel, index, arr) => {
- return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
- });
-
- let directDivider;
- if (directMessageNonTeamItems.length !== 0) {
- directDivider =
- (<div className='sidebar__divider'>
- <div className='sidebar__divider__text'>
- <FormattedMessage
- id='sidebar.otherMembers'
- defaultMessage='Outside this team'
- />
- </div>
- </div>);
- }
-
// update the favicon to show if there are any notifications
if (this.lastBadgesActive !== this.badgesActive) {
var link = document.createElement('link');
@@ -659,7 +675,7 @@ export default class Sidebar extends React.Component {
<li key='more'>
<a
href='#'
- onClick={this.showMoreDirectChannelsModal}
+ onClick={() => this.showMoreDirectChannelsModal()}
>
<FormattedMessage
id='sidebar.moreElips'
@@ -753,6 +769,7 @@ export default class Sidebar extends React.Component {
moreDirectChannelsModal = (
<MoreDirectChannels
onModalDismissed={this.hideMoreDirectChannelsModal}
+ startingUsers={this.state.startingUsers}
/>
);
}
@@ -866,8 +883,6 @@ export default class Sidebar extends React.Component {
</h4>
</li>
{directMessageItems}
- {directDivider}
- {directMessageNonTeamItems}
{directMessageMore}
</ul>
</div>
diff --git a/webapp/components/status_icon.jsx b/webapp/components/status_icon.jsx
index 3e71344d9..cf5ef6947 100644
--- a/webapp/components/status_icon.jsx
+++ b/webapp/components/status_icon.jsx
@@ -33,7 +33,7 @@ export default class StatusIcon extends React.Component {
return (
<span
- className='status'
+ className={'status ' + this.props.className}
dangerouslySetInnerHTML={{__html: statusIcon}}
/>
);
@@ -41,7 +41,12 @@ export default class StatusIcon extends React.Component {
}
+StatusIcon.defaultProps = {
+ className: ''
+};
+
StatusIcon.propTypes = {
status: React.PropTypes.string,
+ className: React.PropTypes.string,
type: React.PropTypes.string
};
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index 3b7bec319..6d4340780 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -12,7 +12,7 @@ import Client from 'client/web_client.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import {Constants, ActionTypes} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
-import {sortChannelsByDisplayName} from 'utils/channel_utils.jsx';
+import {sortChannelsByDisplayName, buildGroupChannelName} from 'utils/channel_utils.jsx';
import React from 'react';
@@ -25,12 +25,15 @@ class SwitchChannelSuggestion extends Suggestion {
className += ' suggestion--selected';
}
- const displayName = item.display_name;
+ let displayName = item.display_name;
let icon = null;
if (item.type === Constants.OPEN_CHANNEL) {
icon = <div className='status'><i className='fa fa-globe'/></div>;
} else if (item.type === Constants.PRIVATE_CHANNEL) {
icon = <div className='status'><i className='fa fa-lock'/></div>;
+ } else if (item.type === Constants.GM_CHANNEL) {
+ displayName = buildGroupChannelName(item.id);
+ icon = <div className='status status--group'>{UserStore.getProfileListInChannel(item.id, true).length}</div>;
} else {
icon = (
<div className='pull-left'>
@@ -74,7 +77,11 @@ export default class SwitchChannelProvider extends Provider {
for (const id of Object.keys(allChannels)) {
const channel = allChannels[id];
if (channel.display_name.toLowerCase().indexOf(channelPrefix.toLowerCase()) !== -1) {
- channels.push(channel);
+ const newChannel = Object.assign({}, channel);
+ if (newChannel.type === Constants.GM_CHANNEL) {
+ newChannel.name = buildGroupChannelName(newChannel.id);
+ }
+ channels.push(newChannel);
}
}