sidebar.jsx 34.8 KB
Newer Older
1
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2 3 4 5 6
// See License.txt for license information.

import $ from 'jquery';
import ReactDOM from 'react-dom';
import NewChannelFlow from './new_channel_flow.jsx';
7
import MoreDirectChannels from 'components/more_direct_channels';
8
import MoreChannels from 'components/more_channels';
9 10 11
import SidebarHeader from './sidebar_header.jsx';
import UnreadChannelIndicator from './unread_channel_indicator.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
12
import StatusIcon from './status_icon.jsx';
13 14 15 16 17

import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
18
import ModalStore from 'stores/modal_store.jsx';
19

20
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
21
import * as AsyncClient from 'utils/async_client.jsx';
22
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
23
import * as Utils from 'utils/utils.jsx';
24
import * as ChannelUtils from 'utils/channel_utils.jsx';
25
import * as ChannelActions from 'actions/channel_actions.jsx';
26

27
import {trackEvent} from 'actions/diagnostics_actions.jsx';
28
import {ActionTypes, Constants} from 'utils/constants.jsx';
29 30 31 32 33 34 35 36 37 38

import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';

const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;

import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import loadingGif from 'images/load.gif';

import React from 'react';
39
import {browserHistory, Link} from 'react-router/es6';
40 41 42 43

import favicon from 'images/favicon/favicon-16x16.png';
import redFavicon from 'images/favicon/redfavicon-16x16.png';

44
import store from 'stores/redux_store.jsx';
45 46
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getUsers} from 'mattermost-redux/selectors/entities/users';
47

48 49 50 51 52 53 54 55 56 57 58
export default class Sidebar extends React.Component {
    constructor(props) {
        super(props);

        this.badgesActive = false;
        this.firstUnreadChannel = null;
        this.lastUnreadChannel = null;

        this.getStateFromStores = this.getStateFromStores.bind(this);

        this.onChange = this.onChange.bind(this);
59 60
        this.onModalChange = this.onModalChange.bind(this);
        this.onInChannelChange = this.onInChannelChange.bind(this);
61 62 63 64 65
        this.onScroll = this.onScroll.bind(this);
        this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
        this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);

        this.showMoreChannelsModal = this.showMoreChannelsModal.bind(this);
66
        this.hideMoreChannelsModal = this.hideMoreChannelsModal.bind(this);
67 68 69 70
        this.showNewChannelModal = this.showNewChannelModal.bind(this);
        this.hideNewChannelModal = this.hideNewChannelModal.bind(this);
        this.showMoreDirectChannelsModal = this.showMoreDirectChannelsModal.bind(this);
        this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
71
        this.handleOpenMoreDirectChannelsModal = this.handleOpenMoreDirectChannelsModal.bind(this);
72 73 74 75

        this.createChannelElement = this.createChannelElement.bind(this);
        this.updateTitle = this.updateTitle.bind(this);

76 77 78 79 80
        this.navigateChannelShortcut = this.navigateChannelShortcut.bind(this);
        this.navigateUnreadChannelShortcut = this.navigateUnreadChannelShortcut.bind(this);
        this.getDisplayedChannels = this.getDisplayedChannels.bind(this);
        this.updateScrollbarOnChannelChange = this.updateScrollbarOnChannelChange.bind(this);

81
        this.isLeaving = new Map();
82
        this.isSwitchingChannel = false;
83
        this.closedDirectChannel = false;
84 85 86 87

        const state = this.getStateFromStores();
        state.newChannelModalType = '';
        state.showDirectChannelsModal = false;
88
        state.showMoreChannelsModal = false;
89
        state.loadingDMChannel = -1;
90
        state.inChannelChange = false;
91 92
        this.state = state;
    }
93

94
    getTotalUnreadCount() {
95 96
        const unreads = ChannelUtils.getCountsStateFromStores(this.state.currentTeam, this.state.teamMembers, this.state.unreadCounts);
        return {msgs: unreads.messageCount, mentions: unreads.mentionCount};
97
    }
98

