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")) l4g.Debug(utils.T("api.post.init.debug"))
BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("POST") 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("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/pltmp/{post_id}", ApiUserRequired(getPermalinkTmp)).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) { func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r) 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) 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) { func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) {
return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType) return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType)
} }

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

@@ -15,6 +15,7 @@ const (
PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show" PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show"
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step" PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings" PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings" PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews" PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"

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

@@ -142,6 +142,31 @@ func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags
return storeChannel 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 { func (s SqlPostStore) Get(id string) StoreChannel {
storeChannel := make(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 Delete(postId string, time int64) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel PermanentDeleteByUser(userId string) StoreChannel
GetPosts(channelId string, offset int, limit int) 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 GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel
GetPostsAfter(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 GetPostsSince(channelId string, time int64) StoreChannel

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

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

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

@@ -9,12 +9,13 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import * as PostUtils from 'utils/post_utils.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 Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_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) { export function handleNewPost(post, msg) {
if (ChannelStore.getCurrentId() === post.channel_id) { if (ChannelStore.getCurrentId() === post.channel_id) {
if (window.isActive) { if (window.isActive) {
@@ -116,3 +117,38 @@ export function setUnreadPost(channelId, postId) {
ChannelStore.emitLastViewed(lastViewed, ownNewMessage); 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)); 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 // Routes for Files
getFileInfo(filename, success, error) { getFileInfo(filename, success, error) {

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

@@ -26,19 +26,18 @@ import PreferenceStore from 'stores/preference_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as TextFormatting from 'utils/text_formatting.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 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'; import Constants from 'utils/constants.jsx';
const UserStatuses = Constants.UserStatuses; const UserStatuses = Constants.UserStatuses;
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap';
import React from 'react'; 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 { export default class ChannelHeader extends React.Component {
constructor(props) { constructor(props) {
@@ -50,6 +49,7 @@ export default class ChannelHeader extends React.Component {
this.showRenameChannelModal = this.showRenameChannelModal.bind(this); this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
this.openRecentMentions = this.openRecentMentions.bind(this); this.openRecentMentions = this.openRecentMentions.bind(this);
this.getFlagged = this.getFlagged.bind(this);
const state = this.getStateFromStores(); const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false; state.showEditChannelPurposeModal = false;
@@ -159,6 +159,11 @@ export default class ChannelHeader extends React.Component {
}); });
} }
getFlagged(e) {
e.preventDefault();
getFlaggedPosts();
}
openRecentMentions(e) { openRecentMentions(e) {
if (Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) { if (Utils.cmdOrCtrlPressed(e) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) {
e.preventDefault(); e.preventDefault();
@@ -220,6 +225,8 @@ export default class ChannelHeader extends React.Component {
} }
render() { render() {
const flagIcon = Constants.FLAG_ICON_OUTLINE_SVG;
if (!this.validState()) { if (!this.validState()) {
return null; return null;
} }
@@ -233,6 +240,16 @@ export default class ChannelHeader extends React.Component {
/> />
</Tooltip> </Tooltip>
); );
const flaggedTooltip = (
<Tooltip id='flaggedTooltip'>
<FormattedMessage
id='channel_header.flagged'
defaultMessage='Flagged Posts'
/>
</Tooltip>
);
const popoverContent = ( const popoverContent = (
<Popover <Popover
id='header-popover' id='header-popover'
@@ -592,6 +609,26 @@ export default class ChannelHeader extends React.Component {
</OverlayTrigger> </OverlayTrigger>
</div> </div>
</th> </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> </tr>
</tbody> </tbody>
</table> </table>

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

@@ -100,6 +100,10 @@ export default class Post extends React.Component {
return true; return true;
} }
if (nextProps.isFlagged !== this.props.isFlagged) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true; return true;
} }
@@ -245,6 +249,7 @@ export default class Post extends React.Component {
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
displayNameType={this.props.displayNameType} displayNameType={this.props.displayNameType}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
isFlagged={this.props.isFlagged}
/> />
<PostBody <PostBody
post={post} post={post}
@@ -281,5 +286,6 @@ Post.propTypes = {
commentCount: React.PropTypes.number, commentCount: React.PropTypes.number,
isCommentMention: React.PropTypes.bool, isCommentMention: React.PropTypes.bool,
useMilitaryTime: React.PropTypes.bool.isRequired, 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} currentUser={this.props.currentUser}
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
isFlagged={this.props.isFlagged}
/> />
</li> </li>
</ul> </ul>
@@ -97,5 +98,6 @@ PostHeader.propTypes = {
sameUser: React.PropTypes.bool.isRequired, sameUser: React.PropTypes.bool.isRequired,
compactDisplay: React.PropTypes.bool, compactDisplay: React.PropTypes.bool,
displayNameType: React.PropTypes.string, 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. // See License.txt for license information.
import $ from 'jquery'; import $ from 'jquery';
import * as Utils from 'utils/utils.jsx';
import PostTime from './post_time.jsx'; import PostTime from './post_time.jsx';
import * as GlobalActions from 'actions/global_actions.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 TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class PostInfo extends React.Component { export default class PostInfo extends React.Component {
constructor(props) { constructor(props) {
@@ -21,7 +25,10 @@ export default class PostInfo extends React.Component {
this.handleDropdownClick = this.handleDropdownClick.bind(this); this.handleDropdownClick = this.handleDropdownClick.bind(this);
this.handlePermalink = this.handlePermalink.bind(this); this.handlePermalink = this.handlePermalink.bind(this);
this.removePost = this.removePost.bind(this); this.removePost = this.removePost.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
} }
handleDropdownClick(e) { handleDropdownClick(e) {
var position = $('#post-list').height() - $(e.target).offset().top; var position = $('#post-list').height() - $(e.target).offset().top;
var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu'); var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu');
@@ -29,10 +36,12 @@ export default class PostInfo extends React.Component {
dropdown.addClass('bottom'); dropdown.addClass('bottom');
} }
} }
componentDidMount() { componentDidMount() {
$('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', () => this.props.handleDropdownOpened(true)); $('#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)); $('#post_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false));
} }
createDropdown() { createDropdown() {
var post = this.props.post; var post = this.props.post;
var isOwner = this.props.currentUser.id === post.user_id; 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( dropdownContents.push(
<li <li
key='copyLink' 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() { render() {
var post = this.props.post; var post = this.props.post;
var comments = ''; var comments = '';
var showCommentClass = ''; var showCommentClass = '';
var highlightMentionClass = ''; var highlightMentionClass = '';
var commentCountText = this.props.commentCount; var commentCountText = this.props.commentCount;
const flagIcon = Constants.FLAG_ICON_SVG;
if (this.props.commentCount >= 1) { if (this.props.commentCount >= 1) {
showCommentClass = ' icon--show'; 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 ( return (
<ul className='post__header--info'> <ul className='post__header--info'>
<li className='col'> <li className='col'>
@@ -249,6 +345,20 @@ export default class PostInfo extends React.Component {
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
useMilitaryTime={this.props.useMilitaryTime} 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> </li>
{options} {options}
</ul> </ul>
@@ -274,5 +384,6 @@ PostInfo.propTypes = {
sameUser: React.PropTypes.bool.isRequired, sameUser: React.PropTypes.bool.isRequired,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
compactDisplay: React.PropTypes.bool, 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 = ( const postCtl = (
<Post <Post
key={keyPrefix + 'postKey'} key={keyPrefix + 'postKey'}
@@ -305,6 +310,7 @@ export default class PostList extends React.Component {
previewCollapsed={this.props.previewsCollapsed} previewCollapsed={this.props.previewsCollapsed}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
emojis={this.props.emojis} emojis={this.props.emojis}
isFlagged={isFlagged}
/> />
); );
@@ -572,5 +578,6 @@ PostList.propTypes = {
previewsCollapsed: React.PropTypes.string, previewsCollapsed: React.PropTypes.string,
useMilitaryTime: React.PropTypes.bool.isRequired, useMilitaryTime: React.PropTypes.bool.isRequired,
isFocusPost: React.PropTypes.bool, 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 PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const ScrollTypes = Constants.ScrollTypes; const ScrollTypes = Constants.ScrollTypes;
@@ -22,6 +23,7 @@ export default class PostFocusView extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this);
this.onUserChange = this.onUserChange.bind(this); this.onUserChange = this.onUserChange.bind(this);
this.onEmojiChange = this.onEmojiChange.bind(this); this.onEmojiChange = this.onEmojiChange.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onPostListScroll = this.onPostListScroll.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this);
const focusedPostId = PostStore.getFocusedPostId(); const focusedPostId = PostStore.getFocusedPostId();
@@ -41,7 +43,8 @@ export default class PostFocusView extends React.Component {
scrollPostId: focusedPostId, scrollPostId: focusedPostId,
atTop: PostStore.getVisibilityAtTop(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId),
atBottom: PostStore.getVisibilityAtBottom(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); PostStore.addChangeListener(this.onPostsChange);
UserStore.addChangeListener(this.onUserChange); UserStore.addChangeListener(this.onUserChange);
EmojiStore.addChangeListener(this.onEmojiChange); EmojiStore.addChangeListener(this.onEmojiChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
} }
componentWillUnmount() { componentWillUnmount() {
@@ -98,6 +102,12 @@ export default class PostFocusView extends React.Component {
}); });
} }
onPreferenceChange() {
this.setState({
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
});
}
onPostListScroll() { onPostListScroll() {
this.setState({scrollType: ScrollTypes.FREE}); this.setState({scrollType: ScrollTypes.FREE});
} }
@@ -128,6 +138,7 @@ export default class PostFocusView extends React.Component {
postsToHighlight={postsToHighlight} postsToHighlight={postsToHighlight}
isFocusPost={true} isFocusPost={true}
emojis={this.state.emojis} 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, 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'), previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
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),
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, 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, 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, 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; return true;
} }
if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) {
return true;
}
if (nextState.lastViewed !== this.state.lastViewed) { if (nextState.lastViewed !== this.state.lastViewed) {
return true; return true;
} }
@@ -292,6 +298,7 @@ export default class PostViewController extends React.Component {
compactDisplay={this.state.compactDisplay} compactDisplay={this.state.compactDisplay}
previewsCollapsed={this.state.previewsCollapsed} previewsCollapsed={this.state.previewsCollapsed}
useMilitaryTime={this.state.useMilitaryTime} useMilitaryTime={this.state.useMilitaryTime}
flaggedPosts={this.state.flaggedPosts}
lastViewed={this.state.lastViewed} lastViewed={this.state.lastViewed}
emojis={this.state.emojis} emojis={this.state.emojis}
ownNewMessage={this.state.ownNewMessage} ownNewMessage={this.state.ownNewMessage}

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

@@ -9,12 +9,14 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import * as GlobalActions from 'actions/global_actions.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 TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedDate} from 'react-intl';
@@ -27,13 +29,17 @@ export default class RhsComment extends React.Component {
super(props); super(props);
this.handlePermalink = this.handlePermalink.bind(this); this.handlePermalink = this.handlePermalink.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
this.state = {}; this.state = {};
} }
handlePermalink(e) { handlePermalink(e) {
e.preventDefault(); e.preventDefault();
GlobalActions.showGetPostLinkModal(this.props.post); GlobalActions.showGetPostLinkModal(this.props.post);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
if (nextProps.compactDisplay !== this.props.compactDisplay) { if (nextProps.compactDisplay !== this.props.compactDisplay) {
return true; return true;
@@ -43,6 +49,10 @@ export default class RhsComment extends React.Component {
return true; return true;
} }
if (nextProps.isFlagged !== this.props.isFlagged) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true; return true;
} }
@@ -53,6 +63,17 @@ export default class RhsComment extends React.Component {
return false; return false;
} }
flagPost(e) {
e.preventDefault();
flagPost(this.props.post.id);
}
unflagPost(e) {
e.preventDefault();
unflagPost(this.props.post.id);
}
createDropdown() { createDropdown() {
var post = this.props.post; var post = this.props.post;
@@ -66,6 +87,44 @@ export default class RhsComment extends React.Component {
var dropdownContents = []; 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( dropdownContents.push(
<li <li
key='rhs-root-permalink' key='rhs-root-permalink'
@@ -151,8 +210,10 @@ export default class RhsComment extends React.Component {
</div> </div>
); );
} }
render() { render() {
var post = this.props.post; var post = this.props.post;
const flagIcon = Constants.FLAG_ICON_SVG;
var currentUserCss = ''; var currentUserCss = '';
if (this.props.currentUser === post.user_id) { 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 ( return (
<div className={'post post--thread ' + currentUserCss + ' ' + compactClass}> <div className={'post post--thread ' + currentUserCss + ' ' + compactClass}>
<div className='post__content'> <div className='post__content'>
@@ -247,6 +346,20 @@ export default class RhsComment extends React.Component {
minute='2-digit' minute='2-digit'
/> />
</time> </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>
<li className='col col__reply'> <li className='col col__reply'>
{dropdown} {dropdown}
@@ -271,5 +384,6 @@ RhsComment.propTypes = {
user: React.PropTypes.object.isRequired, user: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
compactDisplay: React.PropTypes.bool, 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 AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import * as GlobalActions from 'actions/global_actions.jsx'; import * as GlobalActions from 'actions/global_actions.jsx';
import {getFlaggedPosts} from 'actions/post_actions.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
@@ -34,17 +36,21 @@ export default class RhsHeaderPost extends React.Component {
handleBack(e) { handleBack(e) {
e.preventDefault(); e.preventDefault();
AppDispatcher.handleServerAction({ if (this.props.fromSearch) {
type: ActionTypes.RECEIVED_SEARCH_TERM, AppDispatcher.handleServerAction({
term: this.props.fromSearch, type: ActionTypes.RECEIVED_SEARCH_TERM,
do_search: true, term: this.props.fromSearch,
is_mention_search: this.props.isMentionSearch do_search: true,
}); is_mention_search: this.props.isMentionSearch
});
AppDispatcher.handleServerAction({ AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST_SELECTED, type: ActionTypes.RECEIVED_POST_SELECTED,
postId: null postId: null
}); });
} else if (this.props.fromFlaggedPosts) {
getFlaggedPosts();
}
} }
render() { render() {
let back; let back;
@@ -57,14 +63,26 @@ export default class RhsHeaderPost extends React.Component {
</Tooltip> </Tooltip>
); );
const backToResultsTooltip = ( let backToResultsTooltip;
<Tooltip id='backToResultsTooltip'> if (this.props.fromSearch) {
<FormattedMessage backToResultsTooltip = (
id='rhs_header.backToResultsTooltip' <Tooltip id='backToResultsTooltip'>
defaultMessage='Back to Search Results' <FormattedMessage
/> id='rhs_header.backToResultsTooltip'
</Tooltip> 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 = ( const expandSidebarTooltip = (
<Tooltip id='expandSidebarTooltip'> <Tooltip id='expandSidebarTooltip'>
@@ -84,7 +102,7 @@ export default class RhsHeaderPost extends React.Component {
</Tooltip> </Tooltip>
); );
if (this.props.fromSearch) { if (this.props.fromSearch || this.props.fromFlaggedPosts) {
back = ( back = (
<a <a
href='#' href='#'
@@ -161,6 +179,7 @@ RhsHeaderPost.defaultProps = {
RhsHeaderPost.propTypes = { RhsHeaderPost.propTypes = {
isMentionSearch: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool,
fromSearch: React.PropTypes.string, fromSearch: React.PropTypes.string,
fromFlaggedPosts: React.PropTypes.bool,
toggleSize: React.PropTypes.function, toggleSize: React.PropTypes.function,
shrink: React.PropTypes.function shrink: React.PropTypes.function
}; };

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

@@ -1,19 +1,23 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ChannelStore from 'stores/channel_store.jsx';
import UserProfile from './user_profile.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 UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_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 * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_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 Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedDate} from 'react-intl';
@@ -24,13 +28,17 @@ export default class RhsRootPost extends React.Component {
super(props); super(props);
this.handlePermalink = this.handlePermalink.bind(this); this.handlePermalink = this.handlePermalink.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
this.state = {}; this.state = {};
} }
handlePermalink(e) { handlePermalink(e) {
e.preventDefault(); e.preventDefault();
GlobalActions.showGetPostLinkModal(this.props.post); GlobalActions.showGetPostLinkModal(this.props.post);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
if (nextProps.compactDisplay !== this.props.compactDisplay) { if (nextProps.compactDisplay !== this.props.compactDisplay) {
return true; return true;
@@ -40,6 +48,10 @@ export default class RhsRootPost extends React.Component {
return true; return true;
} }
if (nextProps.isFlagged !== this.props.isFlagged) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true; return true;
} }
@@ -50,6 +62,17 @@ export default class RhsRootPost extends React.Component {
return false; return false;
} }
flagPost(e) {
e.preventDefault();
flagPost(this.props.post.id);
}
unflagPost(e) {
e.preventDefault();
unflagPost(this.props.post.id);
}
render() { render() {
const post = this.props.post; const post = this.props.post;
const user = this.props.user; 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); const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var timestamp = UserStore.getProfile(post.user_id).update_at; var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id); var channel = ChannelStore.get(post.channel_id);
const flagIcon = Constants.FLAG_ICON_SVG;
var type = 'Post'; var type = 'Post';
if (post.root_id.length > 0) { if (post.root_id.length > 0) {
@@ -91,6 +115,44 @@ export default class RhsRootPost extends React.Component {
var dropdownContents = []; 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( dropdownContents.push(
<li <li
key='rhs-root-permalink' 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 ( return (
<div className={'post post--root post--thread ' + userCss + ' ' + systemMessageClass + ' ' + compactClass}> <div className={'post post--root post--thread ' + userCss + ' ' + systemMessageClass + ' ' + compactClass}>
<div className='post-right-channel__name'>{channelName}</div> <div className='post-right-channel__name'>{channelName}</div>
@@ -267,11 +367,23 @@ export default class RhsRootPost extends React.Component {
minute='2-digit' minute='2-digit'
/> />
</time> </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>
<li className='col col__reply'> <li className='col col__reply'>
<div> {rootOptions}
{rootOptions}
</div>
</li> </li>
</ul> </ul>
<div className='post__body'> <div className='post__body'>
@@ -297,5 +409,6 @@ RhsRootPost.propTypes = {
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
commentCount: React.PropTypes.number, commentCount: React.PropTypes.number,
compactDisplay: React.PropTypes.bool, 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.windowHeight = Utils.windowHeight();
state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles())); 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.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; this.state = state;
} }
@@ -121,6 +122,10 @@ export default class RhsThread extends React.Component {
return true; return true;
} }
if (!Utils.areObjectsEqual(nextState.flaggedPosts, this.state.flaggedPosts)) {
return true;
}
if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) { if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) {
return true; return true;
} }
@@ -151,7 +156,8 @@ export default class RhsThread extends React.Component {
onPreferenceChange() { onPreferenceChange() {
this.setState({ 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(); this.forceUpdateInfo();
} }
@@ -240,12 +246,18 @@ export default class RhsThread extends React.Component {
profile = profiles[selected.user_id]; profile = profiles[selected.user_id];
} }
let isRootFlagged = false;
if (this.state.flaggedPosts) {
isRootFlagged = this.state.flaggedPosts.get(selected.id) === 'true';
}
return ( return (
<div className='post-right__container'> <div className='post-right__container'>
<FileUploadOverlay overlayType='right'/> <FileUploadOverlay overlayType='right'/>
<div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div>
<div className='sidebar-right__body'> <div className='sidebar-right__body'>
<RhsHeaderPost <RhsHeaderPost
fromFlaggedPosts={this.props.fromFlaggedPosts}
fromSearch={this.props.fromSearch} fromSearch={this.props.fromSearch}
isMentionSearch={this.props.isMentionSearch} isMentionSearch={this.props.isMentionSearch}
toggleSize={this.props.toggleSize} toggleSize={this.props.toggleSize}
@@ -268,6 +280,7 @@ export default class RhsThread extends React.Component {
currentUser={this.props.currentUser} currentUser={this.props.currentUser}
compactDisplay={this.state.compactDisplay} compactDisplay={this.state.compactDisplay}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
isFlagged={isRootFlagged}
/> />
<div className='post-right-comments-container'> <div className='post-right-comments-container'>
{postsArray.map((comPost) => { {postsArray.map((comPost) => {
@@ -277,6 +290,11 @@ export default class RhsThread extends React.Component {
} else { } else {
p = profiles[comPost.user_id]; p = profiles[comPost.user_id];
} }
let isFlagged = false;
if (this.state.flaggedPosts) {
isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true';
}
return ( return (
<Comment <Comment
ref={comPost.id} ref={comPost.id}
@@ -286,6 +304,7 @@ export default class RhsThread extends React.Component {
currentUser={this.props.currentUser} currentUser={this.props.currentUser}
compactDisplay={this.state.compactDisplay} compactDisplay={this.state.compactDisplay}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
isFlagged={isFlagged}
/> />
); );
})} })}
@@ -311,6 +330,7 @@ RhsThread.defaultProps = {
RhsThread.propTypes = { RhsThread.propTypes = {
fromSearch: React.PropTypes.string, fromSearch: React.PropTypes.string,
fromFlaggedPosts: React.PropTypes.bool,
isMentionSearch: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired,
useMilitaryTime: React.PropTypes.bool.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 UserStore from 'stores/user_store.jsx';
import SearchBox from './search_bar.jsx'; import SearchBox from './search_bar.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import SearchResultsHeader from './search_results_header.jsx'; import SearchResultsHeader from './search_results_header.jsx';
import SearchResultsItem from './search_results_item.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); var noResults = (!results || !results.order || !results.order.length);
const searchTerm = this.state.searchTerm; const searchTerm = this.state.searchTerm;
const profiles = this.state.profiles || {}; const profiles = this.state.profiles || {};
const flagIcon = Constants.FLAG_ICON_SVG;
var ctls = null; 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 = ( ctls = (
<div className='sidebar--right__subheader'> <div className='sidebar--right__subheader'>
<FormattedHTMLMessage <FormattedHTMLMessage
@@ -172,6 +207,7 @@ export default class SearchResults extends React.Component {
isMentionSearch={this.props.isMentionSearch} isMentionSearch={this.props.isMentionSearch}
useMilitaryTime={this.props.useMilitaryTime} useMilitaryTime={this.props.useMilitaryTime}
shrink={this.props.shrink} shrink={this.props.shrink}
isFlagged={this.props.isFlaggedPosts}
/> />
); );
}, this); }, this);
@@ -185,6 +221,7 @@ export default class SearchResults extends React.Component {
isMentionSearch={this.props.isMentionSearch} isMentionSearch={this.props.isMentionSearch}
toggleSize={this.props.toggleSize} toggleSize={this.props.toggleSize}
shrink={this.props.shrink} shrink={this.props.shrink}
isFlaggedPosts={this.props.isFlaggedPosts}
/> />
<div <div
id='search-items-container' id='search-items-container'
@@ -202,5 +239,6 @@ SearchResults.propTypes = {
isMentionSearch: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool,
useMilitaryTime: React.PropTypes.bool.isRequired, useMilitaryTime: React.PropTypes.bool.isRequired,
toggleSize: React.PropTypes.function, 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' defaultMessage='Recent Mentions'
/> />
); );
} else if (this.props.isFlaggedPosts) {
title = (
<FormattedMessage
id='search_header.title3'
defaultMessage='Flagged Posts'
/>
);
} }
return ( return (
@@ -140,5 +147,6 @@ export default class SearchResultsHeader extends React.Component {
SearchResultsHeader.propTypes = { SearchResultsHeader.propTypes = {
isMentionSearch: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool,
toggleSize: React.PropTypes.function, 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 TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_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 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 TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;
import {FormattedMessage, FormattedDate} from 'react-intl';
import React from 'react'; import React from 'react';
import {FormattedMessage, FormattedDate} from 'react-intl';
import {browserHistory} from 'react-router/es6'; import {browserHistory} from 'react-router/es6';
export default class SearchResultsItem extends React.Component { 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.handleFocusRHSClick = this.handleFocusRHSClick.bind(this);
this.shrinkSidebar = this.shrinkSidebar.bind(this); this.shrinkSidebar = this.shrinkSidebar.bind(this);
this.unflagPost = this.unflagPost.bind(this);
} }
hideSidebar() { hideSidebar() {
@@ -42,12 +47,20 @@ export default class SearchResultsItem extends React.Component {
GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
} }
unflagPost(e) {
e.preventDefault();
unflagPost(this.props.post.id,
() => getFlaggedPosts()
);
}
render() { render() {
let channelName = null; let channelName = null;
const channel = this.props.channel; const channel = this.props.channel;
const timestamp = UserStore.getCurrentUser().update_at; const timestamp = UserStore.getCurrentUser().update_at;
const user = this.props.user || {}; const user = this.props.user || {};
const post = this.props.post; const post = this.props.post;
const flagIcon = Constants.FLAG_ICON_SVG;
if (channel) { if (channel) {
channelName = channel.display_name; channelName = channel.display_name;
@@ -77,11 +90,50 @@ export default class SearchResultsItem extends React.Component {
} }
let botIndicator; let botIndicator;
if (post.props && post.props.from_webhook) { if (post.props && post.props.from_webhook) {
botIndicator = <li className='bot-indicator'>{Constants.BOT_NAME}</li>; 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 ( return (
<div className='search-item__container'> <div className='search-item__container'>
<div className='date-separator'> <div className='date-separator'>
@@ -126,8 +178,19 @@ export default class SearchResultsItem extends React.Component {
minute='2-digit' minute='2-digit'
/> />
</time> </time>
{flag}
</li> </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 <a
onClick={ onClick={
() => { () => {
@@ -163,18 +226,6 @@ export default class SearchResultsItem extends React.Component {
/> />
</a> </a>
</li> </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> </ul>
<div className='search-item-snippet'> <div className='search-item-snippet'>
<span <span
@@ -197,5 +248,6 @@ SearchResultsItem.propTypes = {
isMentionSearch: React.PropTypes.bool, isMentionSearch: React.PropTypes.bool,
term: React.PropTypes.string, term: React.PropTypes.string,
useMilitaryTime: React.PropTypes.bool.isRequired, 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 SearchResults from './search_results.jsx';
import RhsThread from './rhs_thread.jsx'; import RhsThread from './rhs_thread.jsx';
import SearchStore from 'stores/search_store.jsx'; import SearchStore from 'stores/search_store.jsx';
import PostStore from 'stores/post_store.jsx'; import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_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 * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react'; import React from 'react';
@@ -101,15 +105,20 @@ export default class SidebarRight extends React.Component {
} }
onPreferenceChange() { onPreferenceChange() {
if (this.state.isFlaggedPosts) {
getFlaggedPosts();
}
this.setState({ this.setState({
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Constants.Preferences.USE_MILITARY_TIME, false) useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Constants.Preferences.USE_MILITARY_TIME, false)
}); });
} }
onSelectedChange(fromSearch) { onSelectedChange(fromSearch, fromFlaggedPosts) {
this.setState({ this.setState({
postRightVisible: !!PostStore.getSelectedPost(), postRightVisible: !!PostStore.getSelectedPost(),
fromSearch fromSearch,
fromFlaggedPosts
}); });
} }
@@ -120,7 +129,8 @@ export default class SidebarRight extends React.Component {
onSearchChange() { onSearchChange() {
this.setState({ this.setState({
searchVisible: SearchStore.getSearchResults() !== null, 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 = ( content = (
<SearchResults <SearchResults
isMentionSearch={this.state.isMentionSearch} isMentionSearch={this.state.isMentionSearch}
isFlaggedPosts={this.state.isFlaggedPosts}
useMilitaryTime={this.state.useMilitaryTime} useMilitaryTime={this.state.useMilitaryTime}
toggleSize={this.toggleSize} toggleSize={this.toggleSize}
shrink={this.onShrink} shrink={this.onShrink}
@@ -162,6 +173,7 @@ export default class SidebarRight extends React.Component {
} else if (this.state.postRightVisible) { } else if (this.state.postRightVisible) {
content = ( content = (
<RhsThread <RhsThread
fromFlaggedPosts={this.state.fromFlaggedPosts}
fromSearch={this.state.fromSearch} fromSearch={this.state.fromSearch}
isMentionSearch={this.state.isMentionSearch} isMentionSearch={this.state.isMentionSearch}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}

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

@@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.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 UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.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.handleAboutModal = this.handleAboutModal.bind(this);
this.searchMentions = this.searchMentions.bind(this); this.searchMentions = this.searchMentions.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
this.getFlagged = this.getFlagged.bind(this);
const state = this.getStateFromStores(); const state = this.getStateFromStores();
state.showUserSettingsModal = false; state.showUserSettingsModal = false;
@@ -53,6 +55,11 @@ export default class SidebarRightMenu extends React.Component {
this.setState({showAboutModal: false}); this.setState({showAboutModal: false});
} }
getFlagged(e) {
e.preventDefault();
getFlaggedPosts();
}
componentDidMount() { componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange); PreferenceStore.addChangeListener(this.onPreferenceChange);
} }
@@ -344,6 +351,18 @@ export default class SidebarRightMenu extends React.Component {
/> />
</a> </a>
</li> </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> <li>
<a <a
href='#' href='#'

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

@@ -1018,6 +1018,8 @@
"create_comment.files": "Files uploading", "create_comment.files": "Files uploading",
"create_post.comment": "Comment", "create_post.comment": "Comment",
"create_post.post": "Post", "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.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_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}.", "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_delete.someone": "Someone deleted the message on which you tried to post a comment.",
"post_focus_view.beginning": "Beginning of Channel Archives", "post_focus_view.beginning": "Beginning of Channel Archives",
"post_info.del": "Delete", "post_info.del": "Delete",
"post_info.mobile.flag": "Flag",
"post_info.mobile.unflag": "Unflag",
"post_info.edit": "Edit", "post_info.edit": "Edit",
"post_info.permalink": "Permalink", "post_info.permalink": "Permalink",
"post_info.reply": "Reply", "post_info.reply": "Reply",
@@ -1393,14 +1397,19 @@
"rename_channel.title": "Rename Channel", "rename_channel.title": "Rename Channel",
"rhs_comment.comment": "Comment", "rhs_comment.comment": "Comment",
"rhs_comment.del": "Delete", "rhs_comment.del": "Delete",
"rhs_comment.mobile.flag": "Flag",
"rhs_comment.mobile.unflag": "Unflag",
"rhs_comment.edit": "Edit", "rhs_comment.edit": "Edit",
"rhs_comment.permalink": "Permalink", "rhs_comment.permalink": "Permalink",
"rhs_header.backToResultsTooltip": "Back to Search Results", "rhs_header.backToResultsTooltip": "Back to Search Results",
"rhs_header.backToFlaggedTooltip": "Back to Flagged Posts",
"rhs_header.closeSidebarTooltip": "Close Sidebar", "rhs_header.closeSidebarTooltip": "Close Sidebar",
"rhs_header.details": "Message Details", "rhs_header.details": "Message Details",
"rhs_header.expandSidebarTooltip": "Expand Sidebar", "rhs_header.expandSidebarTooltip": "Expand Sidebar",
"rhs_header.shrinkSidebarTooltip": "Shrink Sidebar", "rhs_header.shrinkSidebarTooltip": "Shrink Sidebar",
"rhs_root.del": "Delete", "rhs_root.del": "Delete",
"rhs_root.mobile.flag": "Flag",
"rhs_root.mobile.unflag": "Unflag",
"rhs_root.direct": "Direct Message", "rhs_root.direct": "Direct Message",
"rhs_root.edit": "Edit", "rhs_root.edit": "Edit",
"rhs_root.permalink": "Permalink", "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_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.results": "Search Results",
"search_header.title2": "Recent Mentions", "search_header.title2": "Recent Mentions",
"search_header.title3": "Flagged Posts",
"search_item.direct": "Direct Message", "search_item.direct": "Direct Message",
"search_item.jump": "Jump", "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>", "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.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_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.accountSettings": "Account Settings",
"sidebar_right_menu.flagged": "Flagged Posts",
"sidebar_right_menu.console": "System Console", "sidebar_right_menu.console": "System Console",
"sidebar_right_menu.help": "Help", "sidebar_right_menu.help": "Help",
"sidebar_right_menu.inviteNew": "Invite New Member", "sidebar_right_menu.inviteNew": "Invite New Member",

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

@@ -137,23 +137,31 @@
} }
} }
.search-item__jump { .col__controls {
@include opacity(.8);
font-size: 13px; font-size: 13px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
&:hover { a {
@include opacity(1); @include opacity(.8);
} vertical-align: top;
}
.search-item__comment { &:hover {
margin-right: 35px; @include opacity(1);
position: absolute; }
right: 0; }
top: 0;
.search-item__jump {
font-size: 13px;
position: relative;
top: 1px;
}
.search-item__comment {
margin-right: 5px;
}
} }
.search-item-time { .search-item-time {

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

@@ -43,6 +43,7 @@
} }
&:last-child { &:last-child {
padding-right: 8px;
width: 8.9%; width: 8.9%;
} }
} }
@@ -375,8 +376,17 @@
font-size: 22px; font-size: 22px;
height: 30px; height: 30px;
line-height: 26px; line-height: 26px;
margin-right: 9px; margin-right: 3px;
width: 24px; text-align: center;
width: 30px;
th {
&:last-child {
div {
margin-right: 10px;
}
}
}
.channel__wrap.move--left & { .channel__wrap.move--left & {
position: absolute; position: absolute;
@@ -384,6 +394,15 @@
top: 14px; top: 14px;
} }
.icon__flag {
svg {
height: 18px;
position: relative;
top: 2px;
width: 19px;
}
}
> a { > a {
@include opacity(.6); @include opacity(.6);
@include single-transition(all, .1s, ease-in); @include single-transition(all, .1s, ease-in);

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

@@ -486,6 +486,7 @@ body.ios {
&:hover { &:hover {
.dropdown, .dropdown,
.comment-icon__container, .comment-icon__container,
.flag-icon__container,
.post__reply, .post__reply,
.post__remove { .post__remove {
visibility: visible; visibility: visible;
@@ -561,7 +562,7 @@ body.ios {
.img-div { .img-div {
max-height: 150px; max-height: 150px;
max-width: 150px; max-width: 150px;
} }
p { p {
line-height: inherit; line-height: inherit;
@@ -573,7 +574,7 @@ body.ios {
ol, ol,
ul { ul {
clear: both; clear: both;
padding-left: 20px; padding-left: 20px;
} }
} }
@@ -694,6 +695,13 @@ body.ios {
} }
} }
.flag-icon__container {
left: 36px;
margin-left: 5px;
position: absolute;
top: 8px;
}
.post__img { .post__img {
img { img {
display: none; display: none;
@@ -835,7 +843,9 @@ body.ios {
} }
.post__img { .post__img {
width: 42px; padding-right: 10px;
text-align: right;
width: 53px;
svg { svg {
height: 32px; height: 32px;
@@ -1076,6 +1086,7 @@ body.ios {
display: inline-block; display: inline-block;
margin-right: 6px; margin-right: 6px;
visibility: hidden; visibility: hidden;
svg { svg {
fill: inherit; fill: inherit;
position: relative; 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 { .web-embed-data {
@include border-radius(2px); @include border-radius(2px);
@include alpha-property(background, $black, 0.05); @include alpha-property(background, $black, 0.05);

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

@@ -161,7 +161,7 @@
.sidebar--right__subheader { .sidebar--right__subheader {
font-size: 1em; font-size: 1em;
padding: 1em 1em 0; padding: 0.5em 1em 0;
h4 { h4 {
font-size: 1em; font-size: 1em;
@@ -176,6 +176,13 @@
font-size: .95em; font-size: .95em;
padding-bottom: 10px; padding-bottom: 10px;
} }
.usage__icon {
@include opacity(.6);
margin: 0 3px;
position: relative;
top: 1px;
}
} }
.suggestion-list__content { .suggestion-list__content {

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

@@ -94,6 +94,16 @@
} }
} }
&.same--root {
&.same--user {
.flag-icon__container {
left: auto;
position: relative;
top: 1px;
}
}
}
.post__content { .post__content {
padding: 0 10px 0 0; padding: 0 10px 0 0;
} }
@@ -182,7 +192,23 @@
} }
} }
.star-icon__container {
left: auto;
position: relative;
top: auto;
&:not(.visible) {
display: none;
}
}
&.same--root { &.same--root {
.star-icon__container {
left: auto;
position: relative;
top: auto;
}
&.same--user { &.same--user {
.post__header { .post__header {
height: auto; height: auto;

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

@@ -116,7 +116,15 @@
.channel__wrap & { .channel__wrap & {
.post__time { .post__time {
font-size: .85em; font-size: .85em;
left: -70px; left: -79px;
position: absolute;
text-align: right;
top: 6px;
width: 60px;
}
.star-icon__container {
left: -65px;
position: absolute; position: absolute;
text-align: right; text-align: right;
top: 6px; top: 6px;
@@ -125,7 +133,7 @@
} }
&:not(.post--thread) { &:not(.post--thread) {
padding: 5px .5em 0 70px; padding: 5px .5em 0 72px;
.post__link { .post__link {
margin: 4px 0 7px; 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--root {
&.same--user { &.same--user {
padding-left: 70px; padding-left: 72px;
padding-top: 0; padding-top: 0;
.flag-icon__container {
left: -19px;
position: absolute;
top: 7px;
}
.post__header { .post__header {
.col__reply { .col__reply {
top: -1px; top: -1px;
@@ -265,12 +291,16 @@
&:not(.post--compact) { &:not(.post--compact) {
.post__time { .post__time {
display: inline-block;
font-size: 11px; font-size: 11px;
left: -4px; left: -14px;
line-height: 37px; line-height: 34px;
position: absolute; position: absolute;
text-align: right;
text-rendering: auto; text-rendering: auto;
top: -2px; top: -2px;
width: 51px;
} }
} }
} }

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

@@ -441,8 +441,8 @@ class PostStoreClass extends EventEmitter {
return threadPosts; return threadPosts;
} }
emitSelectedPostChange(fromSearch) { emitSelectedPostChange(fromSearch, fromFlaggedPosts) {
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch); this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch, fromFlaggedPosts);
} }
addSelectedPostChangeListener(callback) { addSelectedPostChangeListener(callback) {
@@ -599,7 +599,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
break; break;
case ActionTypes.RECEIVED_POST_SELECTED: case ActionTypes.RECEIVED_POST_SELECTED:
PostStore.storeSelectedPostId(action.postId); PostStore.storeSelectedPostId(action.postId);
PostStore.emitSelectedPostChange(action.from_search); PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts);
break; break;
default: default:
} }

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

@@ -18,6 +18,7 @@ class SearchStoreClass extends EventEmitter {
this.searchResults = null; this.searchResults = null;
this.isMentionSearch = false; this.isMentionSearch = false;
this.isFlaggedPosts = false;
this.searchTerm = ''; this.searchTerm = '';
} }
@@ -77,6 +78,10 @@ class SearchStoreClass extends EventEmitter {
return this.isMentionSearch; return this.isMentionSearch;
} }
getIsFlaggedPosts() {
return this.isFlaggedPosts;
}
storeSearchTerm(term) { storeSearchTerm(term) {
this.searchTerm = term; this.searchTerm = term;
} }
@@ -85,9 +90,10 @@ class SearchStoreClass extends EventEmitter {
return this.searchTerm; return this.searchTerm;
} }
storeSearchResults(results, isMentionSearch) { storeSearchResults(results, isMentionSearch, isFlaggedPosts) {
this.searchResults = results; this.searchResults = results;
this.isMentionSearch = isMentionSearch; this.isMentionSearch = isMentionSearch;
this.isFlaggedPosts = isFlaggedPosts;
} }
} }
@@ -98,7 +104,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) { switch (action.type) {
case ActionTypes.RECEIVED_SEARCH: 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(); SearchStore.emitSearchChange();
break; break;
case ActionTypes.RECEIVED_SEARCH_TERM: 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: 'collapse_previews',
COLLAPSE_DISPLAY_DEFAULT: 'false', COLLAPSE_DISPLAY_DEFAULT: 'false',
USE_MILITARY_TIME: 'use_military_time', USE_MILITARY_TIME: 'use_military_time',
CATEGORY_THEME: 'theme' CATEGORY_THEME: 'theme',
CATEGORY_FLAGGED_POST: 'flagged_post'
}; };
export const ActionTypes = keyMirror({ export const ActionTypes = keyMirror({
@@ -326,6 +327,8 @@ export const Constants = {
OPEN_TEAM: 'O', OPEN_TEAM: 'O',
MAX_POST_LEN: 4000, MAX_POST_LEN: 4000,
EMOJI_SIZE: 16, 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>", 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>", 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>", 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) { 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('@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 .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); 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) { 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 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 .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) { if (theme.buttonBg) {