PLT-1759 - Auto-complete for !channels when posting messages. (#3890)
* Auto-complete for !channels when posting messages. This is part 1 of the fix for PLT-1759 to make channels linkable. Still to do: - Make the !channels clickable when they appear in messages. This is blocked until PR #3865 is resolved as it looks like that refactors some of the code that would be touched by making this change. - Unit tests. Again, I think the above referenced PR should be merged before tackling this. * Fix style problems. * Highlighting of !channel-names in messages. This only identifies the !channel-name (not the display name). The implementation of the auto-complete on channel names now needs to be modified to convert to the channel handle before sending the message. * Display !channel-name as !Display Name. When we encounter !channel-name in a message, display it as a link using the channel's actual name rather than it's handle (name). * Match on names and display name, and use name. * Autocomplete channels matching on both the name and the the display name. * Use the name as the text we fill in instead of the display name. It's potentially a bit ugly, but it minimises complexity for now as otherwise we'd have to do complicated things to the message box. * Fix style issues. * Load more channels everywhere. Whenever we load the list of channels, we should also load the list of more channels. This is to enable auto-completing and auto-linking of all channels whether or not the user is in them currently. * Include more channels in the map for linking. * Listen for channel list updates for autolinking. * Remove accidental console.log. * Autocomplete on more channels too. * i18n for channel autocomplete. * Link directly to channels in !channel mentions. This currently does not work if you aren't a member of that channel. Need to decide what the correct behaviour is in that case. * Fix style issues. * Show channel name and handle in suggestion. * Match channels only at start or after space. * Better matching in text-formatting. Only match channels after a space-type character or at the start in the posts list too. * Move the route construction to make tests work. Moves route-construction out of text_formatting.jsx and into utils.jsx so that the unit tests work once again.
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
781ff323db
Коммит
8443ca5828
@@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import EmojiStore from 'stores/emoji_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import {Preferences} from 'utils/constants.jsx';
|
||||
@@ -26,6 +27,7 @@ export default class PostMessageContainer extends React.Component {
|
||||
this.onEmojiChange = this.onEmojiChange.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
|
||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
||||
mentionKeys.push('@here');
|
||||
@@ -34,7 +36,8 @@ export default class PostMessageContainer extends React.Component {
|
||||
emojis: EmojiStore.getEmojis(),
|
||||
enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
|
||||
mentionKeys,
|
||||
usernameMap: UserStore.getProfilesUsernameMap()
|
||||
usernameMap: UserStore.getProfilesUsernameMap(),
|
||||
channelNamesMap: ChannelStore.getChannelNamesMap()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,12 +45,16 @@ export default class PostMessageContainer extends React.Component {
|
||||
EmojiStore.addChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
ChannelStore.addMoreChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EmojiStore.removeChangeListener(this.onEmojiChange);
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
ChannelStore.removeMoreChangeListener(this.onChannelChange);
|
||||
}
|
||||
|
||||
onEmojiChange() {
|
||||
@@ -72,6 +79,12 @@ export default class PostMessageContainer extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
this.setState({
|
||||
channelNamesMap: ChannelStore.getChannelNamesMap()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PostMessageView
|
||||
@@ -81,7 +94,8 @@ export default class PostMessageContainer extends React.Component {
|
||||
enableFormatting={this.state.enableFormatting}
|
||||
mentionKeys={this.state.mentionKeys}
|
||||
usernameMap={this.state.usernameMap}
|
||||
channelNamesMap={this.state.channelNamesMap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ export default class PostMessageView extends React.Component {
|
||||
emojis: React.PropTypes.object.isRequired,
|
||||
enableFormatting: React.PropTypes.bool.isRequired,
|
||||
mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
usernameMap: React.PropTypes.object.isRequired
|
||||
usernameMap: React.PropTypes.object.isRequired,
|
||||
channelNamesMap: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
@@ -40,6 +41,7 @@ export default class PostMessageView extends React.Component {
|
||||
|
||||
// Don't check if props.usernameMap changes since it is very large and inefficient to do so.
|
||||
// This mimics previous behaviour, but could be changed if we decide it's worth it.
|
||||
// The same choice (and reasoning) is also applied to the this.props.channelNamesMap.
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -53,7 +55,8 @@ export default class PostMessageView extends React.Component {
|
||||
emojis: this.props.emojis,
|
||||
siteURL: Utils.getSiteURL(),
|
||||
mentionKeys: this.props.mentionKeys,
|
||||
usernameMap: this.props.usernameMap
|
||||
usernameMap: this.props.usernameMap,
|
||||
channelNamesMap: this.props.channelNamesMap
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -63,4 +66,4 @@ export default class PostMessageView extends React.Component {
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
webapp/components/suggestion/channel_mention_provider.jsx
Обычный файл
130
webapp/components/suggestion/channel_mention_provider.jsx
Обычный файл
@@ -0,0 +1,130 @@
|
||||
// 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;
|
||||
|
||||
class ChannelMentionSuggestion extends Suggestion {
|
||||
render() {
|
||||
const isSelection = this.props.isSelection;
|
||||
const item = this.props.item;
|
||||
|
||||
const channelName = item.channel.display_name;
|
||||
let purpose = item.channel.purpose;
|
||||
|
||||
let className = 'mentions__name';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const description = '(!' + item.channel.name + ')';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className='mention__align'>
|
||||
<span>
|
||||
{channelName}
|
||||
</span>
|
||||
<span className='mention__channelname'>
|
||||
{' '}
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
<div className='mention__purpose'>
|
||||
{purpose}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.addSuggestions(suggestionId, mentions, wrapped, ChannelMentionSuggestion, captured[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import $ from 'jquery';
|
||||
import AtMentionProvider from './suggestion/at_mention_provider.jsx';
|
||||
import ChannelMentionProvider from './suggestion/channel_mention_provider.jsx';
|
||||
import CommandProvider from './suggestion/command_provider.jsx';
|
||||
import EmoticonProvider from './suggestion/emoticon_provider.jsx';
|
||||
import SuggestionList from './suggestion/suggestion_list.jsx';
|
||||
@@ -35,7 +36,7 @@ export default class Textbox extends React.Component {
|
||||
connection: ''
|
||||
};
|
||||
|
||||
this.suggestionProviders = [new AtMentionProvider(), new EmoticonProvider()];
|
||||
this.suggestionProviders = [new AtMentionProvider(), new ChannelMentionProvider(), new EmoticonProvider()];
|
||||
if (props.supportsCommands) {
|
||||
this.suggestionProviders.push(new CommandProvider());
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user