99
    getStateFromStores() {
100
        const members = ChannelStore.getMyMembers();
enahum's avatar
enahum committed
101
        const teamMembers = TeamStore.getMyTeamMembers();
102 103
        const currentChannelId = ChannelStore.getCurrentId();
        const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
Evgeniy's avatar
Evgeniy committed
104

105
        const channels = ChannelStore.getAll();
106 107
        const preferences = getMyPreferences(store.getState());
        const profiles = getUsers(store.getState());
108
        let displayableChannels = {};
109 110 111
        if (!Utils.areObjectsEqual(channels, this.oldChannels) ||
                !Utils.areObjectsEqual(preferences, this.oldPreferences) ||
                !Utils.areObjectsEqual(profiles, this.oldProfiles)) {
112 113
            const channelsArray = channels.map((channel) => Object.assign({}, channel));
            displayableChannels = ChannelUtils.buildDisplayableChannelList(channelsArray);
114
            displayableChannels.favoriteChannels.sort(sortTeamsByDisplayName);
115 116
        }
        this.oldChannels = channels;
117
        this.oldPreferences = preferences;
118
        this.oldProfiles = profiles;
119 120 121 122

        return {
            activeId: currentChannelId,
            members,
enahum's avatar
enahum committed
123
            teamMembers,
124
            ...displayableChannels,
125 126 127
            unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
            showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
            currentTeam: TeamStore.getCurrent(),
128 129 130
            currentUser: UserStore.getCurrentUser(),
            townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL),
            offTopic: ChannelStore.getByName(Constants.OFFTOPIC_CHANNEL)
131 132 133
        };
    }

134 135 136 137
    onInChannelChange() {
        this.setState({inChannelChange: !this.state.inChannelChange});
    }

138 139 140
    componentDidMount() {
        ChannelStore.addChangeListener(this.onChange);
        UserStore.addChangeListener(this.onChange);
141
        UserStore.addInTeamChangeListener(this.onChange);
142
        UserStore.addInChannelChangeListener(this.onInChannelChange);
143 144 145
        UserStore.addStatusesChangeListener(this.onChange);
        TeamStore.addChangeListener(this.onChange);
        PreferenceStore.addChangeListener(this.onChange);
146
        ModalStore.addModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
147 148 149

        this.updateTitle();
        this.updateUnreadIndicators();
150 151 152

        document.addEventListener('keydown', this.navigateChannelShortcut);
        document.addEventListener('keydown', this.navigateUnreadChannelShortcut);
153
    }
154

155 156 157 158 159 160
    shouldComponentUpdate(nextProps, nextState) {
        if (!Utils.areObjectsEqual(nextState, this.state)) {
            return true;
        }
        return false;
    }
161

162
    componentDidUpdate(prevProps, prevState) {
163 164
        this.updateTitle();
        this.updateUnreadIndicators();
Asaad Mahmood's avatar
Asaad Mahmood committed
165 166 167
        if (!Utils.isMobile()) {
            $('.sidebar--left .nav-pills__container').perfectScrollbar();
        }
168

David Lu's avatar
David Lu committed
169 170 171 172 173 174
        // reset the scrollbar upon switching teams
        if (this.state.currentTeam !== prevState.currentTeam) {
            this.refs.container.scrollTop = 0;
            $('.nav-pills__container').perfectScrollbar('update');
        }

175
        // close the LHS on mobile when you change channels
176
        if (this.state.activeId !== prevState.activeId) {
177 178 179 180 181 182 183
            if (this.closedDirectChannel) {
                this.closedDirectChannel = false;
            } else {
                $('.app__body .inner-wrap').removeClass('move--right');
                $('.app__body .sidebar--left').removeClass('move--right');
                $('.multi-teams .team-sidebar').removeClass('move--right');
            }
184
        }
185
    }
186

187 188 189
    componentWillUnmount() {
        ChannelStore.removeChangeListener(this.onChange);
        UserStore.removeChangeListener(this.onChange);
190
        UserStore.removeInTeamChangeListener(this.onChange);
191
        UserStore.removeInChannelChangeListener(this.onChange);
192 193 194
        UserStore.removeStatusesChangeListener(this.onChange);
        TeamStore.removeChangeListener(this.onChange);
        PreferenceStore.removeChangeListener(this.onChange);
195
        ModalStore.removeModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
196 197
        document.removeEventListener('keydown', this.navigateChannelShortcut);
        document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
198
    }
