diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/channel_header.jsx | 27 | ||||
-rw-r--r-- | webapp/components/navbar.jsx | 30 | ||||
-rw-r--r-- | webapp/components/needs_team.jsx | 16 | ||||
-rw-r--r-- | webapp/components/popover_list_members.jsx | 14 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_info.jsx | 61 | ||||
-rw-r--r-- | webapp/components/rhs_comment.jsx | 63 | ||||
-rw-r--r-- | webapp/components/rhs_root_post.jsx | 63 | ||||
-rw-r--r-- | webapp/components/search_results.jsx | 31 | ||||
-rw-r--r-- | webapp/components/search_results_header.jsx | 14 | ||||
-rw-r--r-- | webapp/components/search_results_item.jsx | 13 | ||||
-rw-r--r-- | webapp/components/sidebar_right.jsx | 23 |
11 files changed, 337 insertions, 18 deletions
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 01e1e98cf..556e49863 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -30,7 +30,7 @@ import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import {getSiteURL} from 'utils/url.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; @@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component { this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.handleShortcut = this.handleShortcut.bind(this); this.getFlagged = this.getFlagged.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); this.initWebrtc = this.initWebrtc.bind(this); this.onBusy = this.onBusy.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); @@ -158,6 +159,15 @@ export default class ChannelHeader extends React.Component { } } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.props.channelId); + } + } + getFlagged(e) { e.preventDefault(); if (SearchStore.isFlaggedPosts) { @@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component { render() { const flagIcon = Constants.FLAG_ICON_SVG; + const pinIcon = Constants.PIN_ICON; if (!this.validState()) { // Use an empty div to make sure the header's height stays constant @@ -762,8 +773,20 @@ export default class ChannelHeader extends React.Component { </OverlayTrigger> </div> </th> - <th className='header-list__members'> + <th className='header-list__right'> {popoverListMembers} + <a + href='#' + type='button' + id='pinned-posts-button' + className='pinned-posts-button' + onClick={this.getPinnedPosts} + > + <span + dangerouslySetInnerHTML={{__html: pinIcon}} + aria-hidden='true' + /> + </a> </th> <th className='search-bar__container'> <NavbarSearchBox diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index c945a0b9c..1ad2f916d 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import SearchStore from 'stores/search_store.jsx'; import ChannelSwitchModal from './channel_switch_modal.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {getPinnedPosts} from 'actions/post_actions.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -62,6 +65,7 @@ export default class Navbar extends React.Component { this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; @@ -216,6 +220,15 @@ export default class Navbar extends React.Component { }); } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.state.channel.id); + } + } + toggleFavorite = (e) => { e.preventDefault(); @@ -244,6 +257,7 @@ export default class Navbar extends React.Component { } let viewInfoOption; + let viewPinnedPostsOption; let addMembersOption; let manageMembersOption; let setChannelHeaderOption; @@ -335,6 +349,21 @@ export default class Navbar extends React.Component { </li> ); + viewPinnedPostsOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.getPinnedPosts} + > + <FormattedMessage + id='navbar.viewPinnedPosts' + defaultMessage='View Pinned Posts' + /> + </a> + </li> + ); + if (!ChannelStore.isDefault(channel)) { addMembersOption = ( <li role='presentation'> @@ -561,6 +590,7 @@ export default class Navbar extends React.Component { role='menu' > {viewInfoOption} + {viewPinnedPostsOption} {notificationPreferenceOption} {addMembersOption} {manageMembersOption} diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index fb6029c2b..f589136b5 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import PostStore from 'stores/post_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx'; import Constants from 'utils/constants.jsx'; @@ -173,12 +174,25 @@ export default class NeedsTeam extends React.Component { </div> ); } + + let channel = ChannelStore.getByName(this.props.params.channel); + if (channel == null) { + // the permalink view is not really tied to a particular channel but still needs it + const postId = PostStore.getFocusedPostId(); + const post = PostStore.getEarliestPostFromPage(postId); + + // the post take some time before being available on page load + if (post != null) { + channel = ChannelStore.get(post.channel_id); + } + } + return ( <div className='channel-view'> <ErrorBar/> <WebrtcNotification/> <div className='container-fluid'> - <SidebarRight/> + <SidebarRight channel={channel}/> <SidebarRightMenu teamType={this.state.team.type}/> <WebrtcSidebar/> {content} diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index bd2f744c7..1518b1ebf 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component { } return ( - <div> + <div className='member-popover__container'> <div id='member_popover' className='member-popover__trigger' @@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component { AsyncClient.getProfilesInChannel(this.props.channel.id, 0); }} > - <div> - {countText} - <span - className='fa fa-user' - aria-hidden='true' - /> - </div> + {countText} + <span + className='fa fa-user' + aria-hidden='true' + /> </div> <Overlay rootClose={true} diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index 331fdeb00..5318ec272 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -26,6 +26,8 @@ export default class PostInfo extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -148,6 +150,42 @@ export default class PostInfo extends React.Component { ); } + if (this.props.post.is_pinned) { + dropdownContents.push( + <li + key='unpinLink' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='post_info.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='pinLink' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='post_info.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -250,6 +288,16 @@ export default class PostInfo extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + PostActions.pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + PostActions.unpinPost(this.props.post.channel_id, this.props.post.id); + } + flagPost(e) { e.preventDefault(); PostActions.flagPost(this.props.post.id); @@ -374,6 +422,18 @@ export default class PostInfo extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <ul className='post__header--info'> <li className='col'> @@ -384,6 +444,7 @@ export default class PostInfo extends React.Component { useMilitaryTime={this.props.useMilitaryTime} postId={post.id} /> + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index cb527d850..c9a582877 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list import RhsDropdown from 'components/rhs_dropdown.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -36,6 +36,8 @@ export default class RhsComment extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -128,6 +130,16 @@ export default class RhsComment extends React.Component { unflagPost(this.props.post.id); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + createDropdown() { const post = this.props.post; @@ -195,6 +207,42 @@ export default class RhsComment extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-comment-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-comment-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -503,6 +551,18 @@ export default class RhsComment extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { day: 'numeric', month: 'short', @@ -524,6 +584,7 @@ export default class RhsComment extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 0c1037501..6a6b01a7f 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component { this.handlePermalink = this.handlePermalink.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -143,6 +145,16 @@ export default class RhsRootPost extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + render() { const post = this.props.post; const user = this.props.user; @@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-root-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-root-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -450,6 +498,18 @@ export default class RhsRootPost extends React.Component { flagFunc = this.flagPost; } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { day: 'numeric', month: 'short', @@ -470,6 +530,7 @@ export default class RhsRootPost extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} <OverlayTrigger key={'rootpostflagtooltipkey' + flagVisible} delayShow={Constants.OVERLAY_TIME_DELAY} diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 4c0105738..ceafd766c 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -213,6 +213,31 @@ export default class SearchResults extends React.Component { </ul> </div> ); + } else if (this.props.isPinnedPosts && noResults) { + ctls = ( + <div className='sidebar--right__subheader'> + <ul> + <li> + <FormattedHTMLMessage + id='search_results.usagePin1' + defaultMessage='There are no pinned messages yet.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin2' + defaultMessage={'You can pin a message by clicking the "Pin to channel" option from the message\'s menu.'} + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin3' + defaultMessage='Pinned messages are accessible by all channel members and are a way to mark messages for future reference.' + /> + </li> + </ul> + </div> + ); } else if (!searchTerm && noResults) { ctls = ( <div className='sidebar--right__subheader'> @@ -289,6 +314,8 @@ export default class SearchResults extends React.Component { toggleSize={this.props.toggleSize} shrink={this.props.shrink} isFlaggedPosts={this.props.isFlaggedPosts} + isPinnedPosts={this.props.isPinnedPosts} + channelDisplayName={this.props.channelDisplayName} /> <div id='search-items-container' @@ -307,5 +334,7 @@ SearchResults.propTypes = { useMilitaryTime: React.PropTypes.bool.isRequired, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_header.jsx b/webapp/components/search_results_header.jsx index 1f2818e98..288d883ee 100644 --- a/webapp/components/search_results_header.jsx +++ b/webapp/components/search_results_header.jsx @@ -79,6 +79,16 @@ export default class SearchResultsHeader extends React.Component { defaultMessage='Flagged Posts' /> ); + } else if (this.props.isPinnedPosts) { + title = ( + <FormattedMessage + id='search_header.title4' + defaultMessage='Pinned posts in {channelDisplayName}' + values={{ + channelDisplayName: this.props.channelDisplayName + }} + /> + ); } return ( @@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = { isMentionSearch: React.PropTypes.bool, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index b3de3492c..1c7309f51 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -289,6 +289,18 @@ export default class SearchResultsItem extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <div className='search-item__container'> <div className='date-separator'> @@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post)} + {pinnedBadge} {flagContent} </li> {rhsControls} diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index fb120337a..42b7381f4 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import WebrtcStore from 'stores/webrtc_store.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import {trackEvent} from 'actions/diagnostics_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; -import React from 'react'; +import React, {PropTypes} from 'react'; export default class SidebarRight extends React.Component { constructor(props) { @@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onSelectedChange = this.onSelectedChange.bind(this); + this.onPostPinnedChange = this.onPostPinnedChange.bind(this); this.onSearchChange = this.onSearchChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onShowSearch = this.onShowSearch.bind(this); @@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component { searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts(), postRightVisible: Boolean(PostStore.getSelectedPost()), expanded: false, fromSearch: false, @@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component { componentDidMount() { SearchStore.addSearchChangeListener(this.onSearchChange); PostStore.addSelectedPostChangeListener(this.onSelectedChange); + PostStore.addPostPinnedChangeListener(this.onPostPinnedChange); SearchStore.addShowSearchListener(this.onShowSearch); UserStore.addChangeListener(this.onUserChange); PreferenceStore.addChangeListener(this.onPreferenceChange); @@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component { componentWillUnmount() { SearchStore.removeSearchChangeListener(this.onSearchChange); PostStore.removeSelectedPostChangeListener(this.onSelectedChange); + PostStore.removePostPinnedChangeListener(this.onPostPinnedChange); SearchStore.removeShowSearchListener(this.onShowSearch); UserStore.removeChangeListener(this.onUserChange); PreferenceStore.removeChangeListener(this.onPreferenceChange); @@ -137,6 +141,12 @@ export default class SidebarRight extends React.Component { }); } + onPostPinnedChange() { + if (this.props.channel && this.state.isPinnedPosts) { + getPinnedPosts(this.props.channel.id); + } + } + onShrink() { this.setState({ expanded: false @@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component { this.setState({ searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), - isFlaggedPosts: SearchStore.getIsFlaggedPosts() + isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts() }); } @@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component { <SearchResults isMentionSearch={this.state.isMentionSearch} isFlaggedPosts={this.state.isFlaggedPosts} + isPinnedPosts={this.state.isPinnedPosts} useMilitaryTime={this.state.useMilitaryTime} toggleSize={this.toggleSize} shrink={this.onShrink} + channelDisplayName={this.props.channel ? this.props.channel.display_name : ''} /> ); } else if (this.state.postRightVisible) { @@ -222,3 +235,7 @@ export default class SidebarRight extends React.Component { ); } } + +SidebarRight.propTypes = { + channel: PropTypes.object +}; |