diff options
Diffstat (limited to 'webapp')
31 files changed, 911 insertions, 103 deletions
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 829424c1f..9fc9c7b63 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -190,7 +190,8 @@ export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, postId: Utils.getRootId(post), - from_search: SearchStore.getSearchTerm() + from_search: SearchStore.getSearchTerm(), + from_flagged_posts: SearchStore.getIsFlaggedPosts() }); AppDispatcher.handleServerAction({ diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index 7d830d11b..fd413dfe1 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -9,12 +9,13 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; -import Constants from 'utils/constants.jsx'; -const ActionTypes = Constants.ActionTypes; - import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; +import Constants from 'utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +const Preferences = Constants.Preferences; + export function handleNewPost(post, msg) { if (ChannelStore.getCurrentId() === post.channel_id) { if (window.isActive) { @@ -116,3 +117,38 @@ export function setUnreadPost(channelId, postId) { ChannelStore.emitLastViewed(lastViewed, ownNewMessage); } } + +export function flagPost(postId) { + AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true'); +} + +export function unflagPost(postId, success) { + const pref = { + user_id: UserStore.getCurrentId(), + category: Preferences.CATEGORY_FLAGGED_POST, + name: postId + }; + AsyncClient.deletePreferences([pref], success); +} + +export function getFlaggedPosts() { + Client.getFlaggedPosts(0, Constants.POST_CHUNK_SIZE, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: data, + is_flagged_posts: true + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH_TERM, + term: null, + do_search: false, + is_mention_search: false + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getFlaggedPosts'); + } + ); +} diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index b200b2379..598871002 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1434,6 +1434,15 @@ export default class Client { end(this.handleResponse.bind(this, 'getPostsAfter', success, error)); } + getFlaggedPosts(offset, limit, success, error) { + request. + get(`${this.getTeamNeededRoute()}/posts/flagged/${offset}/${limit}`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getFlaggedPosts', success, error)); + } + // Routes for Files getFileInfo(filename, success, error) { 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> diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 00fbe6ab6..67e366494 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1018,6 +1018,8 @@ "create_comment.files": "Files uploading", "create_post.comment": "Comment", "create_post.post": "Post", + "flag_post.flag": "Flag for follow up", + "flag_post.unflag": "Unflag", "create_post.tutorialTip": "<h4>Sending Messages</h4><p>Type here to write a message and press <strong>Enter</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>", "create_post.write": "Write a message...", "create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.", @@ -1370,6 +1372,8 @@ "post_delete.someone": "Someone deleted the message on which you tried to post a comment.", "post_focus_view.beginning": "Beginning of Channel Archives", "post_info.del": "Delete", + "post_info.mobile.flag": "Flag", + "post_info.mobile.unflag": "Unflag", "post_info.edit": "Edit", "post_info.permalink": "Permalink", "post_info.reply": "Reply", @@ -1393,14 +1397,19 @@ "rename_channel.title": "Rename Channel", "rhs_comment.comment": "Comment", "rhs_comment.del": "Delete", + "rhs_comment.mobile.flag": "Flag", + "rhs_comment.mobile.unflag": "Unflag", "rhs_comment.edit": "Edit", "rhs_comment.permalink": "Permalink", "rhs_header.backToResultsTooltip": "Back to Search Results", + "rhs_header.backToFlaggedTooltip": "Back to Flagged Posts", "rhs_header.closeSidebarTooltip": "Close Sidebar", "rhs_header.details": "Message Details", "rhs_header.expandSidebarTooltip": "Expand Sidebar", "rhs_header.shrinkSidebarTooltip": "Shrink Sidebar", "rhs_root.del": "Delete", + "rhs_root.mobile.flag": "Flag", + "rhs_root.mobile.unflag": "Unflag", "rhs_root.direct": "Direct Message", "rhs_root.edit": "Edit", "rhs_root.permalink": "Permalink", @@ -1409,6 +1418,7 @@ "search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>", "search_header.results": "Search Results", "search_header.title2": "Recent Mentions", + "search_header.title3": "Flagged Posts", "search_item.direct": "Direct Message", "search_item.jump": "Jump", "search_results.because": "<ul><li>If you're searching a partial phrase (ex. searching \"rea\", looking for \"reach\" or \"reaction\"), append a * to your search term.</li><li>Two letter searches and common words like \"this\", \"a\" and \"is\" won't appear in search results due to excessive results returned.</li></ul>", @@ -1440,6 +1450,7 @@ "sidebar.unreadBelow": "Unread post(s) below", "sidebar_header.tutorial": "<h4>Main Menu</h4><p>The <strong>Main Menu</strong> is where you can <strong>Invite New Members</strong>, access your <strong>Account Settings</strong> and set your <strong>Theme Color</strong>.</p><p>Team administrators can also access their <strong>Team Settings</strong> from this menu.</p><p>System administrators will find a <strong>System Console</strong> option to administrate the entire system.</p>", "sidebar_right_menu.accountSettings": "Account Settings", + "sidebar_right_menu.flagged": "Flagged Posts", "sidebar_right_menu.console": "System Console", "sidebar_right_menu.help": "Help", "sidebar_right_menu.inviteNew": "Invite New Member", diff --git a/webapp/sass/components/_search.scss b/webapp/sass/components/_search.scss index d259cfc20..11bcdb92d 100644 --- a/webapp/sass/components/_search.scss +++ b/webapp/sass/components/_search.scss @@ -137,23 +137,31 @@ } } -.search-item__jump { - @include opacity(.8); +.col__controls { font-size: 13px; position: absolute; right: 0; top: 0; - &:hover { - @include opacity(1); + a { + @include opacity(.8); + vertical-align: top; + + &:hover { + @include opacity(1); + } } -} -.search-item__comment { - margin-right: 35px; - position: absolute; - right: 0; - top: 0; + + .search-item__jump { + font-size: 13px; + position: relative; + top: 1px; + } + + .search-item__comment { + margin-right: 5px; + } } .search-item-time { diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss index f2705fc0a..832bed50e 100644 --- a/webapp/sass/layout/_headers.scss +++ b/webapp/sass/layout/_headers.scss @@ -43,6 +43,7 @@ } &:last-child { + padding-right: 8px; width: 8.9%; } } @@ -375,8 +376,17 @@ font-size: 22px; height: 30px; line-height: 26px; - margin-right: 9px; - width: 24px; + margin-right: 3px; + text-align: center; + width: 30px; + + th { + &:last-child { + div { + margin-right: 10px; + } + } + } .channel__wrap.move--left & { position: absolute; @@ -384,6 +394,15 @@ top: 14px; } + .icon__flag { + svg { + height: 18px; + position: relative; + top: 2px; + width: 19px; + } + } + > a { @include opacity(.6); @include single-transition(all, .1s, ease-in); diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index 605c03658..8513f779a 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -486,6 +486,7 @@ body.ios { &:hover { .dropdown, .comment-icon__container, + .flag-icon__container, .post__reply, .post__remove { visibility: visible; @@ -561,7 +562,7 @@ body.ios { .img-div { max-height: 150px; max-width: 150px; - } + } p { line-height: inherit; @@ -573,7 +574,7 @@ body.ios { ol, ul { - clear: both; + clear: both; padding-left: 20px; } } @@ -694,6 +695,13 @@ body.ios { } } + .flag-icon__container { + left: 36px; + margin-left: 5px; + position: absolute; + top: 8px; + } + .post__img { img { display: none; @@ -835,7 +843,9 @@ body.ios { } .post__img { - width: 42px; + padding-right: 10px; + text-align: right; + width: 53px; svg { height: 32px; @@ -1076,6 +1086,7 @@ body.ios { display: inline-block; margin-right: 6px; visibility: hidden; + svg { fill: inherit; position: relative; @@ -1115,6 +1126,36 @@ body.ios { } } + .flag-icon__container { + display: inline-block; + font-size: 12px; + margin-left: 7px; + position: relative; + top: 1px; + visibility: hidden; + + &.visible { + visibility: visible; + } + + path { + fill: inherit; + } + + .fa-star-o { + @include opacity(.8); + } + + &:focus { + outline: none; + } + + &.icon--visible { + visibility: visible; + } + + } + .web-embed-data { @include border-radius(2px); @include alpha-property(background, $black, 0.05); diff --git a/webapp/sass/layout/_sidebar-right.scss b/webapp/sass/layout/_sidebar-right.scss index fb57b6146..497cd3cea 100644 --- a/webapp/sass/layout/_sidebar-right.scss +++ b/webapp/sass/layout/_sidebar-right.scss @@ -161,7 +161,7 @@ .sidebar--right__subheader { font-size: 1em; - padding: 1em 1em 0; + padding: 0.5em 1em 0; h4 { font-size: 1em; @@ -176,6 +176,13 @@ font-size: .95em; padding-bottom: 10px; } + + .usage__icon { + @include opacity(.6); + margin: 0 3px; + position: relative; + top: 1px; + } } .suggestion-list__content { diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index df615aa13..c60233ae8 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -94,6 +94,16 @@ } } + &.same--root { + &.same--user { + .flag-icon__container { + left: auto; + position: relative; + top: 1px; + } + } + } + .post__content { padding: 0 10px 0 0; } @@ -182,7 +192,23 @@ } } + .star-icon__container { + left: auto; + position: relative; + top: auto; + + &:not(.visible) { + display: none; + } + } + &.same--root { + .star-icon__container { + left: auto; + position: relative; + top: auto; + } + &.same--user { .post__header { height: auto; diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index e6cb898fd..bb3d78652 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -116,7 +116,15 @@ .channel__wrap & { .post__time { font-size: .85em; - left: -70px; + left: -79px; + position: absolute; + text-align: right; + top: 6px; + width: 60px; + } + + .star-icon__container { + left: -65px; position: absolute; text-align: right; top: 6px; @@ -125,7 +133,7 @@ } &:not(.post--thread) { - padding: 5px .5em 0 70px; + padding: 5px .5em 0 72px; .post__link { margin: 4px 0 7px; @@ -197,11 +205,29 @@ } } + .flag-icon__container { + left: -21px; + position: absolute; + top: 7px; + } + + .sidebar--right & .flag-icon__container { + left: auto; + position: relative; + top: 1px; + } + &.same--root { &.same--user { - padding-left: 70px; + padding-left: 72px; padding-top: 0; + .flag-icon__container { + left: -19px; + position: absolute; + top: 7px; + } + .post__header { .col__reply { top: -1px; @@ -265,12 +291,16 @@ &:not(.post--compact) { .post__time { + display: inline-block; font-size: 11px; - left: -4px; - line-height: 37px; + left: -14px; + line-height: 34px; position: absolute; + text-align: right; text-rendering: auto; top: -2px; + width: 51px; + } } } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 1c9a877cf..135563866 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -441,8 +441,8 @@ class PostStoreClass extends EventEmitter { return threadPosts; } - emitSelectedPostChange(fromSearch) { - this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); + emitSelectedPostChange(fromSearch, fromFlaggedPosts) { + this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch, fromFlaggedPosts); } addSelectedPostChangeListener(callback) { @@ -599,7 +599,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { break; case ActionTypes.RECEIVED_POST_SELECTED: PostStore.storeSelectedPostId(action.postId); - PostStore.emitSelectedPostChange(action.from_search); + PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts); break; default: } diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx index dc08ca3a6..741590895 100644 --- a/webapp/stores/search_store.jsx +++ b/webapp/stores/search_store.jsx @@ -18,6 +18,7 @@ class SearchStoreClass extends EventEmitter { this.searchResults = null; this.isMentionSearch = false; + this.isFlaggedPosts = false; this.searchTerm = ''; } @@ -77,6 +78,10 @@ class SearchStoreClass extends EventEmitter { return this.isMentionSearch; } + getIsFlaggedPosts() { + return this.isFlaggedPosts; + } + storeSearchTerm(term) { this.searchTerm = term; } @@ -85,9 +90,10 @@ class SearchStoreClass extends EventEmitter { return this.searchTerm; } - storeSearchResults(results, isMentionSearch) { + storeSearchResults(results, isMentionSearch, isFlaggedPosts) { this.searchResults = results; this.isMentionSearch = isMentionSearch; + this.isFlaggedPosts = isFlaggedPosts; } } @@ -98,7 +104,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { switch (action.type) { case ActionTypes.RECEIVED_SEARCH: - SearchStore.storeSearchResults(action.results, action.is_mention_search); + SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts); SearchStore.emitSearchChange(); break; case ActionTypes.RECEIVED_SEARCH_TERM: diff --git a/webapp/tests/client_post.test.jsx b/webapp/tests/client_post.test.jsx index 3c6f05c9f..3b9802fb4 100644 --- a/webapp/tests/client_post.test.jsx +++ b/webapp/tests/client_post.test.jsx @@ -197,5 +197,38 @@ describe('Client.Posts', function() { ); }); }); + + it('getFlaggedPosts', function(done) { + TestHelper.initBasic(() => { + var pref = {}; + pref.user_id = TestHelper.basicUser().id; + pref.category = 'flagged_post'; + pref.name = TestHelper.basicPost().id; + pref.value = 'true'; + + var prefs = []; + prefs.push(pref); + + TestHelper.basicClient().savePreferences( + prefs, + function() { + TestHelper.basicClient().getFlaggedPosts( + 0, + 2, + function(data) { + assert.equal(data.order[0], TestHelper.basicPost().id); + done(); + }, + function(err) { + done(new Error(err.message)); + } + ); + }, + function(err) { + done(new Error(err.message)); + } + ); + }); + }); }); diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 812796ebb..dbdc3e9f1 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -51,7 +51,8 @@ export const Preferences = { COLLAPSE_DISPLAY: 'collapse_previews', COLLAPSE_DISPLAY_DEFAULT: 'false', USE_MILITARY_TIME: 'use_military_time', - CATEGORY_THEME: 'theme' + CATEGORY_THEME: 'theme', + CATEGORY_FLAGGED_POST: 'flagged_post' }; export const ActionTypes = keyMirror({ @@ -326,6 +327,8 @@ export const Constants = { OPEN_TEAM: 'O', MAX_POST_LEN: 4000, EMOJI_SIZE: 16, + FLAG_ICON_OUTLINE_SVG: "<svg width='12px' height='12px' viewBox='0 0 48 48' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' style='fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.41421;'> <g> <g transform='matrix(1,0,0,0.957537,-0.5,1.42123)'> <path d='M2.5,0.5C1.4,0.5 0.5,1.4 0.5,2.5L0.5,45.6C0.5,46.7 1.4,47.6 2.5,47.6C3.6,47.6 4.5,46.7 4.5,45.6L4.5,2.5C4.4,1.4 3.5,0.5 2.5,0.5Z'/> </g> <g transform='matrix(0.923469,0,0,0.870026,1.64285,2.0085)'> <path d='M46.4,3.5C43.3,2.1 40.1,1.4 36.5,1.4C32.1,1.4 27.8,2.4 23.6,3.4C19.4,4.4 15.5,5.3 11.6,5.3C10.5,5.3 9.4,5.2 8.4,5.1L8.4,37C9.4,37.1 10.5,37.2 11.6,37.2C16,37.2 20.3,36.2 24.5,35.2C28.7,34.2 32.6,33.3 36.5,33.3C39.5,33.3 42.3,33.9 44.8,35.1C45.4,35.4 46.1,35.3 46.7,35C47.3,34.6 47.6,34 47.6,33.3L47.6,5.3C47.5,4.6 47.1,3.9 46.4,3.5Z' style='stroke-width:3.23px; fill:none;'/> </g> </g> </svg>", + FLAG_ICON_SVG: "<svg width='12px' height='12px' viewBox='0 0 48 48' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' style='fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.41421;'> <g> <g transform='matrix(1,0,0,0.957537,-0.5,1.42123)'> <path d='M2.5,0.5C1.4,0.5 0.5,1.4 0.5,2.5L0.5,45.6C0.5,46.7 1.4,47.6 2.5,47.6C3.6,47.6 4.5,46.7 4.5,45.6L4.5,2.5C4.4,1.4 3.5,0.5 2.5,0.5Z'/> </g> <g transform='matrix(0.923469,0,0,0.870026,1.64285,2.0085)'> <path d='M46.4,3.5C43.3,2.1 40.1,1.4 36.5,1.4C32.1,1.4 27.8,2.4 23.6,3.4C19.4,4.4 15.5,5.3 11.6,5.3C10.5,5.3 9.4,5.2 8.4,5.1L8.4,37C9.4,37.1 10.5,37.2 11.6,37.2C16,37.2 20.3,36.2 24.5,35.2C28.7,34.2 32.6,33.3 36.5,33.3C39.5,33.3 42.3,33.9 44.8,35.1C45.4,35.4 46.1,35.3 46.7,35C47.3,34.6 47.6,34 47.6,33.3L47.6,5.3C47.5,4.6 47.1,3.9 46.4,3.5Z' style='stroke-width:3.23px;'/> </g> </g> </svg>", ATTACHMENT_ICON_SVG: "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' x='0px' y='0px' viewBox='0 0 48 48' enable-background='new 0 0 48 48' xml:space='preserve'><g><path d='M43.922,6.653c-2.643-2.644-6.201-4.107-9.959-4.069c-3.774,0.019-7.32,1.497-9.983,4.161l-12.3,12.3l-8.523,8.521 c-4.143,4.144-4.217,10.812-0.167,14.862c1.996,1.996,4.626,2.989,7.277,2.989c2.73,0,5.482-1.055,7.583-3.156l15.547-15.545 c0.002-0.002,0.002-0.004,0.004-0.005l5.358-5.358c1.394-1.393,2.176-3.24,2.201-5.2c0.026-1.975-0.716-3.818-2.09-5.192 c-2.834-2.835-7.496-2.787-10.394,0.108L9.689,29.857c-0.563,0.563-0.563,1.474,0,2.036c0.281,0.28,0.649,0.421,1.018,0.421 c0.369,0,0.737-0.141,1.018-0.421l18.787-18.788c1.773-1.774,4.609-1.824,6.322-0.11c0.82,0.82,1.263,1.928,1.247,3.119 c-0.017,1.205-0.497,2.342-1.357,3.201l-5.55,5.551c-0.002,0.002-0.002,0.004-0.004,0.005L15.814,40.225 c-3.02,3.02-7.86,3.094-10.789,0.167c-2.928-2.929-2.854-7.77,0.167-10.791l0.958-0.958c0.001-0.002,0.004-0.002,0.005-0.004 L26.016,8.78c2.123-2.124,4.951-3.303,7.961-3.317c2.998,0.02,5.814,1.13,7.91,3.226c4.35,4.351,4.309,11.472-0.093,15.873 L25.459,40.895c-0.563,0.562-0.563,1.473,0,2.035c0.281,0.281,0.65,0.422,1.018,0.422c0.369,0,0.737-0.141,1.018-0.422 L43.83,26.596C49.354,21.073,49.395,12.126,43.922,6.653z'></path></g></svg>", MATTERMOST_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='0 0 500 500' style='enable-background:new 0 0 500 500;' xml:space='preserve'> <style type='text/css'> .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#222222;} </style> <g id='XMLID_1_'> <g id='XMLID_3_'> <path id='XMLID_4_' class='st0' d='M396.9,47.7l2.6,53.1c43,47.5,60,114.8,38.6,178.1c-32,94.4-137.4,144.1-235.4,110.9 S51.1,253.1,83,158.7C104.5,95.2,159.2,52,222.5,40.5l34.2-40.4C150-2.8,49.3,63.4,13.3,169.9C-31,300.6,39.1,442.5,169.9,486.7 s272.6-25.8,316.9-156.6C522.7,223.9,483.1,110.3,396.9,47.7z'/> </g> <path id='XMLID_2_' class='st0' d='M335.6,204.3l-1.8-74.2l-1.5-42.7l-1-37c0,0,0.2-17.8-0.4-22c-0.1-0.9-0.4-1.6-0.7-2.2 c0-0.1-0.1-0.2-0.1-0.3c0-0.1-0.1-0.2-0.1-0.2c-0.7-1.2-1.8-2.1-3.1-2.6c-1.4-0.5-2.9-0.4-4.2,0.2c0,0-0.1,0-0.1,0 c-0.2,0.1-0.3,0.1-0.4,0.2c-0.6,0.3-1.2,0.7-1.8,1.3c-3,3-13.7,17.2-13.7,17.2l-23.2,28.8l-27.1,33l-46.5,57.8 c0,0-21.3,26.6-16.6,59.4s29.1,48.7,48,55.1c18.9,6.4,48,8.5,71.6-14.7C336.4,238.4,335.6,204.3,335.6,204.3z'/> </g> </svg>", ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>", diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 187c7d7f4..9f8b1ef6c 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -566,7 +566,8 @@ export function applyTheme(theme) { } if (theme.centerChannelColor) { - changeCss('.app__body .post-list__arrows', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3), 1); + changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3), 1); + changeCss('.app__body .channel-header__links .icon', 'stroke:' + theme.centerChannelColor, 1); changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2); changeCss('.app__body .sidebar--left, .app__body .sidebar--right .sidebar--right__header, .app__body .suggestion-list__content .command', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.app__body .post.post--system .post__body', 'color:' + changeOpacity(theme.centerChannelColor, 0.6), 1); @@ -638,7 +639,8 @@ export function applyTheme(theme) { if (theme.linkColor) { changeCss('.app__body a, .app__body a:focus, .app__body a:hover, .app__body .btn, .app__body .btn:focus, .app__body .btn:hover', 'color:' + theme.linkColor, 1); changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + changeOpacity(theme.linkColor, 0.5), 1); - changeCss('.app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor, 1); + changeCss('.app__body .channel-header__links .icon:hover, .app__body .post .flag-icon__container.visible, .app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor, 1); + changeCss('.app__body .channel-header__links .icon:hover', 'stroke:' + theme.linkColor, 1); } if (theme.buttonBg) { |