199

200 201 202 203
    onModalChange(value, args) {
        this.showMoreDirectChannelsModal(args.startingUsers);
    }

204 205 206 207 208
    handleOpenMoreDirectChannelsModal(e) {
        e.preventDefault();
        this.showMoreDirectChannelsModal();
    }

209
    onChange() {
210 211 212
        if (this.state.currentTeam.id !== TeamStore.getCurrentId()) {
            ChannelStore.clear();
        }
213
        this.setState(this.getStateFromStores());
enahum's avatar
enahum committed
214
        this.updateTitle();
215
    }
216

217 218 219 220 221 222 223 224 225
    updateTitle() {
        const channel = ChannelStore.getCurrent();
        if (channel && this.state.currentTeam) {
            let currentSiteName = '';
            if (global.window.mm_config.SiteName != null) {
                currentSiteName = global.window.mm_config.SiteName;
            }

            let currentChannelName = channel.display_name;
226
            if (channel.type === Constants.DM_CHANNEL) {
227 228 229 230
                const teammate = Utils.getDirectTeammate(channel.id);
                if (teammate != null) {
                    currentChannelName = teammate.username;
                }
231 232
            } else if (channel.type === Constants.GM_CHANNEL) {
                currentChannelName = ChannelUtils.buildGroupChannelName(channel.id);
233 234 235 236 237 238 239 240
            }

            const unread = this.getTotalUnreadCount();
            const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
            const unreadTitle = unread.msgs > 0 ? '* ' : '';
            document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName;
        }
    }
241

242 243 244
    onScroll() {
        this.updateUnreadIndicators();
    }
245

246 247 248 249 250 251
    updateUnreadIndicators() {
        const container = $(ReactDOM.findDOMNode(this.refs.container));

        var showTopUnread = false;
        var showBottomUnread = false;

252 253 254
        // Consider partially obscured channels as above/below
        const unreadMargin = 15;

255 256 257
        if (this.firstUnreadChannel) {
            var firstUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.firstUnreadChannel]));

258
            if (firstUnreadElement.position().top + firstUnreadElement.height() < unreadMargin) {
259 260 261 262 263 264 265
                showTopUnread = true;
            }
        }

        if (this.lastUnreadChannel) {
            var lastUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.lastUnreadChannel]));

266
            if (lastUnreadElement.position().top > container.height() - unreadMargin) {
267 268 269 270 271 272 273 274 275
                showBottomUnread = true;
            }
        }

        this.setState({
            showTopUnread,
            showBottomUnread
        });
    }
276

277 278 279 280 281 282 283
    updateScrollbarOnChannelChange(channel) {
        const curChannel = this.refs[channel.name].getBoundingClientRect();
        if ((curChannel.top - Constants.CHANNEL_SCROLL_ADJUSTMENT < 0) || (curChannel.top + curChannel.height > this.refs.container.getBoundingClientRect().height)) {
            this.refs.container.scrollTop = this.refs.container.scrollTop + (curChannel.top - Constants.CHANNEL_SCROLL_ADJUSTMENT);
            $('.nav-pills__container').perfectScrollbar('update');
        }
    }
284

285 286 287
    navigateChannelShortcut(e) {
        if (e.altKey && !e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
            e.preventDefault();
288 289 290 291 292 293

            if (this.isSwitchingChannel) {
                return;
            }

            this.isSwitchingChannel = true;
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
            const allChannels = this.getDisplayedChannels();
            const curChannelId = this.state.activeId;
            let curIndex = -1;
            for (let i = 0; i < allChannels.length; i++) {
                if (allChannels[i].id === curChannelId) {
                    curIndex = i;
                }
            }
            let nextChannel = allChannels[curIndex];
            let nextIndex = curIndex;
            if (e.keyCode === Constants.KeyCodes.DOWN) {
                nextIndex = curIndex + 1;
            } else if (e.keyCode === Constants.KeyCodes.UP) {
                nextIndex = curIndex - 1;
            }
            nextChannel = allChannels[Utils.mod(nextIndex, allChannels.length)];
            ChannelActions.goToChannel(nextChannel);
            this.updateScrollbarOnChannelChange(nextChannel);
312
            this.isSwitchingChannel = false;
313 314
        } else if (Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.keyCode === Constants.KeyCodes.K) {
            this.handleOpenMoreDirectChannelsModal(e);
315 316
        }
    }
