From 8443ca58289055cde25a3cdaaa3987c6f8cfde4a Mon Sep 17 00:00:00 2001
From: George Goldberg <george@gberg.me>
Date: Mon, 19 Sep 2016 13:21:22 +0100
Subject: 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.
---
 .../suggestion/channel_mention_provider.jsx        | 130 +++++++++++++++++++++
 1 file changed, 130 insertions(+)
 create mode 100644 webapp/components/suggestion/channel_mention_provider.jsx

(limited to 'webapp/components/suggestion/channel_mention_provider.jsx')

diff --git a/webapp/components/suggestion/channel_mention_provider.jsx b/webapp/components/suggestion/channel_mention_provider.jsx
new file mode 100644
index 000000000..9e8a7b47b
--- /dev/null
+++ b/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]);
+        }
+    }
+}
-- 
cgit v1.2.3-1-g7c22

