summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorGabin Aureche <gabin.aureche@live.fr>2017-03-13 13:25:08 +0100
committerGeorge Goldberg <george@gberg.me>2017-03-13 12:25:08 +0000
commitfe38d6d5bb36e18ddefbe490cc21f48f4f4c8d81 (patch)
treeb96d457cde64b7397f91028106e93a7f92a179bd /webapp
parent482a0fb5fc248b1ec61db35299dc3e6d963ad5ab (diff)
downloadchat-fe38d6d5bb36e18ddefbe490cc21f48f4f4c8d81.tar.gz
chat-fe38d6d5bb36e18ddefbe490cc21f48f4f4c8d81.tar.bz2
chat-fe38d6d5bb36e18ddefbe490cc21f48f4f4c8d81.zip
Add pinned posts (#4217)
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/post_actions.jsx36
-rw-r--r--webapp/client/client.jsx27
-rw-r--r--webapp/components/channel_header.jsx27
-rw-r--r--webapp/components/navbar.jsx30
-rw-r--r--webapp/components/needs_team.jsx16
-rw-r--r--webapp/components/popover_list_members.jsx14
-rw-r--r--webapp/components/post_view/components/post_info.jsx61
-rw-r--r--webapp/components/rhs_comment.jsx63
-rw-r--r--webapp/components/rhs_root_post.jsx63
-rw-r--r--webapp/components/search_results.jsx31
-rw-r--r--webapp/components/search_results_header.jsx14
-rw-r--r--webapp/components/search_results_item.jsx13
-rw-r--r--webapp/components/sidebar_right.jsx23
-rw-r--r--webapp/i18n/en.json10
-rw-r--r--webapp/sass/layout/_content.scss28
-rw-r--r--webapp/sass/layout/_headers.scss27
-rw-r--r--webapp/sass/layout/_post.scss12
-rw-r--r--webapp/sass/responsive/_desktop.scss17
-rw-r--r--webapp/sass/responsive/_mobile.scss2
-rw-r--r--webapp/sass/responsive/_tablet.scss35
-rw-r--r--webapp/stores/post_store.jsx57
-rw-r--r--webapp/stores/search_store.jsx10
-rw-r--r--webapp/utils/async_client.jsx34
-rw-r--r--webapp/utils/constants.jsx3
-rw-r--r--webapp/utils/utils.jsx5
25 files changed, 614 insertions, 44 deletions
diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx
index cbcddfc7c..0c837621f 100644
--- a/webapp/actions/post_actions.jsx
+++ b/webapp/actions/post_actions.jsx
@@ -68,6 +68,14 @@ export function handleNewPost(post, msg) {
});
}
+export function pinPost(channelId, postId) {
+ AsyncClient.pinPost(channelId, postId);
+}
+
+export function unpinPost(channelId, postId) {
+ AsyncClient.unpinPost(channelId, postId);
+}
+
export function flagPost(postId) {
trackEvent('api', 'api_posts_flagged');
AsyncClient.savePreference(Preferences.CATEGORY_FLAGGED_POST, postId, 'true');
@@ -96,7 +104,8 @@ export function getFlaggedPosts() {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
results: data,
- is_flagged_posts: true
+ is_flagged_posts: true,
+ is_pinned_posts: false
});
loadProfilesForPosts(data.posts);
@@ -107,6 +116,31 @@ export function getFlaggedPosts() {
);
}
+export function getPinnedPosts(channelId) {
+ Client.getPinnedPosts(channelId,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH_TERM,
+ term: null,
+ do_search: false,
+ is_mention_search: false
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH,
+ results: data,
+ is_flagged_posts: false,
+ is_pinned_posts: true
+ });
+
+ loadProfilesForPosts(data.posts);
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getPinnedPosts');
+ }
+ );
+}
+
export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) {
const postList = PostStore.getAllPosts(channelId);
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index eaffd9ff4..a95049f93 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1802,6 +1802,15 @@ export default class Client {
this.trackEvent('api', 'api_posts_get_flagged', {team_id: this.getTeamId()});
}
+ getPinnedPosts(channelId, success, error) {
+ request.
+ get(`${this.getChannelNeededRoute(channelId)}/pinned`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getPinnedPosts', success, error));
+ }
+
getFileInfosForPost(channelId, postId, success, error) {
request.
get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/get_file_infos`).
@@ -2187,6 +2196,24 @@ export default class Client {
});
}
+ pinPost(channelId, postId, success, error) {
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/pin`).
+ set(this.defaultHeaders).
+ accept('application/json').
+ send().
+ end(this.handleResponse.bind(this, 'pinPost', success, error));
+ }
+
+ unpinPost(channelId, postId, success, error) {
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/unpin`).
+ set(this.defaultHeaders).
+ accept('application/json').
+ send().
+ end(this.handleResponse.bind(this, 'unpinPost', success, error));
+ }
+
saveReaction(channelId, reaction, success, error) {
request.
post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`).
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 01e1e98cf..556e49863 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -30,7 +30,7 @@ import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import {getSiteURL} from 'utils/url.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
-import {getFlaggedPosts} from 'actions/post_actions.jsx';
+import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
@@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component {
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
this.handleShortcut = this.handleShortcut.bind(this);
this.getFlagged = this.getFlagged.bind(this);
+ this.getPinnedPosts = this.getPinnedPosts.bind(this);
this.initWebrtc = this.initWebrtc.bind(this);
this.onBusy = this.onBusy.bind(this);
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
@@ -158,6 +159,15 @@ export default class ChannelHeader extends React.Component {
}
}
+ getPinnedPosts(e) {
+ e.preventDefault();
+ if (SearchStore.isPinnedPosts) {
+ GlobalActions.toggleSideBarAction(false);
+ } else {
+ getPinnedPosts(this.props.channelId);
+ }
+ }
+
getFlagged(e) {
e.preventDefault();
if (SearchStore.isFlaggedPosts) {
@@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component {
render() {
const flagIcon = Constants.FLAG_ICON_SVG;
+ const pinIcon = Constants.PIN_ICON;
if (!this.validState()) {
// Use an empty div to make sure the header's height stays constant
@@ -762,8 +773,20 @@ export default class ChannelHeader extends React.Component {
</OverlayTrigger>
</div>
</th>
- <th className='header-list__members'>
+ <th className='header-list__right'>
{popoverListMembers}
+ <a
+ href='#'
+ type='button'
+ id='pinned-posts-button'
+ className='pinned-posts-button'
+ onClick={this.getPinnedPosts}
+ >
+ <span
+ dangerouslySetInnerHTML={{__html: pinIcon}}
+ aria-hidden='true'
+ />
+ </a>
</th>
<th className='search-bar__container'>
<NavbarSearchBox
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index c945a0b9c..1ad2f916d 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import SearchStore from 'stores/search_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
+import {getPinnedPosts} from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -62,6 +65,7 @@ export default class Navbar extends React.Component {
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
+ this.getPinnedPosts = this.getPinnedPosts.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
@@ -216,6 +220,15 @@ export default class Navbar extends React.Component {
});
}
+ getPinnedPosts(e) {
+ e.preventDefault();
+ if (SearchStore.isPinnedPosts) {
+ GlobalActions.toggleSideBarAction(false);
+ } else {
+ getPinnedPosts(this.state.channel.id);
+ }
+ }
+
toggleFavorite = (e) => {
e.preventDefault();
@@ -244,6 +257,7 @@ export default class Navbar extends React.Component {
}
let viewInfoOption;
+ let viewPinnedPostsOption;
let addMembersOption;
let manageMembersOption;
let setChannelHeaderOption;
@@ -335,6 +349,21 @@ export default class Navbar extends React.Component {
</li>
);
+ viewPinnedPostsOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.getPinnedPosts}
+ >
+ <FormattedMessage
+ id='navbar.viewPinnedPosts'
+ defaultMessage='View Pinned Posts'
+ />
+ </a>
+ </li>
+ );
+
if (!ChannelStore.isDefault(channel)) {
addMembersOption = (
<li role='presentation'>
@@ -561,6 +590,7 @@ export default class Navbar extends React.Component {
role='menu'
>
{viewInfoOption}
+ {viewPinnedPostsOption}
{notificationPreferenceOption}
{addMembersOption}
{manageMembersOption}
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index fb6029c2b..f589136b5 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import PostStore from 'stores/post_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -173,12 +174,25 @@ export default class NeedsTeam extends React.Component {
</div>
);
}
+
+ let channel = ChannelStore.getByName(this.props.params.channel);
+ if (channel == null) {
+ // the permalink view is not really tied to a particular channel but still needs it
+ const postId = PostStore.getFocusedPostId();
+ const post = PostStore.getEarliestPostFromPage(postId);
+
+ // the post take some time before being available on page load
+ if (post != null) {
+ channel = ChannelStore.get(post.channel_id);
+ }
+ }
+
return (
<div className='channel-view'>
<ErrorBar/>
<WebrtcNotification/>
<div className='container-fluid'>
- <SidebarRight/>
+ <SidebarRight channel={channel}/>
<SidebarRightMenu teamType={this.state.team.type}/>
<WebrtcSidebar/>
{content}
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index bd2f744c7..1518b1ebf 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component {
}
return (
- <div>
+ <div className='member-popover__container'>
<div
id='member_popover'
className='member-popover__trigger'
@@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component {
AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
}}
>
- <div>
- {countText}
- <span
- className='fa fa-user'
- aria-hidden='true'
- />
- </div>
+ {countText}
+ <span
+ className='fa fa-user'
+ aria-hidden='true'
+ />
</div>
<Overlay
rootClose={true}
diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx
index 331fdeb00..5318ec272 100644
--- a/webapp/components/post_view/components/post_info.jsx
+++ b/webapp/components/post_view/components/post_info.jsx
@@ -26,6 +26,8 @@ export default class PostInfo extends React.Component {
this.removePost = this.removePost.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.pinPost = this.pinPost.bind(this);
+ this.unpinPost = this.unpinPost.bind(this);
this.canEdit = false;
this.canDelete = false;
@@ -148,6 +150,42 @@ export default class PostInfo extends React.Component {
);
}
+ if (this.props.post.is_pinned) {
+ dropdownContents.push(
+ <li
+ key='unpinLink'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.unpinPost}
+ >
+ <FormattedMessage
+ id='post_info.unpin'
+ defaultMessage='Un-pin from channel'
+ />
+ </a>
+ </li>
+ );
+ } else {
+ dropdownContents.push(
+ <li
+ key='pinLink'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.pinPost}
+ >
+ <FormattedMessage
+ id='post_info.pin'
+ defaultMessage='Pin to channel'
+ />
+ </a>
+ </li>
+ );
+ }
+
if (this.canDelete) {
dropdownContents.push(
<li
@@ -250,6 +288,16 @@ export default class PostInfo extends React.Component {
);
}
+ pinPost(e) {
+ e.preventDefault();
+ PostActions.pinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
+ unpinPost(e) {
+ e.preventDefault();
+ PostActions.unpinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
flagPost(e) {
e.preventDefault();
PostActions.flagPost(this.props.post.id);
@@ -374,6 +422,18 @@ export default class PostInfo extends React.Component {
);
}
+ let pinnedBadge;
+ if (post.is_pinned) {
+ pinnedBadge = (
+ <span className='post__pinned-badge'>
+ <FormattedMessage
+ id='post_info.pinned'
+ defaultMessage='Pinned'
+ />
+ </span>
+ );
+ }
+
return (
<ul className='post__header--info'>
<li className='col'>
@@ -384,6 +444,7 @@ export default class PostInfo extends React.Component {
useMilitaryTime={this.props.useMilitaryTime}
postId={post.id}
/>
+ {pinnedBadge}
{flagTrigger}
</li>
{options}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index cb527d850..c9a582877 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list
import RhsDropdown from 'components/rhs_dropdown.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import {flagPost, unflagPost} from 'actions/post_actions.jsx';
+import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
@@ -36,6 +36,8 @@ export default class RhsComment extends React.Component {
this.removePost = this.removePost.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.pinPost = this.pinPost.bind(this);
+ this.unpinPost = this.unpinPost.bind(this);
this.canEdit = false;
this.canDelete = false;
@@ -128,6 +130,16 @@ export default class RhsComment extends React.Component {
unflagPost(this.props.post.id);
}
+ pinPost(e) {
+ e.preventDefault();
+ pinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
+ unpinPost(e) {
+ e.preventDefault();
+ unpinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
createDropdown() {
const post = this.props.post;
@@ -195,6 +207,42 @@ export default class RhsComment extends React.Component {
</li>
);
+ if (post.is_pinned) {
+ dropdownContents.push(
+ <li
+ key='rhs-comment-unpin'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.unpinPost}
+ >
+ <FormattedMessage
+ id='rhs_root.unpin'
+ defaultMessage='Un-pin from channel'
+ />
+ </a>
+ </li>
+ );
+ } else {
+ dropdownContents.push(
+ <li
+ key='rhs-comment-pin'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.pinPost}
+ >
+ <FormattedMessage
+ id='rhs_root.pin'
+ defaultMessage='Pin to channel'
+ />
+ </a>
+ </li>
+ );
+ }
+
if (this.canDelete) {
dropdownContents.push(
<li
@@ -503,6 +551,18 @@ export default class RhsComment extends React.Component {
);
}
+ let pinnedBadge;
+ if (post.is_pinned) {
+ pinnedBadge = (
+ <span className='post__pinned-badge'>
+ <FormattedMessage
+ id='post_info.pinned'
+ defaultMessage='Pinned'
+ />
+ </span>
+ );
+ }
+
const timeOptions = {
day: 'numeric',
month: 'short',
@@ -524,6 +584,7 @@ export default class RhsComment extends React.Component {
{botIndicator}
<li className='col'>
{this.renderTimeTag(post, timeOptions)}
+ {pinnedBadge}
{flagTrigger}
</li>
{options}
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 0c1037501..6a6b01a7f 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import {flagPost, unflagPost} from 'actions/post_actions.jsx';
+import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
@@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component {
this.handlePermalink = this.handlePermalink.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.pinPost = this.pinPost.bind(this);
+ this.unpinPost = this.unpinPost.bind(this);
this.canEdit = false;
this.canDelete = false;
@@ -143,6 +145,16 @@ export default class RhsRootPost extends React.Component {
);
}
+ pinPost(e) {
+ e.preventDefault();
+ pinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
+ unpinPost(e) {
+ e.preventDefault();
+ unpinPost(this.props.post.channel_id, this.props.post.id);
+ }
+
render() {
const post = this.props.post;
const user = this.props.user;
@@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component {
</li>
);
+ if (post.is_pinned) {
+ dropdownContents.push(
+ <li
+ key='rhs-root-unpin'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.unpinPost}
+ >
+ <FormattedMessage
+ id='rhs_root.unpin'
+ defaultMessage='Un-pin from channel'
+ />
+ </a>
+ </li>
+ );
+ } else {
+ dropdownContents.push(
+ <li
+ key='rhs-root-pin'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={this.pinPost}
+ >
+ <FormattedMessage
+ id='rhs_root.pin'
+ defaultMessage='Pin to channel'
+ />
+ </a>
+ </li>
+ );
+ }
+
if (this.canDelete) {
dropdownContents.push(
<li
@@ -450,6 +498,18 @@ export default class RhsRootPost extends React.Component {
flagFunc = this.flagPost;
}
+ let pinnedBadge;
+ if (post.is_pinned) {
+ pinnedBadge = (
+ <span className='post__pinned-badge'>
+ <FormattedMessage
+ id='post_info.pinned'
+ defaultMessage='Pinned'
+ />
+ </span>
+ );
+ }
+
const timeOptions = {
day: 'numeric',
month: 'short',
@@ -470,6 +530,7 @@ export default class RhsRootPost extends React.Component {
{botIndicator}
<li className='col'>
{this.renderTimeTag(post, timeOptions)}
+ {pinnedBadge}
<OverlayTrigger
key={'rootpostflagtooltipkey' + flagVisible}
delayShow={Constants.OVERLAY_TIME_DELAY}
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index 4c0105738..ceafd766c 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -213,6 +213,31 @@ export default class SearchResults extends React.Component {
</ul>
</div>
);
+ } else if (this.props.isPinnedPosts && noResults) {
+ ctls = (
+ <div className='sidebar--right__subheader'>
+ <ul>
+ <li>
+ <FormattedHTMLMessage
+ id='search_results.usagePin1'
+ defaultMessage='There are no pinned messages yet.'
+ />
+ </li>
+ <li>
+ <FormattedHTMLMessage
+ id='search_results.usagePin2'
+ defaultMessage={'You can pin a message by clicking the "Pin to channel" option from the message\'s menu.'}
+ />
+ </li>
+ <li>
+ <FormattedHTMLMessage
+ id='search_results.usagePin3'
+ defaultMessage='Pinned messages are accessible by all channel members and are a way to mark messages for future reference.'
+ />
+ </li>
+ </ul>
+ </div>
+ );
} else if (!searchTerm && noResults) {
ctls = (
<div className='sidebar--right__subheader'>
@@ -289,6 +314,8 @@ export default class SearchResults extends React.Component {
toggleSize={this.props.toggleSize}
shrink={this.props.shrink}
isFlaggedPosts={this.props.isFlaggedPosts}
+ isPinnedPosts={this.props.isPinnedPosts}
+ channelDisplayName={this.props.channelDisplayName}
/>
<div
id='search-items-container'
@@ -307,5 +334,7 @@ SearchResults.propTypes = {
useMilitaryTime: React.PropTypes.bool.isRequired,
toggleSize: React.PropTypes.func,
shrink: React.PropTypes.func,
- isFlaggedPosts: React.PropTypes.bool
+ isFlaggedPosts: React.PropTypes.bool,
+ isPinnedPosts: React.PropTypes.bool,
+ channelDisplayName: React.PropTypes.string.isRequired
};
diff --git a/webapp/components/search_results_header.jsx b/webapp/components/search_results_header.jsx
index 1f2818e98..288d883ee 100644
--- a/webapp/components/search_results_header.jsx
+++ b/webapp/components/search_results_header.jsx
@@ -79,6 +79,16 @@ export default class SearchResultsHeader extends React.Component {
defaultMessage='Flagged Posts'
/>
);
+ } else if (this.props.isPinnedPosts) {
+ title = (
+ <FormattedMessage
+ id='search_header.title4'
+ defaultMessage='Pinned posts in {channelDisplayName}'
+ values={{
+ channelDisplayName: this.props.channelDisplayName
+ }}
+ />
+ );
}
return (
@@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = {
isMentionSearch: React.PropTypes.bool,
toggleSize: React.PropTypes.func,
shrink: React.PropTypes.func,
- isFlaggedPosts: React.PropTypes.bool
+ isFlaggedPosts: React.PropTypes.bool,
+ isPinnedPosts: React.PropTypes.bool,
+ channelDisplayName: React.PropTypes.string.isRequired
};
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index b3de3492c..1c7309f51 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -289,6 +289,18 @@ export default class SearchResultsItem extends React.Component {
);
}
+ let pinnedBadge;
+ if (post.is_pinned) {
+ pinnedBadge = (
+ <span className='post__pinned-badge'>
+ <FormattedMessage
+ id='post_info.pinned'
+ defaultMessage='Pinned'
+ />
+ </span>
+ );
+ }
+
return (
<div className='search-item__container'>
<div className='date-separator'>
@@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component {
{botIndicator}
<li className='col'>
{this.renderTimeTag(post)}
+ {pinnedBadge}
{flagContent}
</li>
{rhsControls}
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx
index fb120337a..42b7381f4 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right.jsx
@@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import WebrtcStore from 'stores/webrtc_store.jsx';
-import {getFlaggedPosts} from 'actions/post_actions.jsx';
+import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import React from 'react';
+import React, {PropTypes} from 'react';
export default class SidebarRight extends React.Component {
constructor(props) {
@@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component {
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onSelectedChange = this.onSelectedChange.bind(this);
+ this.onPostPinnedChange = this.onPostPinnedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.onShowSearch = this.onShowSearch.bind(this);
@@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component {
searchVisible: SearchStore.getSearchResults() !== null,
isMentionSearch: SearchStore.getIsMentionSearch(),
isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
+ isPinnedPosts: SearchStore.getIsPinnedPosts(),
postRightVisible: Boolean(PostStore.getSelectedPost()),
expanded: false,
fromSearch: false,
@@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component {
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
+ PostStore.addPostPinnedChangeListener(this.onPostPinnedChange);
SearchStore.addShowSearchListener(this.onShowSearch);
UserStore.addChangeListener(this.onUserChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
@@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component {
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
+ PostStore.removePostPinnedChangeListener(this.onPostPinnedChange);
SearchStore.removeShowSearchListener(this.onShowSearch);
UserStore.removeChangeListener(this.onUserChange);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
@@ -137,6 +141,12 @@ export default class SidebarRight extends React.Component {
});
}
+ onPostPinnedChange() {
+ if (this.props.channel && this.state.isPinnedPosts) {
+ getPinnedPosts(this.props.channel.id);
+ }
+ }
+
onShrink() {
this.setState({
expanded: false
@@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component {
this.setState({
searchVisible: SearchStore.getSearchResults() !== null,
isMentionSearch: SearchStore.getIsMentionSearch(),
- isFlaggedPosts: SearchStore.getIsFlaggedPosts()
+ isFlaggedPosts: SearchStore.getIsFlaggedPosts(),
+ isPinnedPosts: SearchStore.getIsPinnedPosts()
});
}
@@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component {
<SearchResults
isMentionSearch={this.state.isMentionSearch}
isFlaggedPosts={this.state.isFlaggedPosts}
+ isPinnedPosts={this.state.isPinnedPosts}
useMilitaryTime={this.state.useMilitaryTime}
toggleSize={this.toggleSize}
shrink={this.onShrink}
+ channelDisplayName={this.props.channel ? this.props.channel.display_name : ''}
/>
);
} else if (this.state.postRightVisible) {
@@ -222,3 +235,7 @@ export default class SidebarRight extends React.Component {
);
}
}
+
+SidebarRight.propTypes = {
+ channel: PropTypes.object
+};
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index c6d540ae3..33b905606 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1670,6 +1670,7 @@
"navbar.toggle1": "Toggle sidebar",
"navbar.toggle2": "Toggle sidebar",
"navbar.viewInfo": "View Info",
+ "navbar.viewPinnedPosts": "View Pinned Posts",
"navbar_dropdown.about": "About Mattermost",
"navbar_dropdown.accountSettings": "Account Settings",
"navbar_dropdown.console": "System Console",
@@ -1726,6 +1727,9 @@
"post_info.permalink": "Permalink",
"post_info.reply": "Reply",
"post_info.system": "System",
+ "post_info.pin": "Pin to channel",
+ "post_info.unpin": "Un-pin from channel",
+ "post_info.pinned": "Pinned",
"post_message_view.edited": "(edited)",
"posts_view.loadMore": "Load more messages",
"posts_view.newMsg": "New Messages",
@@ -1778,11 +1782,14 @@
"rhs_root.mobile.flag": "Flag",
"rhs_root.mobile.unflag": "Unflag",
"rhs_root.permalink": "Permalink",
+ "rhs_root.pin": "Pin to channel",
+ "rhs_root.unpin": "Un-pin from channel",
"search_bar.search": "Search",
"search_bar.usage": "<h4>Search Options</h4><ul><li><span>Use </span><b>\"quotation marks\"</b><span> to search for phrases</span></li><li><span>Use </span><b>from:</b><span> to find posts from specific users and </span><b>in:</b><span> to find posts in specific channels</span></li></ul>",
"search_header.results": "Search Results",
"search_header.title2": "Recent Mentions",
"search_header.title3": "Flagged Posts",
+ "search_header.title4": "Pinned posts in {channelDisplayName}",
"search_item.direct": "Direct Message (with {username})",
"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>",
@@ -1792,6 +1799,9 @@
"search_results.usageFlag2": "You can add a flag to messages and comments by clicking the ",
"search_results.usageFlag3": " icon next to the timestamp.",
"search_results.usageFlag4": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.",
+ "search_results.usagePin1": "There are no pinned messages yet.",
+ "search_results.usagePin2": "You can pin a message by clicking the \"Pin to channel\" option from the message's menu.",
+ "search_results.usagePin3": "Pinned messages are accessible by all channel members and are a way to mark messages for future reference.",
"setting_item_max.cancel": "Cancel",
"setting_item_max.save": "Save",
"setting_item_min.edit": "Edit",
diff --git a/webapp/sass/layout/_content.scss b/webapp/sass/layout/_content.scss
index 02f063573..b6fe98eb4 100644
--- a/webapp/sass/layout/_content.scss
+++ b/webapp/sass/layout/_content.scss
@@ -9,10 +9,20 @@
.search-btns {
display: none;
}
- .header-list__members {
+ .header-list__right {
+ // the negative margin-right is used
+ // to prevent the icons in the header from
+ // moving to the left when the RHS is open
+ //
+ // the below z-index is used to ensure the icons
+ // stays on the top and don't get hidden by the
+ // search's input block
+ position: relative;
+ z-index: 6;
+
margin-right: -18px;
- float: right;
padding-right: 0px !important;
+ float: right;
}
}
@@ -23,10 +33,20 @@
.search-btns {
display: none;
}
- .header-list__members {
+ .header-list__right {
+ // the negative margin-right is used
+ // to prevent the icons in the header from
+ // moving to the left when the RHS is open
+ //
+ // the below z-index is used to ensure the icons
+ // stays on the top and don't get hidden by the
+ // search's input block
+ position: relative;
+ z-index: 6;
+
margin-right: -18px;
- float: right;
padding-right: 0px !important;
+ float: right
}
}
}
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 8ee6e8fdc..d38819d03 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -7,26 +7,43 @@
line-height: 56px;
width: 100%;
- .member-popover__trigger {
+ .member-popover__trigger,
+ .pinned-posts-button {
+ display: inline-block;
+ min-width: 30px;
cursor: pointer;
- min-width: 60px;
- padding-right: 10px;
- text-align: right;
+ margin-left: 10px;
+ text-align: center;
white-space: nowrap;
.fa {
font-size: 16px;
+ }
+ }
+
+ .member-popover__container,
+ .member-popover__trigger {
+ display: inline;
+ }
+
+ .member-popover__trigger {
+ .fa {
margin-left: 4px;
}
}
+ .pinned-posts-button svg {
+ position: relative;
+ top: 2px;
+ }
+
&.alt {
margin: 0;
th {
font-weight: normal !important;
- &.header-list__members {
+ &.header-list__right {
padding-right: 4px;
}
}
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index de45eedd5..debcd70e7 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -1343,15 +1343,23 @@
}
}
-.bot-indicator {
+.bot-indicator,
+.post__pinned-badge {
border-radius: 2px;
font-family: inherit;
font-size: 10px;
font-weight: 600;
- margin: 2px 10px 0 -4px;
padding: 1px 4px;
}
+.bot-indicator {
+ margin: 2px 10px 0 -4px;
+}
+
+.post__pinned-badge {
+ margin-left: 7px;
+}
+
.permalink-text {
overflow: hidden;
}
diff --git a/webapp/sass/responsive/_desktop.scss b/webapp/sass/responsive/_desktop.scss
index 891431f20..f671104e1 100644
--- a/webapp/sass/responsive/_desktop.scss
+++ b/webapp/sass/responsive/_desktop.scss
@@ -76,6 +76,23 @@
}
}
}
+
+ &.move--left {
+ .post {
+ &.post--root,
+ &.other--root {
+ .post__header {
+ padding-right: 70px;
+ }
+ }
+
+ &.post--comment {
+ .post__header {
+ padding-right: 70px;
+ }
+ }
+ }
+ }
}
}
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index 37c91846e..ec406c405 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -252,6 +252,7 @@
}
}
}
+
blockquote {
margin-top: 0;
}
@@ -273,6 +274,7 @@
.post__header {
margin-bottom: 0;
+ padding-right: 70px;
.col__reply {
top: -3px;
diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss
index 06a725a31..3bafc38d4 100644
--- a/webapp/sass/responsive/_tablet.scss
+++ b/webapp/sass/responsive/_tablet.scss
@@ -127,6 +127,15 @@
top: auto;
}
}
+
+ &.move--left,
+ &.webrtc--show,
+ &.move--right {
+ .header-list__right {
+ // hide it behind the RHS
+ z-index: -1;
+ }
+ }
}
.post {
.attachment {
@@ -182,6 +191,14 @@
}
}
}
+
+ .sidebar--right__title {
+ display: inline-block;
+ max-width: 300px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
.inner-wrap {
@@ -213,6 +230,11 @@
}
}
+ .post__pinned-badge {
+ margin-left: 0;
+ margin-right: 5px;
+ }
+
&:not(.post--thread) {
padding: 5px .5em 0 77px;
@@ -359,9 +381,16 @@
}
.post__header {
+ float: left;
+ padding-top: 3px;
+
.col__reply {
top: -21px;
}
+
+ .post__pinned-badge {
+ margin-right: 5px;
+ }
}
&:not(.post--compact) {
@@ -381,6 +410,12 @@
}
}
}
+
+ &.post--comment:not(.post--compact) {
+ .post__pinned-badge {
+ margin-left: 10px;
+ }
+ }
}
}
}
diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx
index 6e312f67a..6f81619c2 100644
--- a/webapp/stores/post_store.jsx
+++ b/webapp/stores/post_store.jsx
@@ -16,6 +16,7 @@ const FOCUSED_POST_CHANGE = 'focused_post_change';
const EDIT_POST_EVENT = 'edit_post';
const POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
const SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
+const POST_PINNED_CHANGE_EVENT = 'post_pinned_change';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -259,22 +260,42 @@ class PostStoreClass extends EventEmitter {
this.postsInfo[id].postList = combinedPosts;
}
+ focusedPostListHasPost(id) {
+ const focusedPostId = this.getFocusedPostId();
+ if (focusedPostId == null) {
+ return false;
+ }
+
+ const focusedPostList = makePostListNonNull(this.getAllPosts(focusedPostId));
+ return focusedPostList.posts.hasOwnProperty(id);
+ }
+
storePost(post, isNewPost = false) {
- const postList = makePostListNonNull(this.getAllPosts(post.channel_id));
+ const ids = [
+ post.channel_id
+ ];
- if (post.pending_post_id !== '') {
- this.removePendingPost(post.channel_id, post.pending_post_id);
+ // update the post in the permalink view if it's there
+ if (!isNewPost && this.focusedPostListHasPost(post.id)) {
+ ids.push(this.getFocusedPostId());
}
- post.pending_post_id = '';
+ ids.forEach((id) => {
+ const postList = makePostListNonNull(this.getAllPosts(id));
+ if (post.pending_post_id !== '') {
+ this.removePendingPost(post.channel_id, post.pending_post_id);
+ }
- postList.posts[post.id] = post;
- if (isNewPost && postList.order.indexOf(post.id) === -1) {
- postList.order.unshift(post.id);
- }
+ post.pending_post_id = '';
+
+ postList.posts[post.id] = post;
+ if (isNewPost && postList.order.indexOf(post.id) === -1) {
+ postList.order.unshift(post.id);
+ }
- this.makePostsInfo(post.channel_id);
- this.postsInfo[post.channel_id].postList = postList;
+ this.makePostsInfo(post.channel_id);
+ this.postsInfo[id].postList = postList;
+ });
}
storeFocusedPost(postId, channelId, postList) {
@@ -500,6 +521,18 @@ class PostStoreClass extends EventEmitter {
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
}
+ emitPostPinnedChange() {
+ this.emit(POST_PINNED_CHANGE_EVENT);
+ }
+
+ addPostPinnedChangeListener(callback) {
+ this.on(POST_PINNED_CHANGE_EVENT, callback);
+ }
+
+ removePostPinnedChangeListener(callback) {
+ this.removeListener(POST_PINNED_CHANGE_EVENT, callback);
+ }
+
getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
@@ -686,6 +719,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
PostStore.storeSelectedPostId(action.postId);
PostStore.emitSelectedPostChange(action.from_search, action.from_flagged_posts);
break;
+ case ActionTypes.RECEIVED_POST_PINNED:
+ case ActionTypes.RECEIVED_POST_UNPINNED:
+ PostStore.emitPostPinnedChange();
+ break;
default:
}
});
diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx
index 46a086ddb..49f8b3c2f 100644
--- a/webapp/stores/search_store.jsx
+++ b/webapp/stores/search_store.jsx
@@ -19,6 +19,7 @@ class SearchStoreClass extends EventEmitter {
this.searchResults = null;
this.isMentionSearch = false;
this.isFlaggedPosts = false;
+ this.isPinnedPosts = false;
this.isVisible = false;
this.searchTerm = '';
}
@@ -83,6 +84,10 @@ class SearchStoreClass extends EventEmitter {
return this.isFlaggedPosts;
}
+ getIsPinnedPosts() {
+ return this.isPinnedPosts;
+ }
+
storeSearchTerm(term) {
this.searchTerm = term;
}
@@ -91,10 +96,11 @@ class SearchStoreClass extends EventEmitter {
return this.searchTerm;
}
- storeSearchResults(results, isMentionSearch, isFlaggedPosts) {
+ storeSearchResults(results, isMentionSearch, isFlaggedPosts, isPinnedPosts) {
this.searchResults = results;
this.isMentionSearch = isMentionSearch;
this.isFlaggedPosts = isFlaggedPosts;
+ this.isPinnedPosts = isPinnedPosts;
}
deletePost(post) {
@@ -120,7 +126,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.RECEIVED_SEARCH:
- SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts);
+ SearchStore.storeSearchResults(action.results, action.is_mention_search, action.is_flagged_posts, action.is_pinned_posts);
SearchStore.emitSearchChange();
break;
case ActionTypes.RECEIVED_SEARCH_TERM:
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index e1449e3c5..f4faba934 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1607,6 +1607,40 @@ export function deleteEmoji(id) {
);
}
+export function pinPost(channelId, reaction) {
+ Client.pinPost(
+ channelId,
+ reaction,
+ () => {
+ // the "post_edited" websocket event take cares of updating the posts
+ // the action below is mostly dispatched for the RHS to update
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST_PINNED
+ });
+ },
+ (err) => {
+ dispatchError(err, 'pinPost');
+ }
+ );
+}
+
+export function unpinPost(channelId, reaction) {
+ Client.unpinPost(
+ channelId,
+ reaction,
+ () => {
+ // the "post_edited" websocket event take cares of updating the posts
+ // the action below is mostly dispatched for the RHS to update
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST_UNPINNED
+ });
+ },
+ (err) => {
+ dispatchError(err, 'unpinPost');
+ }
+ );
+}
+
export function saveReaction(channelId, reaction) {
Client.saveReaction(
channelId,
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index cb55da9b8..39dca99b3 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -90,6 +90,8 @@ export const ActionTypes = keyMirror({
RECEIVED_POST_SELECTED: null,
RECEIVED_MENTION_DATA: null,
RECEIVED_ADD_MENTION: null,
+ RECEIVED_POST_PINNED: null,
+ RECEIVED_POST_UNPINNED: null,
RECEIVED_PROFILES: null,
RECEIVED_PROFILES_IN_TEAM: null,
@@ -419,6 +421,7 @@ export const Constants = {
REPLY_ICON: "<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='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>",
SCROLL_BOTTOM_ICON: "<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='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>",
VIDEO_ICON: "<svg width='55%'height='100%'viewBox='0 0 13 8'> <g transform='matrix(1,0,0,1,-507,-146)'> <g transform='matrix(0.0133892,0,0,0.014499,500.635,142.838)'> <path d='M1158,547.286L1158,644.276C1158,684.245 1125.55,716.694 1085.58,716.694L579.341,716.694C539.372,716.694 506.922,684.245 506.922,644.276L506.922,306.322C506.922,266.353 539.371,233.904 579.341,233.903L1085.58,233.903C1125.55,233.904 1158,266.353 1158,306.322L1158,402.939L1359.75,253.14C1365.83,248.362 1373.43,245.973 1382.56,245.973C1386.61,245.973 1390.83,246.602 1395.22,247.859C1408.4,252.134 1414.99,259.552 1414.99,270.113L1414.99,680.485C1414.99,691.046 1408.4,698.464 1395.22,702.739C1390.83,703.996 1386.61,704.624 1382.56,704.624C1373.43,704.624 1365.83,702.236 1359.75,697.458L1158,547.286Z'/> </g> </g> </svg>",
+ PIN_ICON: "<svg width='16px' height='16px' viewBox='0 0 25 25' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='1.414'><path d='M24.78 9.236L15.863.316l-1.487 4.46-4.46 4.46L8.43 7.75 3.972 9.235l4.458 4.458L.776 24.388l10.627-7.72 4.46 4.46 1.485-4.46-1.486-1.485 4.46-4.46 4.46-1.487z' fill-rule='nonzero'/></svg>",
THEMES: {
default: {
type: 'Organization',
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 58278c893..34573a2e5 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -591,6 +591,7 @@ export function applyTheme(theme) {
changeCss('.app__body .markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07));
changeCss('.app__body .channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
changeCss('.app__body .channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
+ changeCss('.app__body .channel-header #pinned-posts-button', 'fill:' + changeOpacity(theme.centerChannelColor, 0.8));
changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header, .app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25));
changeCss('.app__body .search-help-popover .search-autocomplete__divider span, .app__body .suggestion-list__divider > span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7));
@@ -659,12 +660,12 @@ export function applyTheme(theme) {
}
if (theme.buttonBg) {
- changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active', 'background:' + theme.buttonBg);
+ changeCss('.app__body .btn.btn-primary, .app__body .tutorial__circles .circle.active, .app__body .post__pinned-badge', 'background:' + theme.buttonBg);
changeCss('.app__body .btn.btn-primary:hover, .app__body .btn.btn-primary:active, .app__body .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25));
}
if (theme.buttonColor) {
- changeCss('.app__body .btn.btn-primary', 'color:' + theme.buttonColor);
+ changeCss('.app__body .btn.btn-primary, .app__body .post__pinned-badge', 'color:' + theme.buttonColor);
}
if (theme.mentionHighlightBg) {