// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import $ from 'jquery'; import ReactDOM from 'react-dom'; import NewChannelFlow from './new_channel_flow.jsx'; import MoreDirectChannels from './more_direct_channels.jsx'; import SidebarHeader from './sidebar_header.jsx'; import UnreadChannelIndicator from './unread_channel_indicator.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; import StatusIcon from './status_icon.jsx'; 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'; import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; import Constants from 'utils/constants.jsx'; 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'; import {browserHistory, Link} from 'react-router/es6'; import favicon from 'images/favicon/favicon-16x16.png'; import redFavicon from 'images/favicon/redfavicon-16x16.png'; 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); this.onScroll = this.onScroll.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); this.showMoreChannelsModal = this.showMoreChannelsModal.bind(this); this.showNewChannelModal = this.showNewChannelModal.bind(this); this.hideNewChannelModal = this.hideNewChannelModal.bind(this); this.showMoreDirectChannelsModal = this.showMoreDirectChannelsModal.bind(this); this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this); this.createChannelElement = this.createChannelElement.bind(this); this.updateTitle = this.updateTitle.bind(this); this.navigateChannelShortcut = this.navigateChannelShortcut.bind(this); this.navigateUnreadChannelShortcut = this.navigateUnreadChannelShortcut.bind(this); this.getDisplayedChannels = this.getDisplayedChannels.bind(this); this.updateScrollbarOnChannelChange = this.updateScrollbarOnChannelChange.bind(this); this.isLeaving = new Map(); this.isSwitchingChannel = false; const state = this.getStateFromStores(); state.newChannelModalType = ''; state.showDirectChannelsModal = false; state.loadingDMChannel = -1; this.state = state; } getTotalUnreadCount() { let msgs = 0; let mentions = 0; const unreadCounts = this.state.unreadCounts; Object.keys(unreadCounts).forEach((chId) => { const channel = ChannelStore.get(chId); if (channel && (!channel.team_id || channel.team_id === this.state.currentTeam.id)) { msgs += unreadCounts[chId].msgs; mentions += unreadCounts[chId].mentions; } }); return {msgs, mentions}; } getStateFromStores() { const members = ChannelStore.getAllMembers(); const currentChannelId = ChannelStore.getCurrentId(); const currentUserId = UserStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); channels.sort(this.sortChannelsByDisplayName); const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); const directChannels = []; const directNonTeamChannels = []; for (const [name, value] of preferences) { if (value !== 'true') { continue; } const teammateId = name; let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId)); // a direct channel doesn't exist yet so create a fake one if (directChannel == null) { directChannel = { name: Utils.getDirectChannelName(currentUserId, teammateId), last_post_at: 0, total_msg_count: 0, type: Constants.DM_CHANNEL, fake: true }; } else { directChannel = JSON.parse(JSON.stringify(directChannel)); } directChannel.display_name = Utils.displayUsername(teammateId); directChannel.teammate_id = teammateId; directChannel.status = UserStore.getStatus(teammateId) || 'offline'; if (UserStore.hasTeamProfile(teammateId) && TeamStore.hasActiveMemberForTeam(teammateId)) { directChannels.push(directChannel); } else { directNonTeamChannels.push(directChannel); } } directChannels.sort(this.sortChannelsByDisplayName); directNonTeamChannels.sort(this.sortChannelsByDisplayName); const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); return { activeId: currentChannelId, members, publicChannels, privateChannels, directChannels, directNonTeamChannels, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, currentTeam: TeamStore.getCurrent(), currentUser: UserStore.getCurrentUser(), townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL), offTopic: ChannelStore.getByName(Constants.OFFTOPIC_CHANNEL) }; } componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); PreferenceStore.addChangeListener(this.onChange); this.updateTitle(); this.updateUnreadIndicators(); document.addEventListener('keydown', this.navigateChannelShortcut); document.addEventListener('keydown', this.navigateUnreadChannelShortcut); } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areObjectsEqual(nextState, this.state)) { return true; } return false; } componentDidUpdate(prevProps, prevState) { this.updateTitle(); this.updateUnreadIndicators(); if (!Utils.isMobile()) { $('.sidebar--left .nav-pills__container').perfectScrollbar(); } // reset the scrollbar upon switching teams if (this.state.currentTeam !== prevState.currentTeam) { this.refs.container.scrollTop = 0; $('.nav-pills__container').perfectScrollbar('update'); } // close the LHS on mobile when you change channels if (this.state.activeId !== prevState.activeId) { $('.app__body .inner-wrap').removeClass('move--right'); $('.app__body .sidebar--left').removeClass('move--right'); } } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); PreferenceStore.removeChangeListener(this.onChange); document.removeEventListener('keydown', this.navigateChannelShortcut); document.removeEventListener('keydown', this.navigateUnreadChannelShortcut); } onChange() { this.setState(this.getStateFromStores()); } 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; if (channel.type === 'D') { const teammate = Utils.getDirectTeammate(channel.id); if (teammate != null) { currentChannelName = teammate.username; } } 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; } } onScroll() { this.updateUnreadIndicators(); } updateUnreadIndicators() { const container = $(ReactDOM.findDOMNode(this.refs.container)); var showTopUnread = false; var showBottomUnread = false; if (this.firstUnreadChannel) { var firstUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.firstUnreadChannel])); if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) { showTopUnread = true; } } if (this.lastUnreadChannel) { var lastUnreadElement = $(ReactDOM.findDOMNode(this.refs[this.lastUnreadChannel])); if (lastUnreadElement.position().top > container.height()) { showBottomUnread = true; } } this.setState({ showTopUnread, showBottomUnread }); } 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'); } } navigateChannelShortcut(e) { if (e.altKey && !e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) { e.preventDefault(); if (this.isSwitchingChannel) { return; } this.isSwitchingChannel = true; 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); this.isSwitchingChannel = false; } } navigateUnreadChannelShortcut(e) { if (e.altKey && e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) { e.preventDefault(); if (this.isSwitchingChannel) { return; } this.isSwitchingChannel = true; 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); this.isSwitchingChannel = false; } } } getDisplayedChannels() { return this.state.publicChannels.concat(this.state.privateChannels).concat(this.state.directChannels).concat(this.state.directNonTeamChannels); } handleLeaveDirectChannel(e, channel) { e.preventDefault(); if (!this.isLeaving.get(channel.id)) { this.isLeaving.set(channel.id, true); AsyncClient.savePreference( Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false', () => { this.isLeaving.set(channel.id, false); }, () => { this.isLeaving.set(channel.id, false); } ); this.setState(this.getStateFromStores()); } if (channel.id === this.state.activeId) { browserHistory.push('/' + this.state.currentTeam.name + '/channels/town-square'); } } sortChannelsByDisplayName(a, b) { if (a.display_name === b.display_name) { return a.name.localeCompare(b.name); } return a.display_name.localeCompare(b.display_name); } showMoreChannelsModal() { // manually show the modal because using data-toggle messes with keyboard focus when the modal is dismissed $('#more_channels').modal({'data-channeltype': 'O'}).modal('show'); } showNewChannelModal(type) { this.setState({newChannelModalType: type}); } hideNewChannelModal() { this.setState({newChannelModalType: ''}); } showMoreDirectChannelsModal() { this.setState({showDirectChannelsModal: true}); } hideMoreDirectChannelsModal() { this.setState({showDirectChannelsModal: false}); } 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'); }); } } createTutorialTip() { const screens = []; 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; } screens.push(