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
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
|
||||
};
|
||||
Ссылка в новой задаче
Block a user