diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/channel_header.jsx | 49 | ||||
-rw-r--r-- | webapp/components/post_view/components/post.jsx | 8 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_header.jsx | 4 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_info.jsx | 119 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_list.jsx | 9 | ||||
-rw-r--r-- | webapp/components/post_view/post_focus_view_controller.jsx | 13 | ||||
-rw-r--r-- | webapp/components/post_view/post_view_controller.jsx | 11 | ||||
-rw-r--r-- | webapp/components/rhs_comment.jsx | 116 | ||||
-rw-r--r-- | webapp/components/rhs_header_post.jsx | 59 | ||||
-rw-r--r-- | webapp/components/rhs_root_post.jsx | 129 | ||||
-rw-r--r-- | webapp/components/rhs_thread.jsx | 22 | ||||
-rw-r--r-- | webapp/components/search_results.jsx | 42 | ||||
-rw-r--r-- | webapp/components/search_results_header.jsx | 10 | ||||
-rw-r--r-- | webapp/components/search_results_item.jsx | 86 | ||||
-rw-r--r-- | webapp/components/sidebar_right.jsx | 20 | ||||
-rw-r--r-- | webapp/components/sidebar_right_menu.jsx | 19 |
16 files changed, 646 insertions, 70 deletions
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index f26105c7a..66cd61245 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -26,20 +26,19 @@ import PreferenceStore from 'stores/preference_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Utils from 'utils/utils.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import Client from 'client/web_client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import {getFlaggedPosts} from 'actions/post_actions.jsx'; + import Constants from 'utils/constants.jsx'; const UserStatuses = Constants.UserStatuses; +const ActionTypes = Constants.ActionTypes; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router/es6'; - -const ActionTypes = Constants.ActionTypes; - import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap'; -import React from 'react'; - export default class ChannelHeader extends React.Component { constructor(props) { super(props); @@ -50,6 +49,7 @@ export default class ChannelHeader extends React.Component { this.showRenameChannelModal = this.showRenameChannelModal.bind(this); this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.openRecentMentions = this.openRecentMentions.bind(this); + this.getFlagged = this.getFlagged.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; @@ -159,6 +159,11 @@ export default class ChannelHeader extends React.Component { }); } + getFlagged(e) { + e.preventDefault(); + getFlaggedPosts(); + } + openRecentMentions(e) { if (Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) { e.preventDefault(); @@ -220,6 +225,8 @@ export default class ChannelHeader extends React.Component { } render() { + const flagIcon = Constants.FLAG_ICON_OUTLINE_SVG; + if (!this.validState()) { return null; } @@ -233,6 +240,16 @@ export default class ChannelHeader extends React.Component { /> </Tooltip> ); + + const flaggedTooltip = ( + <Tooltip id='flaggedTooltip'> + <FormattedMessage + id='channel_header.flagged' + defaultMessage='Flagged Posts' + /> + </Tooltip> + ); + const popoverContent = ( <Popover id='header-popover' @@ -592,6 +609,26 @@ export default class ChannelHeader extends React.Component { </OverlayTrigger> </div> </th> + <th> + <div className='dropdown channel-header__links'> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='bottom' + overlay={flaggedTooltip} + > + <a + href='#' + type='button' + onClick={this.getFlagged} + > + <span + className='icon icon__flag' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + </a> + </OverlayTrigger> + </div> + </th> </tr> </tbody> </table> diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 3fdd8094e..038bcab78 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -100,6 +100,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.isFlagged !== this.props.isFlagged) { + return true; + } + if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { return true; } @@ -245,6 +249,7 @@ export default class Post extends React.Component { compactDisplay={this.props.compactDisplay} displayNameType={this.props.displayNameType} useMilitaryTime={this.props.useMilitaryTime} + isFlagged={this.props.isFlagged} /> <PostBody post={post} @@ -281,5 +286,6 @@ Post.propTypes = { commentCount: React.PropTypes.number, isCommentMention: React.PropTypes.bool, useMilitaryTime: React.PropTypes.bool.isRequired, - emojis: React.PropTypes.object.isRequired + emojis: React.PropTypes.object.isRequired, + isFlagged: React.PropTypes.bool }; diff --git a/webapp/components/post_view/components/post_header.jsx b/webapp/components/post_view/components/post_header.jsx index 07b601baf..6c356126d 100644 --- a/webapp/components/post_view/components/post_header.jsx +++ b/webapp/components/post_view/components/post_header.jsx @@ -72,6 +72,7 @@ export default class PostHeader extends React.Component { currentUser={this.props.currentUser} compactDisplay={this.props.compactDisplay} useMilitaryTime={this.props.useMilitaryTime} + isFlagged={this.props.isFlagged} /> </li> </ul> @@ -97,5 +98,6 @@ PostHeader.propTypes = { sameUser: React.PropTypes.bool.isRequired, compactDisplay: React.PropTypes.bool, displayNameType: React.PropTypes.string, - useMilitaryTime: React.PropTypes.bool.isRequired + useMilitaryTime: React.PropTypes.bool.isRequired, + isFlagged: React.PropTypes.bool.isRequired }; diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index ba6a9a982..d48d97ba1 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -2,17 +2,21 @@ // See License.txt for license information. import $ from 'jquery'; -import * as Utils from 'utils/utils.jsx'; + import PostTime from './post_time.jsx'; + import * as GlobalActions from 'actions/global_actions.jsx'; +import * as PostActions from 'actions/post_actions.jsx'; + import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; - -import {FormattedMessage} from 'react-intl'; +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import React from 'react'; +import {FormattedMessage} from 'react-intl'; export default class PostInfo extends React.Component { constructor(props) { @@ -21,7 +25,10 @@ export default class PostInfo extends React.Component { this.handleDropdownClick = this.handleDropdownClick.bind(this); this.handlePermalink = this.handlePermalink.bind(this); this.removePost = this.removePost.bind(this); + this.flagPost = this.flagPost.bind(this); + this.unflagPost = this.unflagPost.bind(this); } + handleDropdownClick(e) { var position = $('#post-list').height() - $(e.target).offset().top; var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu'); @@ -29,10 +36,12 @@ export default class PostInfo extends React.Component { dropdown.addClass('bottom'); } } + componentDidMount() { $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', () => this.props.handleDropdownOpened(true)); $('#post_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false)); } + createDropdown() { var post = this.props.post; var isOwner = this.props.currentUser.id === post.user_id; @@ -74,6 +83,44 @@ export default class PostInfo extends React.Component { ); } + if (Utils.isMobile()) { + if (this.props.isFlagged) { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.unflagPost} + > + <FormattedMessage + id='rhs_root.mobile.unflag' + defaultMessage='Unflag' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.flagPost} + > + <FormattedMessage + id='rhs_root.mobile.flag' + defaultMessage='Flag' + /> + </a> + </li> + ); + } + } + dropdownContents.push( <li key='copyLink' @@ -186,12 +233,23 @@ export default class PostInfo extends React.Component { ); } + flagPost(e) { + e.preventDefault(); + PostActions.flagPost(this.props.post.id); + } + + unflagPost(e) { + e.preventDefault(); + PostActions.unflagPost(this.props.post.id); + } + render() { var post = this.props.post; var comments = ''; var showCommentClass = ''; var highlightMentionClass = ''; var commentCountText = this.props.commentCount; + const flagIcon = Constants.FLAG_ICON_SVG; if (this.props.commentCount >= 1) { showCommentClass = ' icon--show'; @@ -240,6 +298,44 @@ export default class PostInfo extends React.Component { ); } + let flag; + let flagFunc; + let flagVisible = ''; + let flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.flag' + defaultMessage='Flag for follow up' + /> + </Tooltip> + ); + if (this.props.isFlagged) { + flagVisible = 'visible'; + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.unflagPost; + flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.unflag' + defaultMessage='Unflag' + /> + </Tooltip> + ); + } else { + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.flagPost; + } + return ( <ul className='post__header--info'> <li className='col'> @@ -249,6 +345,20 @@ export default class PostInfo extends React.Component { compactDisplay={this.props.compactDisplay} useMilitaryTime={this.props.useMilitaryTime} /> + <OverlayTrigger + key={'flagtooltipkey' + flagVisible} + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={flagTooltip} + > + <a + href='#' + className={'flag-icon__container ' + flagVisible} + onClick={flagFunc} + > + {flag} + </a> + </OverlayTrigger> </li> {options} </ul> @@ -274,5 +384,6 @@ PostInfo.propTypes = { sameUser: React.PropTypes.bool.isRequired, currentUser: React.PropTypes.object.isRequired, compactDisplay: React.PropTypes.bool, - useMilitaryTime: React.PropTypes.bool.isRequired + useMilitaryTime: React.PropTypes.bool.isRequired, + isFlagged: React.PropTypes.bool }; diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index befd1a10d..95b30a9d7 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -284,6 +284,11 @@ export default class PostList extends React.Component { } } + let isFlagged = false; + if (this.props.flaggedPosts) { + isFlagged = this.props.flaggedPosts.get(post.id) === 'true'; + } + const postCtl = ( <Post key={keyPrefix + 'postKey'} @@ -305,6 +310,7 @@ export default class PostList extends React.Component { previewCollapsed={this.props.previewsCollapsed} useMilitaryTime={this.props.useMilitaryTime} emojis={this.props.emojis} + isFlagged={isFlagged} /> ); @@ -572,5 +578,6 @@ PostList.propTypes = { previewsCollapsed: React.PropTypes.string, useMilitaryTime: React.PropTypes.bool.isRequired, isFocusPost: React.PropTypes.bool, - emojis: React.PropTypes.object.isRequired + emojis: React.PropTypes.object.isRequired, + flaggedPosts: React.PropTypes.object }; diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index f8738e056..4a7d312f5 100644 --- a/webapp/components/post_view/post_focus_view_controller.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -8,6 +8,7 @@ import EmojiStore from 'stores/emoji_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; import Constants from 'utils/constants.jsx'; const ScrollTypes = Constants.ScrollTypes; @@ -22,6 +23,7 @@ export default class PostFocusView extends React.Component { this.onPostsChange = this.onPostsChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onEmojiChange = this.onEmojiChange.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this); const focusedPostId = PostStore.getFocusedPostId(); @@ -41,7 +43,8 @@ export default class PostFocusView extends React.Component { scrollPostId: focusedPostId, atTop: PostStore.getVisibilityAtTop(focusedPostId), atBottom: PostStore.getVisibilityAtBottom(focusedPostId), - emojis: EmojiStore.getEmojis() + emojis: EmojiStore.getEmojis(), + flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }; } @@ -50,6 +53,7 @@ export default class PostFocusView extends React.Component { PostStore.addChangeListener(this.onPostsChange); UserStore.addChangeListener(this.onUserChange); EmojiStore.addChangeListener(this.onEmojiChange); + PreferenceStore.addChangeListener(this.onPreferenceChange); } componentWillUnmount() { @@ -98,6 +102,12 @@ export default class PostFocusView extends React.Component { }); } + onPreferenceChange() { + this.setState({ + flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) + }); + } + onPostListScroll() { this.setState({scrollType: ScrollTypes.FREE}); } @@ -128,6 +138,7 @@ export default class PostFocusView extends React.Component { postsToHighlight={postsToHighlight} isFocusPost={true} emojis={this.state.emojis} + flaggedPosts={this.state.flaggedPosts} /> ); } diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index a7583fa38..1dd5e9176 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -58,7 +58,8 @@ export default class PostViewController extends React.Component { compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'), useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), - emojis: EmojiStore.getEmojis() + emojis: EmojiStore.getEmojis(), + flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }; } @@ -87,7 +88,8 @@ export default class PostViewController extends React.Component { displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix, - useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false) + useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), + flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }); } @@ -224,6 +226,10 @@ export default class PostViewController extends React.Component { return true; } + if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) { + return true; + } + if (nextState.lastViewed !== this.state.lastViewed) { return true; } @@ -292,6 +298,7 @@ export default class PostViewController extends React.Component { compactDisplay={this.state.compactDisplay} previewsCollapsed={this.state.previewsCollapsed} useMilitaryTime={this.state.useMilitaryTime} + flaggedPosts={this.state.flaggedPosts} lastViewed={this.state.lastViewed} emojis={this.state.emojis} ownNewMessage={this.state.ownNewMessage} diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index ed1f71b1e..a90380510 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -9,12 +9,14 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; +import {flagPost, unflagPost} from 'actions/post_actions.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; import Client from 'client/web_client.jsx'; import Constants from 'utils/constants.jsx'; +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import {FormattedMessage, FormattedDate} from 'react-intl'; @@ -27,13 +29,17 @@ export default class RhsComment extends React.Component { super(props); this.handlePermalink = this.handlePermalink.bind(this); + this.flagPost = this.flagPost.bind(this); + this.unflagPost = this.unflagPost.bind(this); this.state = {}; } + handlePermalink(e) { e.preventDefault(); GlobalActions.showGetPostLinkModal(this.props.post); } + shouldComponentUpdate(nextProps) { if (nextProps.compactDisplay !== this.props.compactDisplay) { return true; @@ -43,6 +49,10 @@ export default class RhsComment extends React.Component { return true; } + if (nextProps.isFlagged !== this.props.isFlagged) { + return true; + } + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } @@ -53,6 +63,17 @@ export default class RhsComment extends React.Component { return false; } + + flagPost(e) { + e.preventDefault(); + flagPost(this.props.post.id); + } + + unflagPost(e) { + e.preventDefault(); + unflagPost(this.props.post.id); + } + createDropdown() { var post = this.props.post; @@ -66,6 +87,44 @@ export default class RhsComment extends React.Component { var dropdownContents = []; + if (Utils.isMobile()) { + if (this.props.isFlagged) { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.unflagPost} + > + <FormattedMessage + id='rhs_root.mobile.unflag' + defaultMessage='Unflag' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.flagPost} + > + <FormattedMessage + id='rhs_root.mobile.flag' + defaultMessage='Flag' + /> + </a> + </li> + ); + } + } + dropdownContents.push( <li key='rhs-root-permalink' @@ -151,8 +210,10 @@ export default class RhsComment extends React.Component { </div> ); } + render() { var post = this.props.post; + const flagIcon = Constants.FLAG_ICON_SVG; var currentUserCss = ''; if (this.props.currentUser === post.user_id) { @@ -225,6 +286,44 @@ export default class RhsComment extends React.Component { ); } + let flag; + let flagFunc; + let flagVisible = ''; + let flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.flag' + defaultMessage='Flag for follow up' + /> + </Tooltip> + ); + if (this.props.isFlagged) { + flagVisible = 'visible'; + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.unflagPost; + flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.unflag' + defaultMessage='Unflag' + /> + </Tooltip> + ); + } else { + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.flagPost; + } + return ( <div className={'post post--thread ' + currentUserCss + ' ' + compactClass}> <div className='post__content'> @@ -247,6 +346,20 @@ export default class RhsComment extends React.Component { minute='2-digit' /> </time> + <OverlayTrigger + key={'commentflagtooltipkey' + flagVisible} + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={flagTooltip} + > + <a + href='#' + className={'flag-icon__container ' + flagVisible} + onClick={flagFunc} + > + {flag} + </a> + </OverlayTrigger> </li> <li className='col col__reply'> {dropdown} @@ -271,5 +384,6 @@ RhsComment.propTypes = { user: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired, compactDisplay: React.PropTypes.bool, - useMilitaryTime: React.PropTypes.bool.isRequired + useMilitaryTime: React.PropTypes.bool.isRequired, + isFlagged: React.PropTypes.bool }; diff --git a/webapp/components/rhs_header_post.jsx b/webapp/components/rhs_header_post.jsx index 8e54016fb..7b71bd7cc 100644 --- a/webapp/components/rhs_header_post.jsx +++ b/webapp/components/rhs_header_post.jsx @@ -4,7 +4,9 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; import {Tooltip, OverlayTrigger} from 'react-bootstrap'; + import * as GlobalActions from 'actions/global_actions.jsx'; +import {getFlaggedPosts} from 'actions/post_actions.jsx'; import {FormattedMessage} from 'react-intl'; @@ -34,17 +36,21 @@ export default class RhsHeaderPost extends React.Component { handleBack(e) { e.preventDefault(); - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH_TERM, - term: this.props.fromSearch, - do_search: true, - is_mention_search: this.props.isMentionSearch - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: null - }); + if (this.props.fromSearch) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH_TERM, + term: this.props.fromSearch, + do_search: true, + is_mention_search: this.props.isMentionSearch + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: null + }); + } else if (this.props.fromFlaggedPosts) { + getFlaggedPosts(); + } } render() { let back; @@ -57,14 +63,26 @@ export default class RhsHeaderPost extends React.Component { </Tooltip> ); - const backToResultsTooltip = ( - <Tooltip id='backToResultsTooltip'> - <FormattedMessage - id='rhs_header.backToResultsTooltip' - defaultMessage='Back to Search Results' - /> - </Tooltip> - ); + let backToResultsTooltip; + if (this.props.fromSearch) { + backToResultsTooltip = ( + <Tooltip id='backToResultsTooltip'> + <FormattedMessage + id='rhs_header.backToResultsTooltip' + defaultMessage='Back to Search Results' + /> + </Tooltip> + ); + } else if (this.props.fromFlaggedPosts) { + backToResultsTooltip = ( + <Tooltip id='backToResultsTooltip'> + <FormattedMessage + id='rhs_header.backToFlaggedTooltip' + defaultMessage='Back to Flagged Posts' + /> + </Tooltip> + ); + } const expandSidebarTooltip = ( <Tooltip id='expandSidebarTooltip'> @@ -84,7 +102,7 @@ export default class RhsHeaderPost extends React.Component { </Tooltip> ); - if (this.props.fromSearch) { + if (this.props.fromSearch || this.props.fromFlaggedPosts) { back = ( <a href='#' @@ -161,6 +179,7 @@ RhsHeaderPost.defaultProps = { RhsHeaderPost.propTypes = { isMentionSearch: React.PropTypes.bool, fromSearch: React.PropTypes.string, + fromFlaggedPosts: React.PropTypes.bool, toggleSize: React.PropTypes.function, shrink: React.PropTypes.function }; diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index cf8c3201a..423abcf82 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -1,19 +1,23 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ChannelStore from 'stores/channel_store.jsx'; import UserProfile from './user_profile.jsx'; +import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; +import FileAttachmentList from './file_attachment_list.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import * as TextFormatting from 'utils/text_formatting.jsx'; -import FileAttachmentList from './file_attachment_list.jsx'; -import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; + import * as GlobalActions from 'actions/global_actions.jsx'; +import {flagPost, unflagPost} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; +import * as TextFormatting from 'utils/text_formatting.jsx'; import Constants from 'utils/constants.jsx'; +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import {FormattedMessage, FormattedDate} from 'react-intl'; @@ -24,13 +28,17 @@ export default class RhsRootPost extends React.Component { super(props); this.handlePermalink = this.handlePermalink.bind(this); + this.flagPost = this.flagPost.bind(this); + this.unflagPost = this.unflagPost.bind(this); this.state = {}; } + handlePermalink(e) { e.preventDefault(); GlobalActions.showGetPostLinkModal(this.props.post); } + shouldComponentUpdate(nextProps) { if (nextProps.compactDisplay !== this.props.compactDisplay) { return true; @@ -40,6 +48,10 @@ export default class RhsRootPost extends React.Component { return true; } + if (nextProps.isFlagged !== this.props.isFlagged) { + return true; + } + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } @@ -50,6 +62,17 @@ export default class RhsRootPost extends React.Component { return false; } + + flagPost(e) { + e.preventDefault(); + flagPost(this.props.post.id); + } + + unflagPost(e) { + e.preventDefault(); + unflagPost(this.props.post.id); + } + render() { const post = this.props.post; const user = this.props.user; @@ -59,6 +82,7 @@ export default class RhsRootPost extends React.Component { const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX); var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); + const flagIcon = Constants.FLAG_ICON_SVG; var type = 'Post'; if (post.root_id.length > 0) { @@ -91,6 +115,44 @@ export default class RhsRootPost extends React.Component { var dropdownContents = []; + if (Utils.isMobile()) { + if (this.props.isFlagged) { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.unflagPost} + > + <FormattedMessage + id='rhs_root.mobile.unflag' + defaultMessage='Unflag' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='mobileFlag' + role='presentation' + > + <a + href='#' + onClick={this.flagPost} + > + <FormattedMessage + id='rhs_root.mobile.flag' + defaultMessage='Flag' + /> + </a> + </li> + ); + } + } + dropdownContents.push( <li key='rhs-root-permalink' @@ -246,6 +308,44 @@ export default class RhsRootPost extends React.Component { /> ); + let flag; + let flagFunc; + let flagVisible = ''; + let flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.flag' + defaultMessage='Flag for follow up' + /> + </Tooltip> + ); + if (this.props.isFlagged) { + flagVisible = 'visible'; + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.unflagPost; + flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.unflag' + defaultMessage='Unflag' + /> + </Tooltip> + ); + } else { + flag = ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + ); + flagFunc = this.flagPost; + } + return ( <div className={'post post--root post--thread ' + userCss + ' ' + systemMessageClass + ' ' + compactClass}> <div className='post-right-channel__name'>{channelName}</div> @@ -267,11 +367,23 @@ export default class RhsRootPost extends React.Component { minute='2-digit' /> </time> + <OverlayTrigger + key={'rootpostflagtooltipkey' + flagVisible} + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={flagTooltip} + > + <a + href='#' + className={'flag-icon__container ' + flagVisible} + onClick={flagFunc} + > + {flag} + </a> + </OverlayTrigger> </li> <li className='col col__reply'> - <div> - {rootOptions} - </div> + {rootOptions} </li> </ul> <div className='post__body'> @@ -297,5 +409,6 @@ RhsRootPost.propTypes = { currentUser: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number, compactDisplay: React.PropTypes.bool, - useMilitaryTime: React.PropTypes.bool.isRequired + useMilitaryTime: React.PropTypes.bool.isRequired, + isFlagged: React.PropTypes.bool }; diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index d99ace6d4..856a686cb 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -62,6 +62,7 @@ export default class RhsThread extends React.Component { state.windowHeight = Utils.windowHeight(); state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles())); state.compactDisplay = PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT; + state.flaggedPosts = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST); this.state = state; } @@ -121,6 +122,10 @@ export default class RhsThread extends React.Component { return true; } + if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) { + return true; + } + if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) { return true; } @@ -151,7 +156,8 @@ export default class RhsThread extends React.Component { onPreferenceChange() { this.setState({ - compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT + compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, + flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }); this.forceUpdateInfo(); } @@ -240,12 +246,18 @@ export default class RhsThread extends React.Component { profile = profiles[selected.user_id]; } + let isRootFlagged = false; + if (this.state.flaggedPosts) { + isRootFlagged = this.state.flaggedPosts.get(selected.id) === 'true'; + } + return ( <div className='post-right__container'> <FileUploadOverlay overlayType='right'/> <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> <div className='sidebar-right__body'> <RhsHeaderPost + fromFlaggedPosts={this.props.fromFlaggedPosts} fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} toggleSize={this.props.toggleSize} @@ -268,6 +280,7 @@ export default class RhsThread extends React.Component { currentUser={this.props.currentUser} compactDisplay={this.state.compactDisplay} useMilitaryTime={this.props.useMilitaryTime} + isFlagged={isRootFlagged} /> <div className='post-right-comments-container'> {postsArray.map((comPost) => { @@ -277,6 +290,11 @@ export default class RhsThread extends React.Component { } else { p = profiles[comPost.user_id]; } + + let isFlagged = false; + if (this.state.flaggedPosts) { + isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true'; + } return ( <Comment ref={comPost.id} @@ -286,6 +304,7 @@ export default class RhsThread extends React.Component { currentUser={this.props.currentUser} compactDisplay={this.state.compactDisplay} useMilitaryTime={this.props.useMilitaryTime} + isFlagged={isFlagged} /> ); })} @@ -311,6 +330,7 @@ RhsThread.defaultProps = { RhsThread.propTypes = { fromSearch: React.PropTypes.string, + fromFlaggedPosts: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool, currentUser: React.PropTypes.object.isRequired, useMilitaryTime: React.PropTypes.bool.isRequired, diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 6431ff2c2..9e3092cca 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -7,6 +7,7 @@ import SearchStore from 'stores/search_store.jsx'; import UserStore from 'stores/user_store.jsx'; import SearchBox from './search_bar.jsx'; import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import SearchResultsHeader from './search_results_header.jsx'; import SearchResultsItem from './search_results_item.jsx'; @@ -122,10 +123,44 @@ export default class SearchResults extends React.Component { var noResults = (!results || !results.order || !results.order.length); const searchTerm = this.state.searchTerm; const profiles = this.state.profiles || {}; + const flagIcon = Constants.FLAG_ICON_SVG; var ctls = null; - if (!searchTerm && noResults) { + if (this.props.isFlaggedPosts && noResults) { + ctls = ( + <div className='sidebar--right__subheader'> + <ul> + <li> + <FormattedHTMLMessage + id='search_results.usageFlag1' + defaultMessage="You haven't flagged any messages yet." + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usageFlag2' + defaultMessage='You can add a flag to messages and comments by clicking the ' + /> + <span + className='usage__icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + <FormattedHTMLMessage + id='search_results.usageFlag3' + defaultMessage=' icon next to the timestamp.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usageFlag4' + defaultMessage='Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.' + /> + </li> + </ul> + </div> + ); + } else if (!searchTerm && noResults) { ctls = ( <div className='sidebar--right__subheader'> <FormattedHTMLMessage @@ -172,6 +207,7 @@ export default class SearchResults extends React.Component { isMentionSearch={this.props.isMentionSearch} useMilitaryTime={this.props.useMilitaryTime} shrink={this.props.shrink} + isFlagged={this.props.isFlaggedPosts} /> ); }, this); @@ -185,6 +221,7 @@ export default class SearchResults extends React.Component { isMentionSearch={this.props.isMentionSearch} toggleSize={this.props.toggleSize} shrink={this.props.shrink} + isFlaggedPosts={this.props.isFlaggedPosts} /> <div id='search-items-container' @@ -202,5 +239,6 @@ SearchResults.propTypes = { isMentionSearch: React.PropTypes.bool, useMilitaryTime: React.PropTypes.bool.isRequired, toggleSize: React.PropTypes.function, - shrink: React.PropTypes.function + shrink: React.PropTypes.function, + isFlaggedPosts: React.PropTypes.bool }; diff --git a/webapp/components/search_results_header.jsx b/webapp/components/search_results_header.jsx index 7cb072b70..e0d57494c 100644 --- a/webapp/components/search_results_header.jsx +++ b/webapp/components/search_results_header.jsx @@ -89,6 +89,13 @@ export default class SearchResultsHeader extends React.Component { defaultMessage='Recent Mentions' /> ); + } else if (this.props.isFlaggedPosts) { + title = ( + <FormattedMessage + id='search_header.title3' + defaultMessage='Flagged Posts' + /> + ); } return ( @@ -140,5 +147,6 @@ export default class SearchResultsHeader extends React.Component { SearchResultsHeader.propTypes = { isMentionSearch: React.PropTypes.bool, toggleSize: React.PropTypes.function, - shrink: React.PropTypes.function + shrink: React.PropTypes.function, + isFlaggedPosts: React.PropTypes.bool }; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index fb8b23a7f..db64463a9 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -7,16 +7,20 @@ import UserProfile from './user_profile.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {unflagPost, getFlaggedPosts} from 'actions/post_actions.jsx'; + import * as TextFormatting from 'utils/text_formatting.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; + import Constants from 'utils/constants.jsx'; +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; const ActionTypes = Constants.ActionTypes; -import {FormattedMessage, FormattedDate} from 'react-intl'; import React from 'react'; +import {FormattedMessage, FormattedDate} from 'react-intl'; import {browserHistory} from 'react-router/es6'; export default class SearchResultsItem extends React.Component { @@ -25,6 +29,7 @@ export default class SearchResultsItem extends React.Component { this.handleFocusRHSClick = this.handleFocusRHSClick.bind(this); this.shrinkSidebar = this.shrinkSidebar.bind(this); + this.unflagPost = this.unflagPost.bind(this); } hideSidebar() { @@ -42,12 +47,20 @@ export default class SearchResultsItem extends React.Component { GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } + unflagPost(e) { + e.preventDefault(); + unflagPost(this.props.post.id, + () => getFlaggedPosts() + ); + } + render() { let channelName = null; const channel = this.props.channel; const timestamp = UserStore.getCurrentUser().update_at; const user = this.props.user || {}; const post = this.props.post; + const flagIcon = Constants.FLAG_ICON_SVG; if (channel) { channelName = channel.display_name; @@ -77,11 +90,50 @@ export default class SearchResultsItem extends React.Component { } let botIndicator; - if (post.props && post.props.from_webhook) { botIndicator = <li className='bot-indicator'>{Constants.BOT_NAME}</li>; } + let flag; + let flagVisible = ''; + let flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.flag' + defaultMessage='Flag for follow up' + /> + </Tooltip> + ); + if (this.props.isFlagged) { + flagVisible = 'visible'; + flagTooltip = ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id='flag_post.unflag' + defaultMessage='Unflag' + /> + </Tooltip> + ); + flag = ( + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={flagTooltip} + > + <a + href='#' + className={'flag-icon__container ' + flagVisible} + onClick={this.unflagPost} + > + <span + className='icon' + dangerouslySetInnerHTML={{__html: flagIcon}} + /> + </a> + </OverlayTrigger> + ); + } + return ( <div className='search-item__container'> <div className='date-separator'> @@ -126,8 +178,19 @@ export default class SearchResultsItem extends React.Component { minute='2-digit' /> </time> + {flag} </li> - <li> + <li className='col__controls'> + <a + href='#' + className='comment-icon__container search-item__comment' + onClick={this.handleFocusRHSClick} + > + <span + className='comment-icon' + dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} + /> + </a> <a onClick={ () => { @@ -163,18 +226,6 @@ export default class SearchResultsItem extends React.Component { /> </a> </li> - <li> - <a - href='#' - className='comment-icon__container search-item__comment' - onClick={this.handleFocusRHSClick} - > - <span - className='comment-icon' - dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} - /> - </a> - </li> </ul> <div className='search-item-snippet'> <span @@ -197,5 +248,6 @@ SearchResultsItem.propTypes = { isMentionSearch: React.PropTypes.bool, term: React.PropTypes.string, useMilitaryTime: React.PropTypes.bool.isRequired, - shrink: React.PropTypes.function + shrink: React.PropTypes.function, + isFlagged: React.PropTypes.bool }; diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index 7cdb894cc..6d1184799 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -5,12 +5,16 @@ import $ from 'jquery'; import SearchResults from './search_results.jsx'; import RhsThread from './rhs_thread.jsx'; + import SearchStore from 'stores/search_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; -import Constants from 'utils/constants.jsx'; + +import {getFlaggedPosts} from 'actions/post_actions.jsx'; + import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; @@ -101,15 +105,20 @@ export default class SidebarRight extends React.Component { } onPreferenceChange() { + if (this.state.isFlaggedPosts) { + getFlaggedPosts(); + } + this.setState({ useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Constants.Preferences.USE_MILITARY_TIME, false) }); } - onSelectedChange(fromSearch) { + onSelectedChange(fromSearch, fromFlaggedPosts) { this.setState({ postRightVisible: !!PostStore.getSelectedPost(), - fromSearch + fromSearch, + fromFlaggedPosts }); } @@ -120,7 +129,8 @@ export default class SidebarRight extends React.Component { onSearchChange() { this.setState({ searchVisible: SearchStore.getSearchResults() !== null, - isMentionSearch: SearchStore.getIsMentionSearch() + isMentionSearch: SearchStore.getIsMentionSearch(), + isFlaggedPosts: SearchStore.getIsFlaggedPosts() }); } @@ -154,6 +164,7 @@ export default class SidebarRight extends React.Component { content = ( <SearchResults isMentionSearch={this.state.isMentionSearch} + isFlaggedPosts={this.state.isFlaggedPosts} useMilitaryTime={this.state.useMilitaryTime} toggleSize={this.toggleSize} shrink={this.onShrink} @@ -162,6 +173,7 @@ export default class SidebarRight extends React.Component { } else if (this.state.postRightVisible) { content = ( <RhsThread + fromFlaggedPosts={this.state.fromFlaggedPosts} fromSearch={this.state.fromSearch} isMentionSearch={this.state.isMentionSearch} currentUser={this.state.currentUser} diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index b36255a01..576181931 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; +import {getFlaggedPosts} from 'actions/post_actions.jsx'; import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -35,6 +36,7 @@ export default class SidebarRightMenu extends React.Component { this.handleAboutModal = this.handleAboutModal.bind(this); this.searchMentions = this.searchMentions.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); + this.getFlagged = this.getFlagged.bind(this); const state = this.getStateFromStores(); state.showUserSettingsModal = false; @@ -53,6 +55,11 @@ export default class SidebarRightMenu extends React.Component { this.setState({showAboutModal: false}); } + getFlagged(e) { + e.preventDefault(); + getFlaggedPosts(); + } + componentDidMount() { PreferenceStore.addChangeListener(this.onPreferenceChange); } @@ -347,6 +354,18 @@ export default class SidebarRightMenu extends React.Component { <li> <a href='#' + onClick={this.getFlagged} + > + <i className='icon fa fa-flag'></i> + <FormattedMessage + id='sidebar_right_menu.flagged' + defaultMessage='Flagged Posts' + /> + </a> + </li> + <li> + <a + href='#' onClick={() => this.setState({showUserSettingsModal: true})} > <i className='icon fa fa-cog'></i> |