317

318 319 320
    navigateUnreadChannelShortcut(e) {
        if (e.altKey && e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
            e.preventDefault();
321 322 323 324 325 326

            if (this.isSwitchingChannel) {
                return;
            }

            this.isSwitchingChannel = true;
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
            const allChannels = this.getDisplayedChannels();
            const curChannelId = this.state.activeId;
            let curIndex = -1;
            for (let i = 0; i < allChannels.length; i++) {
                if (allChannels[i].id === curChannelId) {
                    curIndex = i;
                }
            }
            let nextChannel = allChannels[curIndex];
            let nextIndex = curIndex;
            let count = 0;
            let increment = 0;
            if (e.keyCode === Constants.KeyCodes.UP) {
                increment = -1;
            } else if (e.keyCode === Constants.KeyCodes.DOWN) {
                increment = 1;
            }
            let unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
            while (count < allChannels.length && unreadCounts.msgs === 0 && unreadCounts.mentions === 0) {
                nextIndex += increment;
                count++;
                nextIndex = Utils.mod(nextIndex, allChannels.length);
                unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
            }
            if (unreadCounts.msgs !== 0 || unreadCounts.mentions !== 0) {
                nextChannel = allChannels[nextIndex];
                ChannelActions.goToChannel(nextChannel);
                this.updateScrollbarOnChannelChange(nextChannel);
            }
356
            this.isSwitchingChannel = false;
357 358
        }
    }
359

360
    getDisplayedChannels() {
361
        return this.state.favoriteChannels.concat(this.state.publicChannels).concat(this.state.privateChannels).concat(this.state.directAndGroupChannels);
362
    }
363

364 365 366
    handleLeaveDirectChannel(e, channel) {
        e.preventDefault();

367 368 369
        if (!this.isLeaving.get(channel.id)) {
            this.isLeaving.set(channel.id, true);

370 371 372 373 374 375 376 377 378 379
            let id;
            let category;
            if (channel.type === Constants.DM_CHANNEL) {
                id = channel.teammate_id;
                category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
            } else {
                id = channel.id;
                category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
            }

380
            AsyncClient.savePreference(
381 382
                category,
                id,
383
                'false',
384 385 386 387 388 389 390 391
                () => {
                    this.isLeaving.set(channel.id, false);
                },
                () => {
                    this.isLeaving.set(channel.id, false);
                }
            );

392 393 394 395
            if (ChannelUtils.isFavoriteChannel(channel)) {
                ChannelActions.unmarkFavorite(channel.id);
            }

396 397 398 399
            this.setState(this.getStateFromStores());
        }

        if (channel.id === this.state.activeId) {
400
            this.closedDirectChannel = true;
401
            browserHistory.push('/' + this.state.currentTeam.name + '/channels/town-square');
402 403 404 405
        }
    }

    showMoreChannelsModal() {
406
        this.setState({showMoreChannelsModal: true});
407
        trackEvent('ui', 'ui_channels_more_public');
408 409 410 411
    }

    hideMoreChannelsModal() {
        this.setState({showMoreChannelsModal: false});
412 413 414 415 416
    }

    showNewChannelModal(type) {
        this.setState({newChannelModalType: type});
    }
417

418 419 420 421
    hideNewChannelModal() {
        this.setState({newChannelModalType: ''});
    }

422
    showMoreDirectChannelsModal(startingUsers) {
423
        trackEvent('ui', 'ui_channels_more_direct');
424
        this.setState({showDirectChannelsModal: true, startingUsers});
425
    }
426

