PLT-4403 Add server-based channel autocomplete, search and paging (#4585)
* Add more channel paging API * Add channel paging support to client * Add DB channel search functions * Add API for searching more channels * Add more channel search functionality to client * Add API for autocompleting channels * Add channel autocomplete functionality to the client * Move to be deprecated APIs to their own file * Final clean-up * Fixes related to feedback * Localization changes * Add unit as suffix to timeout constants
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
c96ecae6da
Коммит
981ea33b8e
@@ -1,180 +0,0 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
|
||||
import {localizeMessage} from 'utils/utils.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
export default class FilteredChannelList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFilterChange = this.handleFilterChange.bind(this);
|
||||
this.createChannelRow = this.createChannelRow.bind(this);
|
||||
this.filterChannels = this.filterChannels.bind(this);
|
||||
|
||||
this.state = {
|
||||
filter: '',
|
||||
joiningChannel: '',
|
||||
channels: this.filterChannels(props.channels)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// assume the channel list is immutable
|
||||
if (this.props.channels !== nextProps.channels) {
|
||||
this.setState({
|
||||
channels: this.filterChannels(nextProps.channels)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
|
||||
if (!UserAgent.isMobile()) {
|
||||
ReactDOM.findDOMNode(this.refs.filter).focus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.filter !== this.state.filter) {
|
||||
$(ReactDOM.findDOMNode(this.refs.channelList)).scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
handleJoin(channel) {
|
||||
this.setState({joiningChannel: channel.id});
|
||||
this.props.handleJoin(
|
||||
channel,
|
||||
() => {
|
||||
this.setState({joiningChannel: ''});
|
||||
});
|
||||
}
|
||||
|
||||
createChannelRow(channel) {
|
||||
let joinButton;
|
||||
if (this.state.joiningChannel === channel.id) {
|
||||
joinButton = (
|
||||
<img
|
||||
className='join-channel-loading-gif'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
joinButton = (
|
||||
<button
|
||||
onClick={this.handleJoin.bind(this, channel)}
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_channels.join'
|
||||
defaultMessage='Join'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='more-modal__row'
|
||||
key={channel.id}
|
||||
>
|
||||
<div className='more-modal__details'>
|
||||
<p className='more-modal__name'>{channel.display_name}</p>
|
||||
<p className='more-modal__description'>{channel.purpose}</p>
|
||||
</div>
|
||||
<div className='more-modal__actions'>
|
||||
{joinButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
filterChannels(channels) {
|
||||
if (!this.state || !this.state.filter) {
|
||||
return channels;
|
||||
}
|
||||
|
||||
return channels.filter((chan) => {
|
||||
const filter = this.state.filter.toLowerCase();
|
||||
return Boolean((chan.name.toLowerCase().indexOf(filter) !== -1 || chan.display_name.toLowerCase().indexOf(filter) !== -1) && chan.delete_at === 0);
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterChange(e) {
|
||||
this.setState({
|
||||
filter: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let channels = this.state.channels;
|
||||
|
||||
if (this.state.filter && this.state.filter.length > 0) {
|
||||
channels = this.filterChannels(channels);
|
||||
}
|
||||
|
||||
let count;
|
||||
if (channels.length === this.props.channels.length) {
|
||||
count = (
|
||||
<FormattedMessage
|
||||
id='filtered_channels_list.count'
|
||||
defaultMessage='{count} {count, plural, =0 {0 channels} one {channel} other {channels}}'
|
||||
values={{
|
||||
count: channels.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
count = (
|
||||
<FormattedMessage
|
||||
id='filtered_channels_list.countTotal'
|
||||
defaultMessage='{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} Total'
|
||||
values={{
|
||||
count: channels.length,
|
||||
total: this.props.channels.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='filtered-user-list'>
|
||||
<div className='filter-row'>
|
||||
<div className='col-sm-6'>
|
||||
<input
|
||||
ref='filter'
|
||||
className='form-control filter-textbox'
|
||||
placeholder={localizeMessage('filtered_channels_list.search', 'Search channels')}
|
||||
onInput={this.handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<span className='channel-count pull-left'>{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref='channelList'
|
||||
className='more-modal__list'
|
||||
>
|
||||
{channels.map(this.createChannelRow)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilteredChannelList.defaultProps = {
|
||||
channels: []
|
||||
};
|
||||
|
||||
FilteredChannelList.propTypes = {
|
||||
channels: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
handleJoin: React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -2,18 +2,16 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import NewChannelFlow from './new_channel_flow.jsx';
|
||||
import FilteredChannelList from './filtered_channel_list.jsx';
|
||||
import SearchableChannelList from './searchable_channel_list.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import {joinChannel, searchMoreChannels} from 'actions/channel_actions.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {browserHistory} from 'react-router/es6';
|
||||
@@ -21,19 +19,28 @@ import {browserHistory} from 'react-router/es6';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
const CHANNELS_CHUNK_SIZE = 50;
|
||||
const CHANNELS_PER_PAGE = 50;
|
||||
const SEARCH_TIMEOUT_MILLISECONDS = 100;
|
||||
|
||||
export default class MoreChannels extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.handleJoin = this.handleJoin.bind(this);
|
||||
this.handleNewChannel = this.handleNewChannel.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
this.state = {
|
||||
channelType: '',
|
||||
showNewChannelModal: false,
|
||||
search: false,
|
||||
channels: null,
|
||||
serverError: null
|
||||
};
|
||||
@@ -41,10 +48,10 @@ export default class MoreChannels extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const self = this;
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
|
||||
$(this.refs.modal).on('shown.bs.modal', () => {
|
||||
AsyncClient.getMoreChannels(true);
|
||||
AsyncClient.getMoreChannelsPage(0, CHANNELS_CHUNK_SIZE * 2);
|
||||
});
|
||||
|
||||
$(this.refs.modal).on('show.bs.modal', (e) => {
|
||||
@@ -54,25 +61,26 @@ export default class MoreChannels extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
channels: ChannelStore.getMoreAll(),
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
onListenerChange() {
|
||||
const newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(newState.channels, this.state.channels)) {
|
||||
this.setState(newState);
|
||||
onChange(force) {
|
||||
if (this.state.search && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
channels: ChannelStore.getMoreChannelsList(),
|
||||
serverError: null
|
||||
});
|
||||
}
|
||||
|
||||
nextPage(page) {
|
||||
AsyncClient.getMoreChannelsPage((page + 1) * CHANNELS_PER_PAGE, CHANNELS_PER_PAGE);
|
||||
}
|
||||
|
||||
handleJoin(channel, done) {
|
||||
GlobalActions.emitJoinChannelEvent(
|
||||
joinChannel(
|
||||
channel,
|
||||
() => {
|
||||
$(this.refs.modal).modal('hide');
|
||||
@@ -95,6 +103,28 @@ export default class MoreChannels extends React.Component {
|
||||
this.setState({showNewChannelModal: true});
|
||||
}
|
||||
|
||||
search(term) {
|
||||
if (term === '') {
|
||||
this.onChange(true);
|
||||
this.setState({search: false});
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(
|
||||
() => {
|
||||
searchMoreChannels(
|
||||
term,
|
||||
(channels) => {
|
||||
this.setState({search: true, channels});
|
||||
}
|
||||
);
|
||||
},
|
||||
SEARCH_TIMEOUT_MILLISECONDS
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let serverError;
|
||||
if (this.state.serverError) {
|
||||
@@ -136,31 +166,6 @@ export default class MoreChannels extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
let moreChannels;
|
||||
const channels = this.state.channels;
|
||||
if (channels == null) {
|
||||
moreChannels = <LoadingScreen/>;
|
||||
} else if (channels.length) {
|
||||
moreChannels = (
|
||||
<FilteredChannelList
|
||||
channels={channels}
|
||||
handleJoin={this.handleJoin}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
moreChannels = (
|
||||
<div className='no-channel-message'>
|
||||
<p className='primary-message'>
|
||||
<FormattedMessage
|
||||
id='more_channels.noMore'
|
||||
defaultMessage='No more channels to join'
|
||||
/>
|
||||
</p>
|
||||
{createChannelHelpText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='modal fade more-channel__modal'
|
||||
@@ -200,7 +205,14 @@ export default class MoreChannels extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
{moreChannels}
|
||||
<SearchableChannelList
|
||||
channels={this.state.channels}
|
||||
channelsPerPage={CHANNELS_PER_PAGE}
|
||||
nextPage={this.nextPage}
|
||||
search={this.search}
|
||||
handleJoin={this.handleJoin}
|
||||
noResultsText={createChannelHelpText}
|
||||
/>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
|
||||
205
webapp/components/searchable_channel_list.jsx
Обычный файл
205
webapp/components/searchable_channel_list.jsx
Обычный файл
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import {localizeMessage} from 'utils/utils.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
const NEXT_BUTTON_TIMEOUT_MILLISECONDS = 500;
|
||||
|
||||
export default class SearchableChannelList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.createChannelRow = this.createChannelRow.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.previousPage = this.previousPage.bind(this);
|
||||
this.doSearch = this.doSearch.bind(this);
|
||||
|
||||
this.nextTimeoutId = 0;
|
||||
|
||||
this.state = {
|
||||
joiningChannel: '',
|
||||
page: 0,
|
||||
nextDisabled: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// only focus the search box on desktop so that we don't cause the keyboard to open on mobile
|
||||
if (!UserAgent.isMobile()) {
|
||||
this.refs.filter.focus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.page !== this.state.page) {
|
||||
$(this.refs.channelList).scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
handleJoin(channel) {
|
||||
this.setState({joiningChannel: channel.id});
|
||||
this.props.handleJoin(
|
||||
channel,
|
||||
() => {
|
||||
this.setState({joiningChannel: ''});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createChannelRow(channel) {
|
||||
let joinButton;
|
||||
if (this.state.joiningChannel === channel.id) {
|
||||
joinButton = (
|
||||
<img
|
||||
className='join-channel-loading-gif'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
joinButton = (
|
||||
<button
|
||||
onClick={this.handleJoin.bind(this, channel)}
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_channels.join'
|
||||
defaultMessage='Join'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='more-modal__row'
|
||||
key={channel.id}
|
||||
>
|
||||
<div className='more-modal__details'>
|
||||
<p className='more-modal__name'>{channel.display_name}</p>
|
||||
<p className='more-modal__description'>{channel.purpose}</p>
|
||||
</div>
|
||||
<div className='more-modal__actions'>
|
||||
{joinButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
nextPage(e) {
|
||||
e.preventDefault();
|
||||
this.setState({page: this.state.page + 1, nextDisabled: true});
|
||||
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT_MILLISECONDS);
|
||||
this.props.nextPage(this.state.page + 1);
|
||||
}
|
||||
|
||||
previousPage(e) {
|
||||
e.preventDefault();
|
||||
this.setState({page: this.state.page - 1});
|
||||
}
|
||||
|
||||
doSearch() {
|
||||
const term = this.refs.filter.value;
|
||||
this.props.search(term);
|
||||
if (term === '') {
|
||||
this.setState({page: 0});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const channels = this.props.channels;
|
||||
let listContent;
|
||||
let nextButton;
|
||||
let previousButton;
|
||||
|
||||
if (channels == null) {
|
||||
listContent = <LoadingScreen/>;
|
||||
} else if (channels.length === 0) {
|
||||
listContent = (
|
||||
<div className='no-channel-message'>
|
||||
<p className='primary-message'>
|
||||
<FormattedMessage
|
||||
id='more_channels.noMore'
|
||||
defaultMessage='No more channels to join'
|
||||
/>
|
||||
</p>
|
||||
{this.props.noResultsText}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const pageStart = this.state.page * this.props.channelsPerPage;
|
||||
const pageEnd = pageStart + this.props.channelsPerPage;
|
||||
const channelsToDisplay = this.props.channels.slice(pageStart, pageEnd);
|
||||
listContent = channelsToDisplay.map(this.createChannelRow);
|
||||
|
||||
if (channelsToDisplay.length >= this.props.channelsPerPage) {
|
||||
nextButton = (
|
||||
<button
|
||||
className='btn btn-default filter-control filter-control__next'
|
||||
onClick={this.nextPage}
|
||||
disabled={this.state.nextDisabled}
|
||||
>
|
||||
{'Next'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.page > 0) {
|
||||
previousButton = (
|
||||
<button
|
||||
className='btn btn-default filter-control filter-control__prev'
|
||||
onClick={this.previousPage}
|
||||
>
|
||||
{'Previous'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='filtered-user-list'>
|
||||
<div className='filter-row'>
|
||||
<div className='col-sm-6'>
|
||||
<input
|
||||
ref='filter'
|
||||
className='form-control filter-textbox'
|
||||
placeholder={localizeMessage('filtered_channels_list.search', 'Search channels')}
|
||||
onInput={this.doSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref='channelList'
|
||||
className='more-modal__list'
|
||||
>
|
||||
{listContent}
|
||||
</div>
|
||||
<div className='filter-controls'>
|
||||
{previousButton}
|
||||
{nextButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchableChannelList.defaultProps = {
|
||||
channels: []
|
||||
};
|
||||
|
||||
SearchableChannelList.propTypes = {
|
||||
channels: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
channelsPerPage: React.PropTypes.number,
|
||||
nextPage: React.PropTypes.func.isRequired,
|
||||
search: React.PropTypes.func.isRequired,
|
||||
handleJoin: React.PropTypes.func.isRequired,
|
||||
noResultsText: React.PropTypes.object
|
||||
};
|
||||
@@ -1,15 +1,16 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
const MaxChannelSuggestions = 40;
|
||||
import {autocompleteChannels} from 'actions/channel_actions.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
import {Constants, ActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class ChannelMentionSuggestion extends Suggestion {
|
||||
render() {
|
||||
@@ -48,84 +49,71 @@ class ChannelMentionSuggestion extends Suggestion {
|
||||
}
|
||||
}
|
||||
|
||||
function filterChannelsByPrefix(channels, prefix, limit) {
|
||||
const filtered = [];
|
||||
|
||||
for (const id of Object.keys(channels)) {
|
||||
if (filtered.length >= limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
const channel = channels[id];
|
||||
|
||||
if (channel.delete_at > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (channel.display_name.toLowerCase().startsWith(prefix) || channel.name.startsWith(prefix)) {
|
||||
filtered.push(channel);
|
||||
}
|
||||
export default class ChannelMentionProvider {
|
||||
constructor() {
|
||||
this.timeoutId = '';
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
export default class ChannelMentionProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/(^|\s)(~([^~]*))$/i).exec(pretext.toLowerCase());
|
||||
if (captured) {
|
||||
const prefix = captured[3];
|
||||
|
||||
const channels = ChannelStore.getAll();
|
||||
const moreChannels = ChannelStore.getMoreAll();
|
||||
function autocomplete() {
|
||||
autocompleteChannels(
|
||||
prefix,
|
||||
(data) => {
|
||||
const channels = data;
|
||||
|
||||
// Remove private channels from the list.
|
||||
const publicChannels = channels.filter((channel) => {
|
||||
return channel.type === 'O';
|
||||
});
|
||||
// Wrap channels in an outer object to avoid overwriting the 'type' property.
|
||||
const wrappedChannels = [];
|
||||
const wrappedMoreChannels = [];
|
||||
const moreChannels = [];
|
||||
channels.forEach((item) => {
|
||||
if (ChannelStore.get(item.id)) {
|
||||
wrappedChannels.push({
|
||||
type: Constants.MENTION_CHANNELS,
|
||||
channel: item
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter channels by prefix.
|
||||
const filteredChannels = filterChannelsByPrefix(
|
||||
publicChannels, prefix, MaxChannelSuggestions);
|
||||
const filteredMoreChannels = filterChannelsByPrefix(
|
||||
moreChannels, prefix, MaxChannelSuggestions - filteredChannels.length);
|
||||
wrappedMoreChannels.push({
|
||||
type: Constants.MENTION_MORE_CHANNELS,
|
||||
channel: item
|
||||
});
|
||||
|
||||
// Sort channels by display name.
|
||||
[filteredChannels, filteredMoreChannels].forEach((items) => {
|
||||
items.sort((a, b) => {
|
||||
const aPrefix = a.display_name.startsWith(prefix);
|
||||
const bPrefix = b.display_name.startsWith(prefix);
|
||||
moreChannels.push(item);
|
||||
});
|
||||
|
||||
if (aPrefix === bPrefix) {
|
||||
return a.display_name.localeCompare(b.display_name);
|
||||
} else if (aPrefix) {
|
||||
return -1;
|
||||
const wrapped = wrappedChannels.concat(wrappedMoreChannels);
|
||||
const mentions = wrapped.map((item) => '~' + item.channel.name);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_MORE_CHANNELS,
|
||||
channels: moreChannels
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
|
||||
id: suggestionId,
|
||||
matchedPretext: captured[2],
|
||||
terms: mentions,
|
||||
items: wrapped,
|
||||
component: ChannelMentionSuggestion
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Wrap channels in an outer object to avoid overwriting the 'type' property.
|
||||
const wrappedChannels = filteredChannels.map((item) => {
|
||||
return {
|
||||
type: Constants.MENTION_CHANNELS,
|
||||
channel: item
|
||||
};
|
||||
});
|
||||
const wrappedMoreChannels = filteredMoreChannels.map((item) => {
|
||||
return {
|
||||
type: Constants.MENTION_MORE_CHANNELS,
|
||||
channel: item
|
||||
};
|
||||
});
|
||||
|
||||
const wrapped = wrappedChannels.concat(wrappedMoreChannels);
|
||||
|
||||
const mentions = wrapped.map((item) => '~' + item.channel.name);
|
||||
|
||||
SuggestionStore.clearSuggestions(suggestionId);
|
||||
SuggestionStore.addSuggestions(suggestionId, mentions, wrapped, ChannelMentionSuggestion, captured[2]);
|
||||
this.timeoutId = setTimeout(
|
||||
autocomplete.bind(this),
|
||||
Constants.AUTOCOMPLETE_TIMEOUT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
import {autocompleteChannels} from 'actions/channel_actions.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
import {Constants, ActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SearchChannelSuggestion extends Suggestion {
|
||||
render() {
|
||||
@@ -30,37 +33,64 @@ class SearchChannelSuggestion extends Suggestion {
|
||||
}
|
||||
|
||||
export default class SearchChannelProvider {
|
||||
constructor() {
|
||||
this.timeoutId = '';
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext.toLowerCase());
|
||||
if (captured) {
|
||||
const channelPrefix = captured[1];
|
||||
|
||||
const channels = ChannelStore.getAll();
|
||||
const publicChannels = [];
|
||||
const privateChannels = [];
|
||||
function autocomplete() {
|
||||
autocompleteChannels(
|
||||
channelPrefix,
|
||||
(data) => {
|
||||
const publicChannels = data;
|
||||
|
||||
for (const id of Object.keys(channels)) {
|
||||
const channel = channels[id];
|
||||
const localChannels = ChannelStore.getAll();
|
||||
const privateChannels = [];
|
||||
|
||||
// don't show direct channels
|
||||
if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) {
|
||||
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
publicChannels.push(channel);
|
||||
} else {
|
||||
privateChannels.push(channel);
|
||||
for (const id of Object.keys(localChannels)) {
|
||||
const channel = localChannels[id];
|
||||
if (channel.name.startsWith(channelPrefix) && channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
privateChannels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredPublicChannels = [];
|
||||
publicChannels.forEach((item) => {
|
||||
if (item.name.startsWith(channelPrefix)) {
|
||||
filteredPublicChannels.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
filteredPublicChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const channels = filteredPublicChannels.concat(privateChannels);
|
||||
const channelNames = channels.map((channel) => channel.name);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
|
||||
id: suggestionId,
|
||||
matchedPretext: channelPrefix,
|
||||
terms: channelNames,
|
||||
items: channels,
|
||||
component: SearchChannelSuggestion
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
publicChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const publicChannelNames = publicChannels.map((channel) => channel.name);
|
||||
|
||||
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const privateChannelNames = privateChannels.map((channel) => channel.name);
|
||||
|
||||
SuggestionStore.clearSuggestions(suggestionId);
|
||||
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion, channelPrefix);
|
||||
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion, channelPrefix);
|
||||
this.timeoutId = setTimeout(
|
||||
autocomplete.bind(this),
|
||||
Constants.AUTOCOMPLETE_TIMEOUT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user