PLT-3506 Added flagged posts functionality (#3679)

* Added flagged posts functionality

* UI Improvements to flags (#3697)

* Added flag functionality for mobile

* Updating flagged text (#3699)

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

* Updating position of flags (#3708)

* Plt 3506 - Reverting flag position (#3724)

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

This reverts commit aaa05632c5d9eda35a048300a5bd7e99584c5b58.

* Fixing the icon in search

* Help text and white space improvements (#3730)

* Updatng help text and some white spacing.

* Updating help text
Этот коммит содержится в:
Joram Wilander
2016-08-04 11:38:09 -04:00
коммит произвёл enahum
родитель 9b50b50283
Коммит 0184d6059b
38 изменённых файлов: 1078 добавлений и 104 удалений

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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")
}
}

Просмотреть файл

@@ -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)
}

Просмотреть файл

@@ -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"

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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")
}
}

Просмотреть файл

@@ -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

Просмотреть файл

@@ -190,7 +190,8 @@ export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED,
postId: Utils.getRootId(post),
from_search: SearchStore.getSearchTerm()
from_search: SearchStore.getSearchTerm(),
from_flagged_posts: SearchStore.getIsFlaggedPosts()
});
AppDispatcher.handleServerAction({

Просмотреть файл

@@ -9,12 +9,13 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const Preferences = Constants.Preferences;
export function handleNewPost(post, msg) {
if (ChannelStore.getCurrentId() === post.channel_id) {
if (window.isActive) {
@@ -116,3 +117,38 @@ export function setUnreadPost(channelId, postId) {
ChannelStore.emitLastViewed(lastViewed, ownNewMessage);
}
}
export function flagPost(postId) {
AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true');
}
export function unflagPost(postId, success) {
const pref = {
user_id: UserStore.getCurrentId(),
category: Preferences.CATEGORY_FLAGGED_POST,
name: postId
};
AsyncClient.deletePreferences([pref], success);
}
export function getFlaggedPosts() {
Client.getFlaggedPosts(0, Constants.POST_CHUNK_SIZE,
(data) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: data,
is_flagged_posts: true
});
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH_TERM,
term: null,
do_search: false,
is_mention_search: false
});
},
(err) => {
AsyncClient.dispatchError(err, 'getFlaggedPosts');
}
);
}

Просмотреть файл

@@ -1434,6 +1434,15 @@ export default class Client {
end(this.handleResponse.bind(this, 'getPostsAfter', success, error));
}
getFlaggedPosts(offset, limit, success, error) {
request.
get(`${this.getTeamNeededRoute()}/posts/flagged/${offset}/${limit}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getFlaggedPosts', success, error));
}
// Routes for Files
getFileInfo(filename, success, error) {

Просмотреть файл

@@ -26,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}

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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,

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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}

Просмотреть файл

@@ -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='#'

Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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;

Просмотреть файл

@@ -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;
}
}
}

Просмотреть файл

@@ -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:
}

Просмотреть файл

@@ -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:

Просмотреть файл

@@ -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));
}
);
});
});
});

Просмотреть файл

@@ -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>",

Просмотреть файл

@@ -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) {