427
    hideMoreDirectChannelsModal() {
428
        this.setState({showDirectChannelsModal: false, startingUsers: null});
429 430
    }

431 432 433 434 435 436 437 438 439
    openLeftSidebar() {
        if (Utils.isMobile()) {
            setTimeout(() => {
                document.querySelector('.app__body .inner-wrap').classList.add('move--right');
                document.querySelector('.app__body .sidebar--left').classList.add('move--right');
            });
        }
    }

440 441 442 443 444 445 446
    openQuickSwitcher(e) {
        e.preventDefault();
        AppDispatcher.handleViewAction({
            type: ActionTypes.TOGGLE_QUICK_SWITCH_MODAL
        });
    }

447 448 449
    createTutorialTip() {
        const screens = [];

450 451 452 453 454 455 456 457 458 459
        let townSquareDisplayName = Constants.DEFAULT_CHANNEL_UI_NAME;
        if (this.state.townSquare) {
            townSquareDisplayName = this.state.townSquare.display_name;
        }

        let offTopicDisplayName = Constants.OFFTOPIC_CHANNEL_UI_NAME;
        if (this.state.offTopic) {
            offTopicDisplayName = this.state.offTopic.display_name;
        }

460 461 462 463
        screens.push(
            <div>
                <FormattedHTMLMessage
                    id='sidebar.tutorialScreen1'
464
                    defaultMessage='<h4>Channels</h4><p><strong>Channels</strong> organize conversations across different topics. They’re open to everyone on your team. To send private communications use <strong>Direct Messages</strong> for a single person or <strong>Private Channels</strong> for multiple people.</p>'
465 466 467 468 469 470 471 472
                />
            </div>
        );

        screens.push(
            <div>
                <FormattedHTMLMessage
                    id='sidebar.tutorialScreen2'
473
                    defaultMessage='<h4>"{townsquare}" and "{offtopic}" channels</h4>
474
                    <p>Here are two public channels to start:</p>
475 476 477 478 479 480
                    <p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p>
                    <p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>'
                    values={{
                        townsquare: townSquareDisplayName,
                        offtopic: offTopicDisplayName
                    }}
481 482 483 484 485 486 487 488 489 490
                />
            </div>
        );

        screens.push(
            <div>
                <FormattedHTMLMessage
                    id='sidebar.tutorialScreen3'
                    defaultMessage='<h4>Creating and Joining Channels</h4>
                    <p>Click <strong>"More..."</strong> to create a new channel or join an existing one.</p>
491
                    <p>You can also create a new public or private channel by clicking the <strong>"+" symbol</strong> next to the public or private channel header.</p>'
492 493 494 495 496 497 498 499 500
                />
            </div>
        );

        return (
            <TutorialTip
                placement='right'
                screens={screens}
                overlayClass='tip-overlay--sidebar'
501
                diagnosticsTag='tutorial_tip_2_channels'
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
            />
        );
    }

    createChannelElement(channel, index, arr, handleClose) {
        const members = this.state.members;
        const activeId = this.state.activeId;
        const channelMember = members[channel.id];
        const unreadCount = this.state.unreadCounts[channel.id] || {msgs: 0, mentions: 0};
        let msgCount;

        let linkClass = '';
        if (channel.id === activeId) {
            linkClass = 'active';
        }

        let rowClass = 'sidebar-channel';

        var unread = false;
        if (channelMember) {
            msgCount = unreadCount.msgs + unreadCount.mentions;
            unread = msgCount > 0 || channelMember.mention_count > 0;
        }

        if (unread) {
            rowClass += ' unread-title';

            if (channel.id !== activeId) {
                if (!this.firstUnreadChannel) {
                    this.firstUnreadChannel = channel.name;
                }
                this.lastUnreadChannel = channel.name;
            }
        }

        var badge = null;
        if (channelMember) {
            if (unreadCount.mentions) {
540
                badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
541 542
                this.badgesActive = true;
            }
543
        } else if (this.state.loadingDMChannel === index && channel.type === Constants.DM_CHANNEL) {
544 545 546 547 548 549 550 551 552 553 554 555
            badge = (
                <img
                    className='channel-loading-gif pull-right'
                    src={loadingGif}
                />
            );
        }

        if (msgCount > 0) {
            rowClass += ' has-badge';
        }

556 557
        let displayName = channel.display_name;

558
        var icon = null;
559
        if (channel.type === Constants.OPEN_CHANNEL) {
Christopher Speller's avatar
Christopher Speller committed
560
            icon = <div className='status'><i className='fa fa-globe'/></div>;
561
        } else if (channel.type === Constants.PRIVATE_CHANNEL) {
Christopher Speller's avatar
Christopher Speller committed
562
            icon = <div className='status'><i className='fa fa-lock'/></div>;
563 564 565
        } else if (channel.type === Constants.GM_CHANNEL) {
            displayName = ChannelUtils.buildGroupChannelName(channel.id);
            icon = <div className='status status--group'>{UserStore.getProfileListInChannel(channel.id, true).length}</div>;
566 567
        } else {
            // set up status icon for direct message channels (status is null for other channel types)
568 569 570 571 572
            icon = (
                <StatusIcon
                    type='avatar'
                    status={channel.status}
                />);
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
        }

        let closeButton = null;
        const removeTooltip = (
            <Tooltip id='remove-dm-tooltip'>
                <FormattedMessage
                    id='sidebar.removeList'
                    defaultMessage='Remove from list'
                />
            </Tooltip>
        );
        if (handleClose && !badge) {
            closeButton = (
                <OverlayTrigger
                    delayShow={1000}
                    placement='top'
                    overlay={removeTooltip}
                >
591
                    <span
592
                        onClick={(e) => handleClose(e, channel)}
593 594 595 596
                        className='btn-close'
                    >
                        {'×'}
                    </span>
597 598 599 600 601 602 603 604 605
                </OverlayTrigger>
            );

            rowClass += ' has-close';
        }

        let tutorialTip = null;
        if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) {
            tutorialTip = this.createTutorialTip();
606
            this.openLeftSidebar();
607 608
        }

