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)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8c5cee9521
Коммит
3a91d4e5e4
@@ -76,4 +76,4 @@ MultiSelectSetting.propTypes = {
|
||||
noResultText: React.PropTypes.node,
|
||||
errorText: React.PropTypes.node,
|
||||
notPresent: React.PropTypes.node
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +140,17 @@ 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));
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -118,72 +158,51 @@ export default class MoreDirectChannels extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
toggleList(e) {
|
||||
const listType = e.target.value;
|
||||
let users;
|
||||
if (listType === 'any') {
|
||||
users = UserStore.getProfileList(true);
|
||||
} else {
|
||||
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
users,
|
||||
listType
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
257
webapp/components/multiselect/multiselect.jsx
Обычный файл
257
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
|
||||
};
|
||||
169
webapp/components/multiselect/multiselect_list.jsx
Обычный файл
169
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
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user