summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/post.go28
-rw-r--r--api/post_test.go35
-rw-r--r--model/client.go12
-rw-r--r--model/preference.go1
-rw-r--r--store/sql_post_store.go25
-rw-r--r--store/sql_post_store_test.go64
-rw-r--r--store/store.go1
-rw-r--r--webapp/actions/global_actions.jsx3
-rw-r--r--webapp/actions/post_actions.jsx42
-rw-r--r--webapp/client/client.jsx9
-rw-r--r--webapp/components/channel_header.jsx49
-rw-r--r--webapp/components/post_view/components/post.jsx8
-rw-r--r--webapp/components/post_view/components/post_header.jsx4
-rw-r--r--webapp/components/post_view/components/post_info.jsx119
-rw-r--r--webapp/components/post_view/components/post_list.jsx9
-rw-r--r--webapp/components/post_view/post_focus_view_controller.jsx13
-rw-r--r--webapp/components/post_view/post_view_controller.jsx11
-rw-r--r--webapp/components/rhs_comment.jsx116
-rw-r--r--webapp/components/rhs_header_post.jsx59
-rw-r--r--webapp/components/rhs_root_post.jsx129
-rw-r--r--webapp/components/rhs_thread.jsx22
-rw-r--r--webapp/components/search_results.jsx42
-rw-r--r--webapp/components/search_results_header.jsx10
-rw-r--r--webapp/components/search_results_item.jsx86
-rw-r--r--webapp/components/sidebar_right.jsx20
-rw-r--r--webapp/components/sidebar_right_menu.jsx19
-rw-r--r--webapp/i18n/en.json11
-rw-r--r--webapp/sass/components/_search.scss28
-rw-r--r--webapp/sass/layout/_headers.scss23
-rw-r--r--webapp/sass/layout/_post.scss47
-rw-r--r--webapp/sass/layout/_sidebar-right.scss9
-rw-r--r--webapp/sass/responsive/_mobile.scss26
-rw-r--r--webapp/sass/responsive/_tablet.scss40
-rw-r--r--webapp/stores/post_store.jsx6
-rw-r--r--webapp/stores/search_store.jsx10
-rw-r--r--webapp/tests/client_post.test.jsx33
-rw-r--r--webapp/utils/constants.jsx5
-rw-r--r--webapp/utils/utils.jsx6
38 files changed, 1077 insertions, 103 deletions
diff --git a/api/post.go b/api/post.go
index 223e8ee15..46c0284d0 100644
--- a/api/post.go
+++ b/api/post.go
@@ -34,6 +34,7 @@ func InitPost() {
l4g.Debug(utils.T("api.post.init.debug"))
BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("POST")
+ BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getFlaggedPosts, false)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).Methods("GET")
@@ -1034,6 +1035,33 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getFlaggedPosts", "offset")
+ return
+ }
+
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getFlaggedPosts", "limit")
+ return
+ }
+
+ posts := &model.PostList{}
+
+ if result := <-Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ posts = result.Data.(*model.PostList)
+ }
+
+ w.Write([]byte(posts.ToJson()))
+}
+
func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
diff --git a/api/post_test.go b/api/post_test.go
index 29fe63ddc..2a2a9f41b 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -924,3 +924,38 @@ func TestGetOutOfChannelMentions(t *testing.T) {
t.Fatalf("getOutOfChannelMentions returned %v when two users on a different team were mentioned", mentioned)
}
}
+
+func TestGetFlaggedPosts(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+ user1 := th.BasicUser
+ post1 := th.BasicPost
+
+ preferences := &model.Preferences{
+ {
+ UserId: user1.Id,
+ Category: model.PREFERENCE_CATEGORY_FLAGGED_POST,
+ Name: post1.Id,
+ Value: "true",
+ },
+ }
+ Client.Must(Client.SetPreferences(preferences))
+
+ r1 := Client.Must(Client.GetFlaggedPosts(0, 2)).Data.(*model.PostList)
+
+ if len(r1.Order) == 0 {
+ t.Fatal("should have gotten a flagged post")
+ }
+
+ if _, ok := r1.Posts[post1.Id]; !ok {
+ t.Fatal("missing flagged post")
+ }
+
+ Client.DeletePreferences(preferences)
+
+ r2 := Client.Must(Client.GetFlaggedPosts(0, 2)).Data.(*model.PostList)
+
+ if len(r2.Order) != 0 {
+ t.Fatal("should not have gotten a flagged post")
+ }
+}
diff --git a/model/client.go b/model/client.go
index cad551613..b9a5d8830 100644
--- a/model/client.go
+++ b/model/client.go
@@ -1228,6 +1228,18 @@ func (c *Client) SearchPosts(terms string, isOrSearch bool) (*Result, *AppError)
}
}
+// GetFlaggedPosts will return a post list of posts that have been flagged by the user.
+// The page is set by the integer parameters offset and limit.
+func (c *Client) GetFlaggedPosts(offset int, limit int) (*Result, *AppError) {
+ if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/posts/flagged/%v/%v", offset, limit), "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
}
diff --git a/model/preference.go b/model/preference.go
index b74e25d81..5787fe6ef 100644
--- a/model/preference.go
+++ b/model/preference.go
@@ -15,6 +15,7 @@ const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
+ PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 57bb2a512..07192b4a6 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -142,6 +142,31 @@ func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags
return storeChannel
}
+func (s SqlPostStore) GetFlaggedPosts(userId string, offset int, limit int) StoreChannel {
+ storeChannel := make(StoreChannel)
+ go func() {
+ result := StoreResult{}
+ pl := &model.PostList{}
+
+ var posts []*model.Post
+ if _, err := s.GetReplica().Select(&posts, "SELECT * FROM Posts WHERE Id IN (SELECT Name FROM Preferences WHERE UserId = :UserId AND Category = :Category) ORDER BY CreateAt ASC LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Category": model.PREFERENCE_CATEGORY_FLAGGED_POST, "Offset": offset, "Limit": limit}); err != nil {
+ result.Err = model.NewLocAppError("SqlPostStore.GetFlaggedPosts", "store.sql_post.get_flagged_posts.app_error", nil, err.Error())
+ } else {
+ for _, post := range posts {
+ pl.AddPost(post)
+ pl.AddOrder(post.Id)
+ }
+ }
+
+ result.Data = pl
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlPostStore) Get(id string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index 3c317b926..d8f8c2e6b 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -905,3 +905,67 @@ func TestPostCountsByDay(t *testing.T) {
}
}
}
+
+func TestPostStoreGetFlaggedPosts(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ o2 := &model.Post{}
+ o2.ChannelId = o1.ChannelId
+ o2.UserId = model.NewId()
+ o2.Message = "a" + model.NewId() + "b"
+ o2 = (<-store.Post().Save(o2)).Data.(*model.Post)
+ time.Sleep(2 * time.Millisecond)
+
+ r1 := (<-store.Post().GetFlaggedPosts(o1.ChannelId, 0, 2)).Data.(*model.PostList)
+
+ if len(r1.Order) != 0 {
+ t.Fatal("should be empty")
+ }
+
+ preferences := model.Preferences{
+ {
+ UserId: o1.UserId,
+ Category: model.PREFERENCE_CATEGORY_FLAGGED_POST,
+ Name: o1.Id,
+ Value: "true",
+ },
+ }
+
+ Must(store.Preference().Save(&preferences))
+
+ r2 := (<-store.Post().GetFlaggedPosts(o1.UserId, 0, 2)).Data.(*model.PostList)
+
+ if len(r2.Order) != 1 {
+ t.Fatal("should have 1 post")
+ }
+
+ preferences = model.Preferences{
+ {
+ UserId: o1.UserId,
+ Category: model.PREFERENCE_CATEGORY_FLAGGED_POST,
+ Name: o2.Id,
+ Value: "true",
+ },
+ }
+
+ Must(store.Preference().Save(&preferences))
+
+ r3 := (<-store.Post().GetFlaggedPosts(o1.UserId, 0, 1)).Data.(*model.PostList)
+
+ if len(r3.Order) != 1 {
+ t.Fatal("should have 1 post")
+ }
+
+ r4 := (<-store.Post().GetFlaggedPosts(o1.UserId, 0, 2)).Data.(*model.PostList)
+
+ if len(r4.Order) != 2 {
+ t.Fatal("should have 2 posts")
+ }
+}
diff --git a/store/store.go b/store/store.go
index 66cc05214..b9a55fa2e 100644
--- a/store/store.go
+++ b/store/store.go
@@ -112,6 +112,7 @@ type PostStore interface {
Delete(postId string, time int64) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
GetPosts(channelId string, offset int, limit int) StoreChannel
+ GetFlaggedPosts(userId string, offset int, limit int) StoreChannel
GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel
GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel
GetPostsSince(channelId string, time int64) StoreChannel
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) {