609 610
        let link = '';
        if (channel.fake) {
Christopher Speller's avatar
Christopher Speller committed
611
            link = '/' + this.state.currentTeam.name + '/channels/' + channel.name + '?fakechannel=' + encodeURIComponent(JSON.stringify(channel));
612
        } else {
Christopher Speller's avatar
Christopher Speller committed
613
            link = '/' + this.state.currentTeam.name + '/channels/' + channel.name;
614 615
        }

616 617 618 619 620 621
        return (
            <li
                key={channel.name}
                ref={channel.name}
                className={linkClass}
            >
622 623
                <Link
                    to={link}
624
                    className={rowClass}
625
                    onClick={this.trackChannelSelectedEvent}
626 627
                >
                    {icon}
628
                    {displayName}
629 630
                    {badge}
                    {closeButton}
631
                </Link>
632 633 634 635
                {tutorialTip}
            </li>
        );
    }
636

637 638 639 640
    trackChannelSelectedEvent() {
        trackEvent('ui', 'ui_channel_selected');
    }

641 642 643 644 645 646
    render() {
        // Check if we have all info needed to render
        if (this.state.currentTeam == null || this.state.currentUser == null) {
            return (<div/>);
        }

647
        this.lastBadgesActive = this.badgesActive;
648 649 650 651 652 653
        this.badgesActive = false;

        // keep track of the first and last unread channels so we can use them to set the unread indicators
        this.firstUnreadChannel = null;
        this.lastUnreadChannel = null;

654
        // create elements for all 4 types of channels
655 656 657 658 659
        const favoriteItems = this.state.favoriteChannels.
            map((channel, index, arr) => {
                if (channel.type === Constants.DM_CHANNEL) {
                    return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
                }
660

661 662
                return this.createChannelElement(channel);
            });
663

664 665 666 667
        const publicChannelItems = this.state.publicChannels.map(this.createChannelElement);

        const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);

