summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2016-11-24 09:35:09 -0500
committerHarrison Healey <harrisonmhealey@gmail.com>2016-11-24 09:35:09 -0500
commit981ea33b8e10456bc279f36235c814305d01b243 (patch)
tree00fb6119d9ef16f60d4c0dbdaad1bd6dfbc347ed /webapp
parentc96ecae6da31aceabf29586cde872876b81d11d9 (diff)
downloadchat-981ea33b8e10456bc279f36235c814305d01b243.tar.gz
chat-981ea33b8e10456bc279f36235c814305d01b243.tar.bz2
chat-981ea33b8e10456bc279f36235c814305d01b243.zip
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
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/channel_actions.jsx52
-rw-r--r--webapp/actions/global_actions.jsx8
-rw-r--r--webapp/client/client.jsx29
-rw-r--r--webapp/components/filtered_channel_list.jsx180
-rw-r--r--webapp/components/more_channels.jsx100
-rw-r--r--webapp/components/searchable_channel_list.jsx205
-rw-r--r--webapp/components/suggestion/channel_mention_provider.jsx134
-rw-r--r--webapp/components/suggestion/search_channel_provider.jsx80
-rw-r--r--webapp/stores/channel_store.jsx16
-rw-r--r--webapp/tests/client_channel.test.jsx46
-rw-r--r--webapp/utils/async_client.jsx24
11 files changed, 542 insertions, 332 deletions
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index 5003d6530..ae32a481b 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -191,3 +191,55 @@ export function loadChannelsForCurrentUser() {
AsyncClient.getChannels();
AsyncClient.getMyChannelMembers();
}
+
+export function joinChannel(channel, success, error) {
+ Client.joinChannel(
+ channel.id,
+ () => {
+ ChannelStore.removeMoreChannel(channel.id);
+
+ if (success) {
+ success();
+ }
+ },
+ () => {
+ if (error) {
+ error();
+ }
+ }
+ );
+}
+
+export function searchMoreChannels(term, success, error) {
+ Client.searchMoreChannels(
+ term,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function autocompleteChannels(term, success, error) {
+ Client.autocompleteChannels(
+ term,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'autocompleteChannels');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index 9337595af..d743b787b 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -482,14 +482,6 @@ export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = tr
);
}
-export function emitJoinChannelEvent(channel, success, failure) {
- Client.joinChannel(
- channel.id,
- success,
- failure
- );
-}
-
export function emitSearchMentionsEvent(user) {
let terms = '';
if (user.notify_props && user.notify_props.mention_keys) {
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index a7d7a5c8a..38cc2f111 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1362,6 +1362,7 @@ export default class Client {
this.track('api', 'api_channel_get');
}
+ // SCHEDULED FOR DEPRECATION IN 3.7 - use getMoreChannelsPage instead
getMoreChannels(success, error) {
request.
get(`${this.getChannelsRoute()}/more`).
@@ -1371,6 +1372,34 @@ export default class Client {
end(this.handleResponse.bind(this, 'getMoreChannels', success, error));
}
+ getMoreChannelsPage(offset, limit, success, error) {
+ request.
+ get(`${this.getChannelsRoute()}/more/${offset}/${limit}`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getMoreChannelsPage', success, error));
+ }
+
+ searchMoreChannels(term, success, error) {
+ request.
+ post(`${this.getChannelsRoute()}/more/search`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send({term}).
+ end(this.handleResponse.bind(this, 'searchMoreChannels', success, error));
+ }
+
+ autocompleteChannels(term, success, error) {
+ request.
+ get(`${this.getChannelsRoute()}/autocomplete?term=${encodeURIComponent(term)}`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'autocompleteChannels', success, error));
+ }
+
getChannelCounts(success, error) {
request.
get(`${this.getChannelsRoute()}/counts`).
diff --git a/webapp/components/filtered_channel_list.jsx b/webapp/components/filtered_channel_list.jsx
deleted file mode 100644
index 64d033bc5..000000000
--- a/webapp/components/filtered_channel_list.jsx
+++ /dev/null
@@ -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
-};
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index e57c5d25f..fca13a7d0 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -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(),
+ onChange(force) {
+ if (this.state.search && !force) {
+ return;
+ }
+
+ this.setState({
+ channels: ChannelStore.getMoreChannelsList(),
serverError: null
- };
+ });
}
- onListenerChange() {
- const newState = this.getStateFromStores();
- if (!Utils.areObjectsEqual(newState.channels, this.state.channels)) {
- this.setState(newState);
- }
+ 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'>
diff --git a/webapp/components/searchable_channel_list.jsx b/webapp/components/searchable_channel_list.jsx
new file mode 100644
index 000000000..4a7f90455
--- /dev/null
+++ b/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
+};
diff --git a/webapp/components/suggestion/channel_mention_provider.jsx b/webapp/components/suggestion/channel_mention_provider.jsx
index d80433271..c644d1a9f 100644
--- a/webapp/components/suggestion/channel_mention_provider.jsx
+++ b/webapp/components/suggestion/channel_mention_provider.jsx
@@ -1,15 +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 SuggestionStore from 'stores/suggestion_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import Constants from 'utils/constants.jsx';
-import Suggestion from './suggestion.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import {Constants, ActionTypes} from 'utils/constants.jsx';
-const MaxChannelSuggestions = 40;
+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();
-
- // Remove private channels from the list.
- const publicChannels = channels.filter((channel) => {
- return channel.type === 'O';
- });
-
- // Filter channels by prefix.
- const filteredChannels = filterChannelsByPrefix(
- publicChannels, prefix, MaxChannelSuggestions);
- const filteredMoreChannels = filterChannelsByPrefix(
- moreChannels, prefix, MaxChannelSuggestions - filteredChannels.length);
-
- // 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);
-
- if (aPrefix === bPrefix) {
- return a.display_name.localeCompare(b.display_name);
- } else if (aPrefix) {
- return -1;
+ function autocomplete() {
+ autocompleteChannels(
+ prefix,
+ (data) => {
+ const channels = data;
+
+ // 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;
+ }
+
+ wrappedMoreChannels.push({
+ type: Constants.MENTION_MORE_CHANNELS,
+ channel: item
+ });
+
+ moreChannels.push(item);
+ });
+
+ 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
+ );
}
}
}
diff --git a/webapp/components/suggestion/search_channel_provider.jsx b/webapp/components/suggestion/search_channel_provider.jsx
index 0f07b6e29..66011af9f 100644
--- a/webapp/components/suggestion/search_channel_provider.jsx
+++ b/webapp/components/suggestion/search_channel_provider.jsx
@@ -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);
+ }
+ }
- publicChannels.sort((a, b) => a.name.localeCompare(b.name));
- const publicChannelNames = publicChannels.map((channel) => channel.name);
+ const filteredPublicChannels = [];
+ publicChannels.forEach((item) => {
+ if (item.name.startsWith(channelPrefix)) {
+ filteredPublicChannels.push(item);
+ }
+ });
- privateChannels.sort((a, b) => a.name.localeCompare(b.name));
- const privateChannelNames = privateChannels.map((channel) => channel.name);
+ 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
+ });
+ }
+ );
+ }
- 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
+ );
}
}
}
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index d578a5d29..0e2c43a60 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -234,11 +234,23 @@ class ChannelStoreClass extends EventEmitter {
}
storeMoreChannels(channels) {
- this.moreChannels = channels;
+ const newChannels = {};
+ for (let i = 0; i < channels.length; i++) {
+ newChannels[channels[i].id] = channels[i];
+ }
+ this.moreChannels = Object.assign({}, this.moreChannels, newChannels);
+ }
+
+ removeMoreChannel(channelId) {
+ Reflect.deleteProperty(this.moreChannels, channelId);
}
getMoreChannels() {
- return this.moreChannels;
+ return Object.assign({}, this.moreChannels);
+ }
+
+ getMoreChannelsList() {
+ return Object.keys(this.moreChannels).map((cid) => this.moreChannels[cid]);
}
storeStats(stats) {
diff --git a/webapp/tests/client_channel.test.jsx b/webapp/tests/client_channel.test.jsx
index b7fa57dc8..e8466021f 100644
--- a/webapp/tests/client_channel.test.jsx
+++ b/webapp/tests/client_channel.test.jsx
@@ -271,6 +271,52 @@ describe('Client.Channels', function() {
});
});
+ it('getMoreChannelsPage', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().getMoreChannelsPage(
+ 0,
+ 100,
+ function(data) {
+ assert.equal(data.length, 0);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
+ it('searchMoreChannels', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().searchMoreChannels(
+ 'blargh',
+ function(data) {
+ assert.equal(data.length, 0);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
+ it('autocompleteChannels', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().autocompleteChannels(
+ TestHelper.basicChannel().name,
+ function(data) {
+ assert.equal(data != null, true);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
it('getChannelCounts', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getChannelCounts(
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 71fbc8db0..d41b2ddf7 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -239,6 +239,30 @@ export function getMoreChannels(force) {
}
}
+export function getMoreChannelsPage(offset, limit) {
+ if (isCallInProgress('getMoreChannelsPage')) {
+ return;
+ }
+
+ callTracker.getMoreChannelsPage = utils.getTimestamp();
+ Client.getMoreChannelsPage(
+ offset,
+ limit,
+ (data) => {
+ callTracker.getMoreChannelsPage = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MORE_CHANNELS,
+ channels: data
+ });
+ },
+ (err) => {
+ callTracker.getMoreChannelsPage = 0;
+ dispatchError(err, 'getMoreChannelsPage');
+ }
+ );
+}
+
export function getChannelStats(channelId = ChannelStore.getCurrentId(), doVersionCheck = false) {
if (isCallInProgress('getChannelStats' + channelId) || channelId == null) {
return;