diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-03-16 18:16:11 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-03-16 18:16:11 -0400 |
commit | 4a4859351a4cd277f94d3faa804daaad0733b270 (patch) | |
tree | 4e7f83d3e2564b9b89d669e9f7905ff11768b11a /webapp/components/sidebar.jsx | |
parent | 29fe6a3d13c9c7aa490fffebbe5d1b5fdf1e3090 (diff) | |
parent | 12896bd23eeba79884245c1c29fdc568cf21a7fa (diff) | |
download | chat-4a4859351a4cd277f94d3faa804daaad0733b270.tar.gz chat-4a4859351a4cd277f94d3faa804daaad0733b270.tar.bz2 chat-4a4859351a4cd277f94d3faa804daaad0733b270.zip |
Merge pull request #2453 from mattermost/plt-2340
PLT-2340 Converting to Webpack. Stage 1.
Diffstat (limited to 'webapp/components/sidebar.jsx')
-rw-r--r-- | webapp/components/sidebar.jsx | 717 |
1 files changed, 717 insertions, 0 deletions
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx new file mode 100644 index 000000000..49ae1bec6 --- /dev/null +++ b/webapp/components/sidebar.jsx @@ -0,0 +1,717 @@ +// 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 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 Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.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 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.handleResize = this.handleResize.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.isLeaving = new Map(); + + const state = this.getStateFromStores(); + state.newChannelModalType = ''; + state.showDirectChannelsModal = false; + state.loadingDMChannel = -1; + state.windowWidth = Utils.windowWidth(); + this.state = state; + } + getTotalUnreadCount() { + let msgs = 0; + let mentions = 0; + const unreadCounts = this.state.unreadCounts; + + Object.keys(unreadCounts).forEach((chId) => { + 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((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 preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + + const directChannels = []; + for (const preference of preferences) { + if (preference.value !== 'true') { + continue; + } + + const teammateId = preference.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); + + directChannels.push(directChannel); + } + + directChannels.sort(this.sortChannelsByDisplayName); + + const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - directChannels.length; + + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + + return { + activeId: currentChannelId, + members, + publicChannels, + privateChannels, + directChannels, + hiddenDirectChannelCount, + unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), + showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, + currentTeam: TeamStore.getCurrent(), + currentUser: UserStore.getCurrentUser() + }; + } + + 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(); + + window.addEventListener('resize', this.handleResize); + } + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(nextState, this.state)) { + return true; + } + return false; + } + componentDidUpdate() { + this.updateTitle(); + this.updateUnreadIndicators(); + } + 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() + }); + } + 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 + }); + } + + 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); + } + + 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}); + } + + createTutorialTip() { + const screens = []; + + screens.push( + <div> + <FormattedHTMLMessage + id='sidebar.tutorialScreen1' + 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 Groups</strong> for multiple people.</p>' + /> + </div> + ); + + screens.push( + <div> + <FormattedHTMLMessage + id='sidebar.tutorialScreen2' + defaultMessage='<h4>"Town Square" and "Off-Topic" channels</h4> + <p>Here are two public channels to start:</p> + <p><strong>Town Square</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p> + <p><strong>Off-Topic</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>' + /> + </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> + <p>You can also create a new channel or private group by clicking the <strong>"+" symbol</strong> next to the channel or private group header.</p>' + /> + </div> + ); + + return ( + <TutorialTip + placement='right' + screens={screens} + overlayClass='tip-overlay--sidebar' + /> + ); + } + + 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) { + badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>; + this.badgesActive = true; + } + } else if (this.state.loadingDMChannel === index && channel.type === 'D') { + badge = ( + <img + className='channel-loading-gif pull-right' + src={loadingGif} + /> + ); + } + + if (msgCount > 0) { + rowClass += ' has-badge'; + } + + // set up status icon for direct message channels + var status = null; + if (channel.type === 'D') { + var statusIcon = ''; + if (channel.status === 'online') { + statusIcon = Constants.ONLINE_ICON_SVG; + } else if (channel.status === 'away') { + statusIcon = Constants.AWAY_ICON_SVG; + } else { + statusIcon = Constants.OFFLINE_ICON_SVG; + } + status = ( + <span + className='status' + dangerouslySetInnerHTML={{__html: statusIcon}} + /> + ); + } + + var icon = null; + if (channel.type === 'O') { + icon = <div className='status'><i className='fa fa-globe'></i></div>; + } else if (channel.type === 'P') { + icon = <div className='status'><i className='fa fa-lock'></i></div>; + } + + // set up click handler to switch channels (or create a new channel for non-existant ones) + var handleClick = null; + var href = '#'; + + if (!channel.fake) { + handleClick = function clickHandler(e) { + if (e.target.attributes.getNamedItem('data-close')) { + handleClose(channel); + } else { + Utils.switchChannel(channel); + } + + e.preventDefault(); + }; + } else if (channel.fake) { + // It's a direct message channel that doesn't exist yet so let's create it now + var otherUserId = Utils.getUserIdFromChannelName(channel); + + if (this.state.loadingDMChannel === -1) { + handleClick = function clickHandler(e) { + e.preventDefault(); + + if (e.target.attributes.getNamedItem('data-close')) { + handleClose(channel); + } else { + this.setState({loadingDMChannel: index}); + + Client.createDirectChannel(channel, otherUserId, + (data) => { + this.setState({loadingDMChannel: -1}); + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + }, + () => { + this.setState({loadingDMChannel: -1}); + window.location.href = '/' + this.state.currentTeam.name; + } + ); + } + }.bind(this); + } + } + + 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} + > + <span + className='btn-close' + data-close='true' + > + {'×'} + </span> + </OverlayTrigger> + ); + + rowClass += ' has-close'; + } + + let tutorialTip = null; + if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) { + tutorialTip = this.createTutorialTip(); + } + + return ( + <li + key={channel.name} + ref={channel.name} + className={linkClass} + > + <a + className={rowClass} + href={href} + onClick={handleClick} + > + {icon} + {status} + {channel.display_name} + {badge} + {closeButton} + </a> + {tutorialTip} + </li> + ); + } + render() { + // Check if we have all info needed to render + if (this.state.currentTeam == null || this.state.currentUser == null) { + return (<div/>); + } + + 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; + + // create elements for all 3 types of channels + const publicChannelItems = this.state.publicChannels.map(this.createChannelElement); + + const privateChannelItems = this.state.privateChannels.map(this.createChannelElement); + + const directMessageItems = this.state.directChannels.map((channel, index, arr) => { + return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); + }); + + // update the favicon to show if there are any notifications + 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); + + var directMessageMore = null; + if (this.state.hiddenDirectChannelCount > 0) { + directMessageMore = ( + <li key='more'> + <a + href='#' + onClick={this.showMoreDirectChannelsModal} + > + <FormattedMessage + id='sidebar.more' + defaultMessage='More ({count})' + values={{ + count: this.state.hiddenDirectChannelCount + }} + /> + </a> + </li> + ); + } + + let showChannelModal = false; + if (this.state.newChannelModalType !== '') { + showChannelModal = true; + } + + const createChannelTootlip = ( + <Tooltip id='new-channel-tooltip' > + <FormattedMessage + id='sidebar.createChannel' + defaultMessage='Create new channel' + /> + </Tooltip> + ); + const createGroupTootlip = ( + <Tooltip id='new-group-tooltip'> + <FormattedMessage + id='sidebar.createGroup' + defaultMessage='Create new group' + /> + </Tooltip> + ); + + const above = ( + <FormattedMessage + id='sidebar.unreadAbove' + defaultMessage='Unread post(s) above' + /> + ); + + const below = ( + <FormattedMessage + id='sidebar.unreadBelow' + defaultMessage='Unread post(s) below' + /> + ); + + return ( + <div + className='sidebar--left' + id='sidebar-left' + > + <NewChannelFlow + show={showChannelModal} + channelType={this.state.newChannelModalType} + onModalDismissed={this.hideNewChannelModal} + /> + <MoreDirectChannels + show={this.state.showDirectChannelsModal} + onModalDismissed={this.hideMoreDirectChannelsModal} + /> + + <SidebarHeader + teamDisplayName={this.state.currentTeam.display_name} + 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} + > + <ul className='nav nav-pills nav-stacked'> + <li> + <h4> + <FormattedMessage + id='sidebar.channels' + defaultMessage='Channels' + /> + <OverlayTrigger + delayShow={500} + placement='top' + overlay={createChannelTootlip} + > + <a + className='add-channel-btn' + href='#' + onClick={this.showNewChannelModal.bind(this, 'O')} + > + {'+'} + </a> + </OverlayTrigger> + </h4> + </li> + {publicChannelItems} + <li> + <a + 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' + defaultMessage='Private Groups' + /> + <OverlayTrigger + delayShow={500} + placement='top' + overlay={createGroupTootlip} + > + <a + className='add-channel-btn' + href='#' + onClick={this.showNewChannelModal.bind(this, 'P')} + > + {'+'} + </a> + </OverlayTrigger> + </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> + </div> + ); + } +} + +Sidebar.defaultProps = { +}; +Sidebar.propTypes = { +}; |