668
        const directMessageItems = this.state.directAndGroupChannels.map((channel, index, arr) => {
669 670 671 672
            return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
        });

        // update the favicon to show if there are any notifications
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
        if (this.lastBadgesActive !== this.badgesActive) {
            var link = document.createElement('link');
            link.type = 'image/x-icon';
            link.rel = 'shortcut icon';
            link.id = 'favicon';
            if (this.badgesActive) {
                link.href = redFavicon;
            } else {
                link.href = favicon;
            }
            var head = document.getElementsByTagName('head')[0];
            var oldLink = document.getElementById('favicon');
            if (oldLink) {
                head.removeChild(oldLink);
            }
            head.appendChild(link);
689 690
        }

691 692 693 694
        var directMessageMore = (
            <li key='more'>
                <a
                    href='#'
695
                    onClick={this.handleOpenMoreDirectChannelsModal}
696 697
                >
                    <FormattedMessage
698 699
                        id='sidebar.moreElips'
                        defaultMessage='More...'
700 701 702 703
                    />
                </a>
            </li>
        );
704 705 706 707 708 709 710 711 712 713

        let showChannelModal = false;
        if (this.state.newChannelModalType !== '') {
            showChannelModal = true;
        }

        const createChannelTootlip = (
            <Tooltip id='new-channel-tooltip' >
                <FormattedMessage
                    id='sidebar.createChannel'
714
                    defaultMessage='Create new public channel'
715 716 717 718 719 720 721
                />
            </Tooltip>
        );
        const createGroupTootlip = (
            <Tooltip id='new-group-tooltip'>
                <FormattedMessage
                    id='sidebar.createGroup'
722
                    defaultMessage='Create new private channel'
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
                />
            </Tooltip>
        );

        const above = (
            <FormattedMessage
                id='sidebar.unreadAbove'
                defaultMessage='Unread post(s) above'
            />
        );

        const below = (
            <FormattedMessage
                id='sidebar.unreadBelow'
                defaultMessage='Unread post(s) below'
            />
        );

741 742 743 744 745 746
        const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
        const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();

        let createPublicChannelIcon = (
            <OverlayTrigger
                delayShow={500}
Asaad Mahmood's avatar
Asaad Mahmood committed
747
                trigger='hover'
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764
                placement='top'
                overlay={createChannelTootlip}
            >
                <a
                    className='add-channel-btn'
                    href='#'
                    onClick={this.showNewChannelModal.bind(this, Constants.OPEN_CHANNEL)}
                >
                    {'+'}
                </a>
            </OverlayTrigger>
        );

        let createPrivateChannelIcon = (
            <OverlayTrigger
                delayShow={500}
                placement='top'
Asaad Mahmood's avatar
Asaad Mahmood committed
765
                trigger='hover'
766 767 768 769 770 771 772 773 774 775 776 777
                overlay={createGroupTootlip}
            >
                <a
                    className='add-channel-btn'
                    href='#'
                    onClick={this.showNewChannelModal.bind(this, Constants.PRIVATE_CHANNEL)}
                >
                    {'+'}
                </a>
            </OverlayTrigger>
        );

778 779 780
        if (!ChannelUtils.showCreateOption(Constants.OPEN_CHANNEL, isAdmin, isSystemAdmin)) {
            createPublicChannelIcon = null;
        }
781

782 783
        if (!ChannelUtils.showCreateOption(Constants.PRIVATE_CHANNEL, isAdmin, isSystemAdmin)) {
            createPrivateChannelIcon = null;
784 785
        }

786 787 788 789 790
        let moreDirectChannelsModal;
        if (this.state.showDirectChannelsModal) {
            moreDirectChannelsModal = (
                <MoreDirectChannels
                    onModalDismissed={this.hideMoreDirectChannelsModal}
791
                    startingUsers={this.state.startingUsers}
792 793 794 795
                />
            );
        }

796 797 798 799 800 801 802 803 804 805 806 807 808
        let moreChannelsModal;
        if (this.state.showMoreChannelsModal) {
            moreChannelsModal = (
                <MoreChannels
                    onModalDismissed={this.hideMoreChannelsModal}
                    handleNewChannel={() => {
                        this.hideMoreChannelsModal();
                        this.showNewChannelModal(Constants.OPEN_CHANNEL);
                    }}
                />
            );
        }

