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 aaa05632c5.

* Fixing the icon in search

* Help text and white space improvements (#3730)

* Updatng help text and some white spacing.

* Updating help text
This commit is contained in:
Joram Wilander
2016-08-04 10:38:09 -05:00
committed by enahum
parent 9b50b50283
commit 0184d6059b
38 changed files with 1078 additions and 104 deletions
+28
View File
@@ -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)
+35
View File
@@ -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")
}
}
+12
View File
@@ -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)
}
+1
View File
@@ -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"
+25
View File
@@ -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)
+64
View File
@@ -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")
}
}
+1
View File
@@ -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
+2 -1
View File
@@ -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({
+39 -3
View File
@@ -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');
}
);
}
+9
View File
@@ -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) {
+44 -7
View File
@@ -26,19 +26,18 @@ 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;
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';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap';
export default class ChannelHeader extends React.Component {
constructor(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}
+115 -1
View File
@@ -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
};
+38 -19
View File
@@ -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
});
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
});
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
};
+121 -8
View File
@@ -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
};
+21 -1
View File
@@ -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,
+40 -2
View File
@@ -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
};
+9 -1
View File
@@ -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
};
+69 -17
View File
@@ -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
};
+16 -4
View File
@@ -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}
+19
View File
@@ -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);
}
@@ -344,6 +351,18 @@ export default class SidebarRightMenu extends React.Component {
/>
</a>
</li>
<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='#'
+11
View File
@@ -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",
+19 -11
View File
@@ -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;
.search-item__comment {
margin-right: 35px;
position: absolute;
right: 0;
top: 0;
&:hover {
@include opacity(1);
}
}
.search-item__jump {
font-size: 13px;
position: relative;
top: 1px;
}
.search-item__comment {
margin-right: 5px;
}
}
.search-item-time {
+21 -2
View File
@@ -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);
+44 -3
View File
@@ -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);
+8 -1
View File
@@ -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 {
+26
View File
@@ -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;
+35 -5
View File
@@ -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;
}
}
}
+3 -3
View File
@@ -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:
}
+8 -2
View File
@@ -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:
+33
View File
@@ -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));
}
);
});
});
});
+4 -1
View File
@@ -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>",
+4 -2
View File
@@ -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) {