// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. const NewChannelFlow = require('./new_channel_flow.jsx'); const MoreDirectChannels = require('./more_direct_channels.jsx'); const SearchBox = require('./search_bar.jsx'); const SidebarHeader = require('./sidebar_header.jsx'); const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); const TutorialTip = require('./tutorial/tutorial_tip.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const TeamStore = require('../stores/team_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); const AsyncClient = require('../utils/async_client.jsx'); const Client = require('../utils/client.jsx'); const Utils = require('../utils/utils.jsx'); const Constants = require('../utils/constants.jsx'); const Preferences = Constants.Preferences; const TutorialSteps = Constants.TutorialSteps; const NotificationPrefs = Constants.NotificationPrefs; const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; 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.updateScrollbar = this.updateScrollbar.bind(this); this.handleResize = this.handleResize.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.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this); this.getUnreadCount = this.getUnreadCount.bind(this); this.isLeaving = new Map(); const state = this.getStateFromStores(); state.newChannelModalType = ''; state.showDirectChannelsModal = false; state.loadingDMChannel = -1; state.windowWidth = Utils.windowWidth(); this.state = state; this.unreadCountPerChannel = {}; this.setUnreadCountPerChannel(); } setUnreadCountPerChannel() { const channels = ChannelStore.getAll(); const members = ChannelStore.getAllMembers(); const channelUnreadCounts = {}; channels.forEach((ch) => { const chMember = members[ch.id]; let chMentionCount = chMember.mention_count; let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount; if (ch.type === 'D') { chMentionCount = chUnreadCount; chUnreadCount = 0; } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) { chUnreadCount = 0; } channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; }); this.unreadCountPerChannel = channelUnreadCounts; } getUnreadCount(channelId) { let mentions = 0; let msgs = 0; if (channelId) { return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions}; } Object.keys(this.unreadCountPerChannel).forEach((chId) => { msgs += this.unreadCountPerChannel[chId].msgs; mentions += this.unreadCountPerChannel[chId].mentions; }); return {msgs, mentions}; } getStateFromStores() { const members = ChannelStore.getAllMembers(); const currentChannelId = ChannelStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); channels.sort((a, b) => a.display_name.localeCompare(b.display_name)); const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL); const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); var visibleDirectChannels = []; for (var i = 0; i < directChannels.length; i++) { const dm = directChannels[i]; const teammate = Utils.getDirectTeammate(dm.id); if (!teammate) { continue; } const member = members[dm.id]; const msgCount = dm.total_msg_count - member.msg_count; // always show a channel if either it is the current one or if it is unread, but it is not currently being left const forceShow = (currentChannelId === dm.id || msgCount > 0) && !this.isLeaving.get(dm.id); const preferenceShow = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false')); if (preferenceShow || forceShow) { dm.display_name = Utils.displayUsername(teammate.id); dm.teammate_id = teammate.id; dm.status = UserStore.getStatus(teammate.id); visibleDirectChannels.push(dm); if (forceShow && !preferenceShow) { // make sure that unread direct channels are visible const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); AsyncClient.savePreferences([preference]); } } } const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length; visibleDirectChannels.sort(this.sortChannelsByDisplayName); const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); return { activeId: currentChannelId, members, publicChannels, privateChannels, visibleDirectChannels, hiddenDirectChannelCount, showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER }; } 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(); this.updateScrollbar(); window.addEventListener('resize', this.handleResize); } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areObjectsEqual(nextState, this.state)) { return true; } return false; } componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); this.updateScrollbar(); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); ChannelStore.removeChangeListener(this.onChange); UserStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); PreferenceStore.removeChangeListener(this.onChange); } handleResize() { this.setState({ windowWidth: Utils.windowWidth(), windowHeight: Utils.windowHeight() }); } updateScrollbar() { if (this.state.windowWidth > 768) { $('.nav-pills__container').perfectScrollbar(); $('.nav-pills__container').perfectScrollbar('update'); } } onChange() { this.setState(this.getStateFromStores()); } updateTitle() { const channel = ChannelStore.getCurrent(); if (channel) { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { currentSiteName = global.window.mm_config.SiteName; } let currentChannelName = channel.display_name; if (channel.type === 'D') { currentChannelName = Utils.getDirectTeammate(channel.id).username; } const unread = this.getUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().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 }); } handleLeaveDirectChannel(channel) { if (!this.isLeaving.get(channel.id)) { this.isLeaving.set(channel.id, true); const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false'); // bypass AsyncClient since we've already saved the updated preferences Client.savePreferences( [preference], () => { this.isLeaving.set(channel.id, false); }, () => { this.isLeaving.set(channel.id, false); } ); this.setState(this.getStateFromStores()); } if (channel.id === this.state.activeId) { Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); } } sortChannelsByDisplayName(a, b) { return a.display_name.localeCompare(b.display_name); } showNewChannelModal(type) { this.setState({newChannelModalType: type}); } hideNewChannelModal() { this.setState({newChannelModalType: ''}); } showMoreDirectChannelsModal() { this.setState({showDirectChannelsModal: true}); } hideMoreDirectChannelsModal() { this.setState({showDirectChannelsModal: false}); } createTutorialTip() { const screens = []; screens.push(
{'Channels'}{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}{'Direct Messages'}{' for a single person or '}{'Private Groups'}{' for multiple people.'}
{'Here are two public channels to start:'}
{'Town Square'}{' is a place for team-wide communication. Everyone in your team is a member of this channel.'}
{'Off-Topic'}{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'}
{'Click '}{'"More..."'}{' to create a new channel or join an existing one.'}
{'You can also create a new channel or private group by clicking the '}{'"+" symbol'}{' next to the channel or private group header.'}