Commit fc89c20b authored by Joram Wilander's avatar Joram Wilander Committed by enahum
Browse files

PLT-3506 Added flagged posts functionality (#3679)

* Added flagged posts functionality

* UI Improvements to flags (#3697)

* Added flag functionality for mobile

* Updating flagged text (#3699)

* Add back button to RHS thread when coming from flagged posts

* Updating position of flags (#3708)

* Plt 3506 - Reverting flag position (#3724)

* Revert "Updating position of flags (#3708)"

This reverts commit aaa05632c5d9eda35a048300a5bd7e99584c5b58.

* Fixing the icon in search

* Help text and white space improvements (#3730)

* Updatng help text and some white spacing.

* Updating help text
parent 248f45f2
......@@ -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({
......
......@@ -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');
}
);
}
......@@ -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) {
......
......@@ -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>
......
......@@ -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
};
......@@ -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
};
......@@ -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
};
......@@ -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
};
......@@ -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}
/>
);
}
......
......@@ -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}
......
......@@ -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}
>