809 810 811 812
        const quickSwitchText = 'channel_switch_modal.title';

        let quickSwitchTextShortcut = 'quick_switch_modal.channelsShortcut.windows';
        let quickSwitchDefault = '- CTRL+K';
813
        if (Utils.isMac()) {
814 815
            quickSwitchTextShortcut = 'quick_switch_modal.channelsShortcut.mac';
            quickSwitchDefault = '- ⌘K';
816 817
        }

818 819 820 821
        return (
            <div
                className='sidebar--left'
                id='sidebar-left'
822
                key='sidebar-left'
823 824 825 826 827 828
            >
                <NewChannelFlow
                    show={showChannelModal}
                    channelType={this.state.newChannelModalType}
                    onModalDismissed={this.hideNewChannelModal}
                />
829
                {moreDirectChannelsModal}
830
                {moreChannelsModal}
831 832 833

                <SidebarHeader
                    teamDisplayName={this.state.currentTeam.display_name}
834
                    teamDescription={this.state.currentTeam.description}
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
                    teamName={this.state.currentTeam.name}
                    teamType={this.state.currentTeam.type}
                    currentUser={this.state.currentUser}
                />

                <UnreadChannelIndicator
                    show={this.state.showTopUnread}
                    extraClass='nav-pills__unread-indicator-top'
                    text={above}
                />
                <UnreadChannelIndicator
                    show={this.state.showBottomUnread}
                    extraClass='nav-pills__unread-indicator-bottom'
                    text={below}
                />

                <div
                    ref='container'
                    className='nav-pills__container'
                    onScroll={this.onScroll}
                >
856 857 858 859 860 861 862 863 864 865 866
                    {favoriteItems.length !== 0 && <ul className='nav nav-pills nav-stacked'>
                        <li>
                            <h4>
                                <FormattedMessage
                                    id='sidebar.favorite'
                                    defaultMessage='Favorites'
                                />
                            </h4>
                        </li>
                        {favoriteItems}
                    </ul>}
867 868 869 870 871 872 873
                    <ul className='nav nav-pills nav-stacked'>
                        <li>
                            <h4>
                                <FormattedMessage
                                    id='sidebar.channels'
                                    defaultMessage='Channels'
                                />
874
                                {createPublicChannelIcon}
875 876 877 878 879
                            </h4>
                        </li>
                        {publicChannelItems}
                        <li>
                            <a
880
                                id='sidebarChannelsMore'
881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897
                                href='#'
                                className='nav-more'
                                onClick={this.showMoreChannelsModal}
                            >
                                <FormattedMessage
                                    id='sidebar.moreElips'
                                    defaultMessage='More...'
                                />
                            </a>
                        </li>
                    </ul>

                    <ul className='nav nav-pills nav-stacked'>
                        <li>
                            <h4>
                                <FormattedMessage
                                    id='sidebar.pg'
898
                                    defaultMessage='Private Channels'
899
                                />
900
                                {createPrivateChannelIcon}
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
                            </h4>
                        </li>
                        {privateChannelItems}
                    </ul>
                    <ul className='nav nav-pills nav-stacked'>
                        <li>
                            <h4>
                                <FormattedMessage
                                    id='sidebar.direct'
                                    defaultMessage='Direct Messages'
                                />
                            </h4>
                        </li>
                        {directMessageItems}
                        {directMessageMore}
                    </ul>
                </div>
918 919 920
                <div className='sidebar__switcher'>
                    <button
                        className='btn btn-link'
921 922 923 924
                        onClick={this.openQuickSwitcher}
                    >
                        <FormattedMessage
                            id={quickSwitchText}
925
                            defaultMessage='Switch Channels'
926
                        />
927 928 929 930 931 932 933
                        <span className='switch__shortcut'>
                            <FormattedMessage
                                id={quickSwitchTextShortcut}
                                defaultMessage={quickSwitchDefault}
                            />
                        </span>
                    </button>
934
                </div>
935 936 937 938
            </div>
        );
    }
}