summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/.eslintrc.json11
-rw-r--r--webapp/actions/channel_actions.jsx1
-rw-r--r--webapp/actions/post_actions.jsx36
-rw-r--r--webapp/client/client.jsx27
-rw-r--r--webapp/components/admin_console/password_settings.jsx16
-rw-r--r--webapp/components/admin_console/reset_password_modal.jsx2
-rw-r--r--webapp/components/audio_video_preview.jsx4
-rw-r--r--webapp/components/channel_header.jsx60
-rw-r--r--webapp/components/create_team/components/display_name.jsx9
-rw-r--r--webapp/components/integrations/components/confirm_integration.jsx11
-rw-r--r--webapp/components/login/login_controller.jsx1
-rw-r--r--webapp/components/navbar.jsx38
-rw-r--r--webapp/components/needs_team.jsx16
-rw-r--r--webapp/components/new_channel_modal.jsx6
-rw-r--r--webapp/components/popover_list_members.jsx14
-rw-r--r--webapp/components/post_view/components/date_separator.jsx27
-rw-r--r--webapp/components/post_view/components/post_attachment.jsx3
-rw-r--r--webapp/components/post_view/components/post_attachment_opengraph.jsx23
-rw-r--r--webapp/components/post_view/components/post_image.jsx2
-rw-r--r--webapp/components/post_view/components/post_info.jsx61
-rw-r--r--webapp/components/post_view/components/post_time.jsx7
-rw-r--r--webapp/components/profile_picture.jsx2
-rw-r--r--webapp/components/profile_popover.jsx1
-rw-r--r--webapp/components/rhs_comment.jsx66
-rw-r--r--webapp/components/rhs_root_post.jsx66
-rw-r--r--webapp/components/rhs_thread.jsx90
-rw-r--r--webapp/components/search_bar.jsx5
-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/searchable_user_list.jsx12
-rw-r--r--webapp/components/setting_item_max.jsx24
-rw-r--r--webapp/components/setting_item_min.jsx8
-rw-r--r--webapp/components/setting_picture.jsx52
-rw-r--r--webapp/components/sidebar.jsx14
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx2
-rw-r--r--webapp/components/sidebar_right.jsx23
-rw-r--r--webapp/components/team_general_tab.jsx9
-rw-r--r--webapp/components/user_settings/desktop_notification_settings.jsx9
-rw-r--r--webapp/components/user_settings/email_notification_setting.jsx4
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx21
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx18
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx3
-rw-r--r--webapp/components/view_image.jsx12
-rw-r--r--webapp/i18n/en.json16
-rw-r--r--webapp/package.json64
-rw-r--r--webapp/sass/components/_modal.scss2
-rw-r--r--webapp/sass/components/_popover.scss9
-rw-r--r--webapp/sass/components/_tooltip.scss6
-rw-r--r--webapp/sass/layout/_content.scss28
-rw-r--r--webapp/sass/layout/_forms.scss5
-rw-r--r--webapp/sass/layout/_headers.scss29
-rw-r--r--webapp/sass/layout/_post-right.scss9
-rw-r--r--webapp/sass/layout/_post.scss20
-rw-r--r--webapp/sass/layout/_webhooks.scss16
-rw-r--r--webapp/sass/responsive/_desktop.scss17
-rw-r--r--webapp/sass/responsive/_mobile.scss8
-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/tests/formatting_imgs.test.jsx55
-rw-r--r--webapp/utils/async_client.jsx40
-rw-r--r--webapp/utils/constants.jsx5
-rw-r--r--webapp/utils/markdown.jsx1
-rw-r--r--webapp/utils/utils.jsx17
-rw-r--r--webapp/webpack.config.js18
66 files changed, 1089 insertions, 252 deletions
diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json
index 183df2017..bb4721c22 100644
--- a/webapp/.eslintrc.json
+++ b/webapp/.eslintrc.json
@@ -37,6 +37,7 @@
"block-scoped-var": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": false }],
"camelcase": [2, {"properties": "never"}],
+ "capitalized-comments": 0,
"class-methods-use-this": 1,
"comma-dangle": [2, "never"],
"comma-spacing": [2, {"before": false, "after": true}],
@@ -76,9 +77,11 @@
"newline-per-chained-call": 0,
"no-alert": 2,
"no-array-constructor": 2,
+ "no-await-in-loop": 2,
"no-caller": 2,
"no-case-declarations": 2,
"no-class-assign": 2,
+ "no-compare-neg-zero": 2,
"no-cond-assign": [2, "except-parens"],
"no-confusing-arrow": 2,
"no-console": 2,
@@ -120,6 +123,7 @@
"no-magic-numbers": [1, { "ignore": [-1, 0, 1, 2], "enforceConst": true, "detectObjects": true } ],
"no-mixed-operators": [2, {"allowSamePrecedence": false}],
"no-mixed-spaces-and-tabs": 2,
+ "no-multi-assign": 2,
"no-multi-spaces": [2, { "exceptions": { "Property": false } }],
"no-multi-str": 0,
"no-multiple-empty-lines": [2, {"max": 1}],
@@ -181,11 +185,14 @@
"object-shorthand": [2, "always"],
"one-var": [2, "never"],
"one-var-declaration-per-line": 0,
+ "operator-assignment": [2, "always"],
"operator-linebreak": [2, "after"],
"padded-blocks": [2, "never"],
"prefer-arrow-callback": 2,
"prefer-const": 2,
+ "prefer-destructuring": 0,
"prefer-numeric-literals": 2,
+ "prefer-promise-reject-errors": 2,
"prefer-rest-params": 2,
"prefer-spread": 2,
"prefer-template": 0,
@@ -194,6 +201,7 @@
"radix": 2,
"react/display-name": [2, { "ignoreTranspilerName": false }],
"react/forbid-component-props": 0,
+ "react/forbid-elements": [2, { "forbid": ["embed"] }],
"react/jsx-boolean-value": [2, "always"],
"react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }],
"react/jsx-curly-spacing": [2, "never"],
@@ -217,6 +225,7 @@
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/jsx-wrap-multilines": 2,
+ "react/no-array-index-key": 1,
"react/no-children-prop": 2,
"react/no-danger": 0,
"react/no-danger-with-children": 2,
@@ -236,11 +245,13 @@
"react/prefer-es6-class": 2,
"react/prefer-stateless-function": 0,
"react/prop-types": 2,
+ "react/require-default-props": 0,
"react/require-optimization": 1,
"react/require-render-return": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
"react/style-prop-object": 2,
+ "require-await": 2,
"require-yield": 2,
"rest-spread-spacing": [2, "never"],
"semi": [2, "always"],
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index e3563d79c..acbc943cf 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -313,6 +313,7 @@ export function joinChannel(channel, success, error) {
channel.id,
() => {
ChannelStore.removeMoreChannel(channel.id);
+ ChannelStore.storeChannel(channel);
if (success) {
success();
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/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx
index 3707977b8..43ec40904 100644
--- a/webapp/components/admin_console/password_settings.jsx
+++ b/webapp/components/admin_console/password_settings.jsx
@@ -39,16 +39,16 @@ export default class PasswordSettings extends AdminSettings {
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.PasswordRequirements === 'true') {
let sampleErrorMsgId = 'user.settings.security.passwordError';
if (props.config.PasswordSettings.Lowercase) {
- sampleErrorMsgId = sampleErrorMsgId + 'Lowercase';
+ sampleErrorMsgId += 'Lowercase';
}
if (props.config.PasswordSettings.Uppercase) {
- sampleErrorMsgId = sampleErrorMsgId + 'Uppercase';
+ sampleErrorMsgId += 'Uppercase';
}
if (props.config.PasswordSettings.Number) {
- sampleErrorMsgId = sampleErrorMsgId + 'Number';
+ sampleErrorMsgId += 'Number';
}
if (props.config.PasswordSettings.Symbol) {
- sampleErrorMsgId = sampleErrorMsgId + 'Symbol';
+ sampleErrorMsgId += 'Symbol';
}
this.sampleErrorMsg = (
<FormattedMessage
@@ -101,16 +101,16 @@ export default class PasswordSettings extends AdminSettings {
}
let sampleErrorMsgId = 'user.settings.security.passwordError';
if (this.refs.lowercase.checked) {
- sampleErrorMsgId = sampleErrorMsgId + 'Lowercase';
+ sampleErrorMsgId += 'Lowercase';
}
if (this.refs.uppercase.checked) {
- sampleErrorMsgId = sampleErrorMsgId + 'Uppercase';
+ sampleErrorMsgId += 'Uppercase';
}
if (this.refs.number.checked) {
- sampleErrorMsgId = sampleErrorMsgId + 'Number';
+ sampleErrorMsgId += 'Number';
}
if (this.refs.symbol.checked) {
- sampleErrorMsgId = sampleErrorMsgId + 'Symbol';
+ sampleErrorMsgId += 'Symbol';
}
return (
<FormattedMessage
diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
index 757f85517..1b9e5b37a 100644
--- a/webapp/components/admin_console/reset_password_modal.jsx
+++ b/webapp/components/admin_console/reset_password_modal.jsx
@@ -61,7 +61,7 @@ class ResetPasswordModal extends React.Component {
if (this.state.serverError) {
urlClass += ' has-error';
- serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ serverError = <div className='has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
}
let title;
diff --git a/webapp/components/audio_video_preview.jsx b/webapp/components/audio_video_preview.jsx
index 4956900a9..9a55e4835 100644
--- a/webapp/components/audio_video_preview.jsx
+++ b/webapp/components/audio_video_preview.jsx
@@ -94,7 +94,6 @@ export default class AudioVideoPreview extends React.Component {
<video
key={this.props.fileInfo.id}
ref='video'
- style={{maxHeight: this.props.maxHeight}}
data-setup='{}'
controls='controls'
width={width}
@@ -111,6 +110,5 @@ export default class AudioVideoPreview extends React.Component {
AudioVideoPreview.propTypes = {
fileInfo: React.PropTypes.object.isRequired,
- fileUrl: React.PropTypes.string.isRequired,
- maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired
+ fileUrl: React.PropTypes.string.isRequired
};
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 01e1e98cf..120846b8d 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
@@ -230,7 +241,10 @@ export default class ChannelHeader extends React.Component {
);
const flaggedTooltip = (
- <Tooltip id='flaggedTooltip'>
+ <Tooltip
+ id='flaggedTooltip'
+ className='text-nowrap'
+ >
<FormattedMessage
id='channel_header.flagged'
defaultMessage='Flagged Posts'
@@ -665,19 +679,27 @@ export default class ChannelHeader extends React.Component {
headerText = channel.header;
}
- const toggleFavoriteTooltip = (
- <Tooltip id='favoriteTooltip'>
- {this.state.isFavorite ?
+ let toggleFavoriteTooltip;
+ if (this.state.isFavorite) {
+ toggleFavoriteTooltip = (
+ <Tooltip id='favoriteTooltip'>
<FormattedMessage
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
- /> :
- <FormattedMessage
- id='channelHeader.addToFavorites'
- defaultMessage='Add to Favorites'
- />}
- </Tooltip>
- );
+ />
+ </Tooltip>
+ );
+ } else {
+ toggleFavoriteTooltip = (
+ <Tooltip id='favoriteTooltip'>
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />
+ </Tooltip>
+ );
+ }
+
const toggleFavorite = (
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
@@ -762,8 +784,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/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx
index aeb8afbb9..865c0e6db 100644
--- a/webapp/components/create_team/components/display_name.jsx
+++ b/webapp/components/create_team/components/display_name.jsx
@@ -10,7 +10,6 @@ import logoImage from 'images/logo.png';
import React from 'react';
import ReactDOM from 'react-dom';
-import {Link} from 'react-router/es6';
import {FormattedMessage} from 'react-intl';
export default class TeamSignupDisplayNamePage extends React.Component {
@@ -118,14 +117,6 @@ export default class TeamSignupDisplayNamePage extends React.Component {
defaultMessage='Next'
/><i className='fa fa-chevron-right'/>
</button>
- <div className='margin--extra'>
- <Link to='/select_team'>
- <FormattedMessage
- id='create_team.display_name.back'
- defaultMessage='Back to previous step'
- />
- </Link>
- </div>
</form>
</div>
);
diff --git a/webapp/components/integrations/components/confirm_integration.jsx b/webapp/components/integrations/components/confirm_integration.jsx
index 6d778f241..b4f299d1c 100644
--- a/webapp/components/integrations/components/confirm_integration.jsx
+++ b/webapp/components/integrations/components/confirm_integration.jsx
@@ -5,7 +5,7 @@ import React from 'react';
import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-import {Link} from 'react-router/es6';
+import {Link, browserHistory} from 'react-router/es6';
import UserStore from 'stores/user_store.jsx';
import IntegrationStore from 'stores/integration_store.jsx';
@@ -25,6 +25,7 @@ export default class ConfirmIntegration extends React.Component {
super(props);
this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+ this.handleKeyPress = this.handleKeyPress.bind(this);
const userId = UserStore.getCurrentId();
@@ -38,10 +39,12 @@ export default class ConfirmIntegration extends React.Component {
componentDidMount() {
IntegrationStore.addChangeListener(this.handleIntegrationChange);
+ window.addEventListener('keypress', this.handleKeyPress);
}
componentWillUnmount() {
IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ window.removeEventListener('keypress', this.handleKeyPress);
}
handleIntegrationChange() {
@@ -53,6 +56,12 @@ export default class ConfirmIntegration extends React.Component {
});
}
+ handleKeyPress(e) {
+ if (e.key === 'Enter') {
+ browserHistory.push('/' + this.props.team.name + '/integrations/' + this.state.type);
+ }
+ }
+
render() {
let headerText = null;
let helpText = null;
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 38c594cf7..01da0199f 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -413,6 +413,7 @@ export default class LoginController extends React.Component {
</div>
<div className='form-group'>
<button
+ id='loginButton'
type='submit'
className='btn btn-primary'
>
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index c945a0b9c..28d8fae05 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'>
@@ -525,10 +554,10 @@ export default class Navbar extends React.Component {
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
/> :
- <FormattedMessage
- id='channelHeader.addToFavorites'
- defaultMessage='Add to Favorites'
- />}
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />}
</a>
</li>
);
@@ -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 5cb714ebc..11e75bfb7 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 {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx';
@@ -177,12 +178,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/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index 2f9533b0e..f16b4596f 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -77,7 +77,7 @@ export default class NewChannelModal extends React.Component {
e.preventDefault();
const displayName = ReactDOM.findDOMNode(this.refs.display_name).value.trim();
- if (displayName.length < 1) {
+ if (displayName.length < Constants.MIN_CHANNELNAME_LENGTH) {
this.setState({displayNameError: true});
return;
}
@@ -104,7 +104,7 @@ export default class NewChannelModal extends React.Component {
<p className='input__help error'>
<FormattedMessage
id='channel_modal.displayNameError'
- defaultMessage='This field is required'
+ defaultMessage='Channel name must be 2 or more characters'
/>
{this.state.displayNameError}
</p>
@@ -232,7 +232,7 @@ export default class NewChannelModal extends React.Component {
ref='display_name'
className='form-control'
placeholder={Utils.localizeMessage('channel_modal.nameEx', 'E.g.: "Bugs", "Marketing", "客户支持"')}
- maxLength='22'
+ maxLength={Constants.MAX_CHANNELNAME_LENGTH}
value={this.props.channelData.displayName}
autoFocus={true}
tabIndex='1'
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/date_separator.jsx b/webapp/components/post_view/components/date_separator.jsx
new file mode 100644
index 000000000..18dc0c7ff
--- /dev/null
+++ b/webapp/components/post_view/components/date_separator.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import {FormattedDate} from 'react-intl';
+
+export default class DateSeparator extends React.Component {
+ render() {
+ return (
+ <div
+ className='date-separator'
+ >
+ <hr className='separator__hr'/>
+ <div className='separator__text'>
+ <FormattedDate
+ value={this.props.date}
+ weekday='short'
+ month='short'
+ day='2-digit'
+ year='numeric'
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+DateSeparator.propTypes = {
+ date: React.PropTypes.instanceOf(Date)
+};
diff --git a/webapp/components/post_view/components/post_attachment.jsx b/webapp/components/post_view/components/post_attachment.jsx
index 57335b94a..1b2cddcd6 100644
--- a/webapp/components/post_view/components/post_attachment.jsx
+++ b/webapp/components/post_view/components/post_attachment.jsx
@@ -184,6 +184,7 @@ class PostAttachment extends React.Component {
author.push(
<img
className='attachment__author-icon'
+ crossOrigin='anonymous'
src={data.author_icon}
key={'attachment__author-icon'}
height='14'
@@ -257,6 +258,7 @@ class PostAttachment extends React.Component {
image = (
<img
className='attachment__image'
+ crossOrigin='anonymous'
src={data.image_url}
/>
);
@@ -269,6 +271,7 @@ class PostAttachment extends React.Component {
className='attachment__thumb-container'
>
<img
+ crossOrigin='anonymous'
src={data.thumb_url}
/>
</div>
diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx
index 12437e672..13171202a 100644
--- a/webapp/components/post_view/components/post_attachment_opengraph.jsx
+++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx
@@ -32,7 +32,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
this.onImageLoad = this.onImageLoad.bind(this);
this.onImageError = this.onImageError.bind(this);
this.truncateText = this.truncateText.bind(this);
- this.setImageWidth = this.setImageWidth.bind(this);
}
IMAGE_LOADED = {
@@ -75,20 +74,16 @@ export default class PostAttachmentOpenGraph extends React.Component {
componentDidMount() {
OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
- this.setImageWidth();
- window.addEventListener('resize', this.setImageWidth);
}
componentDidUpdate() {
if (this.props.childComponentDidUpdateFunction) {
this.props.childComponentDidUpdateFunction();
}
- this.setImageWidth();
}
componentWillUnmount() {
OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
- window.removeEventListener('resize', this.setImageWidth);
}
onOpenGraphMetadataChange(url) {
@@ -163,9 +158,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
return (
<div
className='attachment__image__container--openraph'
- style={{
- width: (this.imageDimentions.height * this.imageRatio) + this.smallImageContainerLeftPadding
- }} // Initially set the width accordinly to max image heigh, ie 80px. Later on it would be modified according to actul height of image.
ref={(div) => {
this.smallImageContainer = div;
}}
@@ -201,6 +193,7 @@ export default class PostAttachmentOpenGraph extends React.Component {
element = this.wrapInSmallImageContainer(
<img
className={'attachment__image attachment__image--openraph'}
+ crossOrigin='anonymous'
src={imageUrl}
ref={(img) => {
this.smallImageElement = img;
@@ -215,20 +208,6 @@ export default class PostAttachmentOpenGraph extends React.Component {
return element;
}
- setImageWidth() {
- if (
- this.state.imageLoaded === this.IMAGE_LOADED.YES &&
- this.smallImageContainer &&
- this.smallImageElement
- ) {
- this.smallImageContainer.style.width = (
- (this.smallImageElement.offsetHeight * this.imageRatio) +
- this.smallImageContainerLeftPadding +
- 'px'
- );
- }
- }
-
truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) {
if (text.length > maxLength) {
return text.substring(0, maxLength - ellipsis.length) + ellipsis;
diff --git a/webapp/components/post_view/components/post_image.jsx b/webapp/components/post_view/components/post_image.jsx
index 9a761bfca..6fe954e99 100644
--- a/webapp/components/post_view/components/post_image.jsx
+++ b/webapp/components/post_view/components/post_image.jsx
@@ -67,6 +67,7 @@ export default class PostImageEmbed extends React.Component {
return (
<img
className='img-div placeholder'
+ crossOrigin='anonymous'
height='500px'
/>
);
@@ -75,6 +76,7 @@ export default class PostImageEmbed extends React.Component {
return (
<img
className='img-div'
+ crossOrigin='anonymous'
src={this.props.link}
/>
);
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/post_view/components/post_time.jsx b/webapp/components/post_view/components/post_time.jsx
index 25d533e0a..77f3f3266 100644
--- a/webapp/components/post_view/components/post_time.jsx
+++ b/webapp/components/post_view/components/post_time.jsx
@@ -40,12 +40,15 @@ export default class PostTime extends React.Component {
}
renderTimeTag() {
+ const date = getDateForUnixTicks(this.props.eventTime);
+
return (
<time
className='post__time'
- dateTime={getDateForUnixTicks(this.props.eventTime).toISOString()}
+ dateTime={date.toISOString()}
+ title={date}
>
- {getDateForUnixTicks(this.props.eventTime).toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})}
+ {date.toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})}
</time>
);
}
diff --git a/webapp/components/profile_picture.jsx b/webapp/components/profile_picture.jsx
index 7a5f892db..737a4400b 100644
--- a/webapp/components/profile_picture.jsx
+++ b/webapp/components/profile_picture.jsx
@@ -69,6 +69,7 @@ export default class ProfilePicture extends React.Component {
width={this.props.width}
height={this.props.width}
src={this.props.src}
+ crossOrigin='anonymous'
/>
<StatusIcon status={this.props.status}/>
</span>
@@ -82,6 +83,7 @@ export default class ProfilePicture extends React.Component {
width={this.props.width}
height={this.props.width}
src={this.props.src}
+ crossOrigin='anonymous'
/>
<StatusIcon status={this.props.status}/>
</span>
diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx
index c7d45474f..e21716cb3 100644
--- a/webapp/components/profile_popover.jsx
+++ b/webapp/components/profile_popover.jsx
@@ -182,6 +182,7 @@ export default class ProfilePopover extends React.Component {
height='128'
width='128'
key='user-popover-image'
+ crossOrigin='anonymous'
/>
);
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index cb527d850..52e4d9851 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,10 +551,19 @@ 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',
- year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: !this.props.useMilitaryTime
@@ -524,6 +581,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..83d930bca 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,10 +498,19 @@ 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',
- year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: !this.props.useMilitaryTime
@@ -470,6 +527,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/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index 3c0b2e114..2c1d03901 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -8,6 +8,7 @@ import RootPost from './rhs_root_post.jsx';
import Comment from './rhs_comment.jsx';
import FileUploadOverlay from './file_upload_overlay.jsx';
import FloatingTimestamp from './post_view/components/floating_timestamp.jsx';
+import DateSeparator from './post_view/components/date_separator.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -325,6 +326,7 @@ export default class RhsThread extends React.Component {
const postsArray = this.state.postsArray;
const selected = this.state.selected;
const profiles = this.state.profiles || {};
+ let previousPostDay = Utils.getDateForUnixTicks(selected.create_at);
if (postsArray == null || selected == null) {
return (
@@ -355,6 +357,55 @@ export default class RhsThread extends React.Component {
rootStatus = this.state.statuses[selected.user_id] || 'offline';
}
+ const commentsLists = [];
+ for (let i = 0; i < postsArray.length; i++) {
+ const comPost = postsArray[i];
+ let p;
+ if (UserStore.getCurrentId() === comPost.user_id) {
+ p = UserStore.getCurrentUser();
+ } else {
+ p = profiles[comPost.user_id];
+ }
+
+ let isFlagged = false;
+ if (this.state.flaggedPosts) {
+ isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true';
+ }
+
+ let status = 'offline';
+ if (this.state.statuses && p && p.id) {
+ status = this.state.statuses[p.id] || 'offline';
+ }
+
+ const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id;
+
+ const currentPostDay = Utils.getDateForUnixTicks(comPost.create_at);
+
+ if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
+ previousPostDay = currentPostDay;
+ commentsLists.push(
+ <DateSeparator
+ date={currentPostDay}
+ />);
+ }
+
+ commentsLists.push(
+ <div key={keyPrefix + 'commentKey'}>
+ <Comment
+ ref={comPost.id}
+ post={comPost}
+ user={p}
+ currentUser={this.props.currentUser}
+ compactDisplay={this.state.compactDisplay}
+ useMilitaryTime={this.props.useMilitaryTime}
+ isFlagged={isFlagged}
+ status={status}
+ isBusy={this.state.isBusy}
+ />
+ </div>
+ );
+ }
+
return (
<div className='post-right__container'>
<FileUploadOverlay overlayType='right'/>
@@ -384,6 +435,9 @@ export default class RhsThread extends React.Component {
onScroll={this.handleScroll}
>
<div className='post-right__scroll'>
+ <DateSeparator
+ date={previousPostDay}
+ />
<RootPost
ref={selected.id}
post={selected}
@@ -401,41 +455,7 @@ export default class RhsThread extends React.Component {
ref='rhspostlist'
className='post-right-comments-container'
>
- {postsArray.map((comPost) => {
- let p;
- if (UserStore.getCurrentId() === comPost.user_id) {
- p = UserStore.getCurrentUser();
- } else {
- p = profiles[comPost.user_id];
- }
-
- let isFlagged = false;
- if (this.state.flaggedPosts) {
- isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true';
- }
-
- let status = 'offline';
- if (this.state.statuses && p && p.id) {
- status = this.state.statuses[p.id] || 'offline';
- }
-
- const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id;
-
- return (
- <Comment
- ref={comPost.id}
- key={keyPrefix + 'commentKey'}
- post={comPost}
- user={p}
- currentUser={this.props.currentUser}
- compactDisplay={this.state.compactDisplay}
- useMilitaryTime={this.props.useMilitaryTime}
- isFlagged={isFlagged}
- status={status}
- isBusy={this.state.isBusy}
- />
- );
- })}
+ {commentsLists}
</div>
<div className='post-create__container'>
<CreateComment
diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx
index 1c9f607e6..b88e67a11 100644
--- a/webapp/components/search_bar.jsx
+++ b/webapp/components/search_bar.jsx
@@ -216,7 +216,10 @@ export default class SearchBar extends React.Component {
);
const flaggedTooltip = (
- <Tooltip id='flaggedTooltip'>
+ <Tooltip
+ id='flaggedTooltip'
+ className='text-nowrap'
+ >
<FormattedMessage
id='channel_header.flagged'
defaultMessage='Flagged Posts'
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/searchable_user_list.jsx b/webapp/components/searchable_user_list.jsx
index d25c8a506..ab3f9ee9b 100644
--- a/webapp/components/searchable_user_list.jsx
+++ b/webapp/components/searchable_user_list.jsx
@@ -19,6 +19,7 @@ export default class SearchableUserList extends React.Component {
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.doSearch = this.doSearch.bind(this);
+ this.focusSearchBar = this.focusSearchBar.bind(this);
this.nextTimeoutId = 0;
@@ -30,15 +31,14 @@ export default class SearchableUserList extends React.Component {
}
componentDidMount() {
- if (this.props.focusOnMount) {
- this.refs.filter.focus();
- }
+ this.focusSearchBar();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.page !== prevState.page) {
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
}
+ this.focusSearchBar();
}
componentWillUnmount() {
@@ -57,6 +57,12 @@ export default class SearchableUserList extends React.Component {
this.setState({page: this.state.page - 1});
}
+ focusSearchBar() {
+ if (this.props.focusOnMount) {
+ this.refs.filter.focus();
+ }
+ }
+
doSearch() {
const term = this.refs.filter.value;
this.props.search(term);
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index 5b6a5d53a..9f3c4f0cf 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -31,12 +31,30 @@ export default class SettingItemMax extends React.Component {
render() {
var clientError = null;
if (this.props.client_error) {
- clientError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.client_error}</label></div>);
+ clientError = (
+ <div className='form-group'>
+ <label
+ id='clientError'
+ className='col-sm-12 has-error'
+ >
+ {this.props.client_error}
+ </label>
+ </div>
+ );
}
var serverError = null;
if (this.props.server_error) {
- serverError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.server_error}</label></div>);
+ serverError = (
+ <div className='form-group'>
+ <label
+ id='serverError'
+ className='col-sm-12 has-error'
+ >
+ {this.props.server_error}
+ </label>
+ </div>
+ );
}
var extraInfo = null;
@@ -48,6 +66,7 @@ export default class SettingItemMax extends React.Component {
if (this.props.submit) {
submit = (
<input
+ id='saveSetting'
type='submit'
className='btn btn-sm btn-primary'
href='#'
@@ -88,6 +107,7 @@ export default class SettingItemMax extends React.Component {
{clientError}
{submit}
<a
+ id={this.props.title + 'Cancel'}
className='btn btn-sm'
href='#'
onClick={this.props.updateSection}
diff --git a/webapp/components/setting_item_min.jsx b/webapp/components/setting_item_min.jsx
index 96d8bf459..4f756c46e 100644
--- a/webapp/components/setting_item_min.jsx
+++ b/webapp/components/setting_item_min.jsx
@@ -12,6 +12,7 @@ export default class SettingItemMin extends React.Component {
editButton = (
<li className='col-xs-12 col-sm-3 section-edit'>
<a
+ id={this.props.title}
className='theme'
href='#'
onClick={this.props.updateSection}
@@ -33,7 +34,12 @@ export default class SettingItemMin extends React.Component {
>
<li className='col-xs-12 col-sm-9 section-title'>{this.props.title}</li>
{editButton}
- <li className='col-xs-12 section-describe'>{this.props.describe}</li>
+ <li
+ id={this.props.title + 'Desc'}
+ className='col-xs-12 section-describe'
+ >
+ {this.props.describe}
+ </li>
</ul>
);
}
diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx
index b74ee8eb7..45ac4096d 100644
--- a/webapp/components/setting_picture.jsx
+++ b/webapp/components/setting_picture.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
import loadingGif from 'images/load.gif';
@@ -14,17 +12,35 @@ export default class SettingPicture extends React.Component {
super(props);
this.setPicture = this.setPicture.bind(this);
+ this.confirmImage = this.confirmImage.bind(this);
}
setPicture(file) {
if (file) {
var reader = new FileReader();
- var img = ReactDOM.findDOMNode(this.refs.image);
- reader.onload = function load(e) {
- $(img).attr('src', e.target.result);
- };
+ reader.onload = (e) => {
+ const canvas = this.refs.profileImageCanvas;
+ const context = canvas.getContext('2d');
+ const imageObj = new Image();
+ imageObj.onload = () => {
+ if (imageObj.width > imageObj.height) {
+ const side = imageObj.height;
+ const rem = imageObj.width - side;
+ const startX = parseInt(rem / 2, 10);
+ context.drawImage(imageObj, startX, 0, side, side,
+ 0, 0, canvas.width, canvas.height);
+ } else {
+ const side = imageObj.width;
+ const rem = imageObj.height - side;
+ const startY = parseInt(rem / 2, 10);
+ context.drawImage(imageObj, 0, startY, side, side,
+ 0, 0, canvas.width, canvas.height);
+ }
+ };
+ imageObj.src = e.target.result;
+ };
reader.readAsDataURL(file);
}
}
@@ -48,10 +64,11 @@ export default class SettingPicture extends React.Component {
var img = null;
if (this.props.picture) {
img = (
- <img
- ref='image'
- className='profile-img rounded'
- src=''
+ <canvas
+ ref='profileImageCanvas'
+ className='profile-img'
+ width='256px'
+ height='256px'
/>
);
} else {
@@ -83,7 +100,7 @@ export default class SettingPicture extends React.Component {
confirmButton = (
<a
className={confirmButtonClass}
- onClick={this.props.submit}
+ onClick={this.confirmImage}
>
<FormattedMessage
id='setting_picture.save'
@@ -147,6 +164,16 @@ export default class SettingPicture extends React.Component {
</ul>
);
}
+
+ confirmImage(e) {
+ e.persist();
+ this.refs.profileImageCanvas.toBlob((blob) => {
+ blob.lastModifiedDate = new Date();
+ blob.name = 'image.jpg';
+ this.props.imageCropChange(blob);
+ this.props.submit(e);
+ }, 'image/jpeg', 0.95);
+ }
}
SettingPicture.propTypes = {
@@ -158,5 +185,6 @@ SettingPicture.propTypes = {
submitActive: React.PropTypes.bool,
submit: React.PropTypes.func,
title: React.PropTypes.string,
- pictureChange: React.PropTypes.func
+ pictureChange: React.PropTypes.func,
+ imageCropChange: React.PropTypes.func
};
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index 08d80d363..b9356c5a1 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -636,13 +636,15 @@ export default class Sidebar extends React.Component {
this.lastUnreadChannel = null;
// create elements for all 4 types of channels
- const favoriteItems = this.state.favoriteChannels.map((channel, index, arr) => {
- if (channel.type === Constants.DM_CHANNEL) {
- return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
- }
+ const favoriteItems = this.state.favoriteChannels.
+ sort(Utils.sortTeamsByDisplayName).
+ map((channel, index, arr) => {
+ if (channel.type === Constants.DM_CHANNEL) {
+ return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
+ }
- return this.createChannelElement(channel);
- });
+ return this.createChannelElement(channel);
+ });
const publicChannelItems = this.state.publicChannels.map(this.createChannelElement);
diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx
index 484ca3298..34c228ac2 100644
--- a/webapp/components/sidebar_header_dropdown.jsx
+++ b/webapp/components/sidebar_header_dropdown.jsx
@@ -467,6 +467,7 @@ export default class SidebarHeaderDropdown extends React.Component {
<Dropdown.Menu>
<li>
<a
+ id='accountSettings'
href='#'
onClick={this.toggleAccountSettingsModal}
>
@@ -480,6 +481,7 @@ export default class SidebarHeaderDropdown extends React.Component {
{teamLink}
<li>
<a
+ id='logout'
href='#'
onClick={() => GlobalActions.emitUserLoggedOutEvent()}
>
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/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 0100cad64..bc6c70e7f 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -106,14 +106,11 @@ class GeneralTab extends React.Component {
let valid = true;
const name = this.state.name.trim();
- if (!name) {
+ if (name) {
+ state.clientError = '';
+ } else {
state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required');
valid = false;
- } else if (name === this.props.team.display_name) {
- state.clientError = Utils.localizeMessage('general_tab.chooseName', 'Please choose a new name for your team');
- valid = false;
- } else {
- state.clientError = '';
}
this.setState(state);
diff --git a/webapp/components/user_settings/desktop_notification_settings.jsx b/webapp/components/user_settings/desktop_notification_settings.jsx
index 3a330b623..be403ebb6 100644
--- a/webapp/components/user_settings/desktop_notification_settings.jsx
+++ b/webapp/components/user_settings/desktop_notification_settings.jsx
@@ -74,6 +74,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundOn'
type='radio'
name='notificationSounds'
checked={soundRadio[0]}
@@ -89,6 +90,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundOff'
type='radio'
name='notificationSounds'
checked={soundRadio[1]}
@@ -136,6 +138,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundDuration3'
type='radio'
name='desktopDuration'
checked={durationRadio[0]}
@@ -154,6 +157,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundDuration5'
type='radio'
name='desktopDuration'
checked={durationRadio[1]}
@@ -172,6 +176,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundDuration10'
type='radio'
name='desktopDuration'
checked={durationRadio[2]}
@@ -189,6 +194,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='soundDurationUnlimited'
type='radio'
name='desktopDuration'
checked={durationRadio[3]}
@@ -225,6 +231,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='desktopNotificationAllActivity'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[0]}
@@ -240,6 +247,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='desktopNotificationMentions'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[1]}
@@ -255,6 +263,7 @@ export default class DesktopNotificationSettings extends React.Component {
<div className='radio'>
<label>
<input
+ id='desktopNotificationNever'
type='radio'
name='desktopNotificationLevel'
checked={activityRadio[2]}
diff --git a/webapp/components/user_settings/email_notification_setting.jsx b/webapp/components/user_settings/email_notification_setting.jsx
index 457512507..1e6c5d7f5 100644
--- a/webapp/components/user_settings/email_notification_setting.jsx
+++ b/webapp/components/user_settings/email_notification_setting.jsx
@@ -113,6 +113,7 @@ export default class EmailNotificationSetting extends React.Component {
<div className='radio'>
<label>
<input
+ id='emailNotificationMinutes'
type='radio'
name='emailNotifications'
checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_FIFTEEN_MINUTES}
@@ -128,6 +129,7 @@ export default class EmailNotificationSetting extends React.Component {
<div className='radio'>
<label>
<input
+ id='emailNotificationHour'
type='radio'
name='emailNotifications'
checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_HOUR}
@@ -164,6 +166,7 @@ export default class EmailNotificationSetting extends React.Component {
<div className='radio'>
<label>
<input
+ id='emailNotificationImmediately'
type='radio'
name='emailNotifications'
checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_IMMEDIATE}
@@ -179,6 +182,7 @@ export default class EmailNotificationSetting extends React.Component {
<div className='radio'>
<label>
<input
+ id='emailNotificationNever'
type='radio'
name='emailNotifications'
checked={!this.props.enableEmail}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index f9c624aa0..d558958f0 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -101,6 +101,7 @@ class UserSettingsGeneralTab extends React.Component {
this.updatePicture = this.updatePicture.bind(this);
this.updateSection = this.updateSection.bind(this);
this.updatePosition = this.updatePosition.bind(this);
+ this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this);
this.state = this.setupInitialState(props);
}
@@ -311,6 +312,17 @@ class UserSettingsGeneralTab extends React.Component {
this.setState({confirmEmail: e.target.value});
}
+ updatedCroppedPicture(file) {
+ if (file) {
+ this.setState({picture: file});
+
+ this.submitActive = true;
+ this.setState({clientError: null});
+ } else {
+ this.setState({picture: null});
+ }
+ }
+
updatePicture(e) {
if (e.target.files && e.target.files[0]) {
this.setState({picture: e.target.files[0]});
@@ -410,6 +422,7 @@ class UserSettingsGeneralTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='primaryEmail'
className='form-control'
type='email'
onChange={this.updateEmail}
@@ -431,6 +444,7 @@ class UserSettingsGeneralTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='confirmEmail'
className='form-control'
type='email'
onChange={this.updateConfirmEmail}
@@ -684,6 +698,7 @@ class UserSettingsGeneralTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='firstName'
className='form-control'
type='text'
onChange={this.updateFirstName}
@@ -706,6 +721,7 @@ class UserSettingsGeneralTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='lastName'
className='form-control'
type='text'
onChange={this.updateLastName}
@@ -832,6 +848,7 @@ class UserSettingsGeneralTab extends React.Component {
<label className='col-sm-5 control-label'>{nicknameLabel}</label>
<div className='col-sm-7'>
<input
+ id='nickname'
className='form-control'
type='text'
onChange={this.updateNickname}
@@ -916,6 +933,7 @@ class UserSettingsGeneralTab extends React.Component {
<label className='col-sm-5 control-label'>{usernameLabel}</label>
<div className='col-sm-7'>
<input
+ id='username'
maxLength={Constants.MAX_USERNAME_LENGTH}
className='form-control'
type='text'
@@ -1006,6 +1024,7 @@ class UserSettingsGeneralTab extends React.Component {
<label className='col-sm-5 control-label'>{positionLabel}</label>
<div className='col-sm-7'>
<input
+ id='position'
className='form-control'
type='text'
onChange={this.updatePosition}
@@ -1086,6 +1105,7 @@ class UserSettingsGeneralTab extends React.Component {
pictureChange={this.updatePicture}
submitActive={this.submitActive}
loadingPicture={this.state.loadingPicture}
+ imageCropChange={this.updatedCroppedPicture}
/>
);
} else {
@@ -1123,6 +1143,7 @@ class UserSettingsGeneralTab extends React.Component {
<div>
<div className='modal-header'>
<button
+ id='closeUserSettings'
type='button'
className='close'
data-dismiss='modal'
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index 7c82488f6..ebd43e5af 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -64,6 +64,9 @@ function getNotificationsStateFromStores() {
} else {
usernameKey = true;
keys.splice(keys.indexOf(user.username), 1);
+ if (keys.indexOf(`@${user.username}`) !== -1) {
+ keys.splice(keys.indexOf(`@${user.username}`), 1);
+ }
}
customKeys = keys.join(',');
@@ -281,6 +284,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationOnline'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[0]}
@@ -296,6 +300,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationAway'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[1]}
@@ -311,6 +316,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationOffline'
type='radio'
name='pushNotificationStatus'
checked={pushStatusRadio[2]}
@@ -347,6 +353,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationAllActivity'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[0]}
@@ -362,6 +369,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationMentions'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[1]}
@@ -377,6 +385,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='pushNotificationNever'
type='radio'
name='pushNotificationLevel'
checked={pushActivityRadio[2]}
@@ -520,6 +529,7 @@ export default class NotificationsTab extends React.Component {
<div className='checkbox'>
<label>
<input
+ id='notificationTriggerFirst'
type='checkbox'
checked={this.state.firstNameKey}
onChange={handleUpdateFirstNameKey}
@@ -545,6 +555,7 @@ export default class NotificationsTab extends React.Component {
<div className='checkbox'>
<label>
<input
+ id='notificationTriggerUsername'
type='checkbox'
checked={this.state.usernameKey}
onChange={handleUpdateUsernameKey}
@@ -569,6 +580,7 @@ export default class NotificationsTab extends React.Component {
<div className='checkbox'>
<label>
<input
+ id='notificationTriggerShouts'
type='checkbox'
checked={this.state.channelKey}
onChange={handleUpdateChannelKey}
@@ -587,6 +599,7 @@ export default class NotificationsTab extends React.Component {
<div className='checkbox'>
<label>
<input
+ id='notificationTriggerCustom'
ref='customcheck'
type='checkbox'
checked={this.state.customKeysChecked}
@@ -599,6 +612,7 @@ export default class NotificationsTab extends React.Component {
</label>
</div>
<input
+ id='notificationTriggerCustomText'
ref='custommentions'
className='form-control mentions-input'
type='text'
@@ -697,6 +711,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='notificationCommentsAny'
type='radio'
name='commentsNotificationLevel'
checked={commentsActive[0]}
@@ -712,6 +727,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='notificationCommentsRoot'
type='radio'
name='commentsNotificationLevel'
checked={commentsActive[1]}
@@ -727,6 +743,7 @@ export default class NotificationsTab extends React.Component {
<div className='radio'>
<label>
<input
+ id='notificationCommentsNever'
type='radio'
name='commentsNotificationLevel'
checked={commentsActive[2]}
@@ -804,6 +821,7 @@ export default class NotificationsTab extends React.Component {
<div>
<div className='modal-header'>
<button
+ id='closeButton'
type='button'
className='close'
data-dismiss='modal'
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index b6ee2d915..9ca7f4b62 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -331,6 +331,7 @@ export default class SecurityTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='currentPassword'
className='form-control'
type='password'
onChange={this.updateCurrentPassword}
@@ -352,6 +353,7 @@ export default class SecurityTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='newPassword'
className='form-control'
type='password'
onChange={this.updateNewPassword}
@@ -373,6 +375,7 @@ export default class SecurityTab extends React.Component {
</label>
<div className='col-sm-7'>
<input
+ id='confirmPassword'
className='form-control'
type='password'
onChange={this.updateConfirmPassword}
diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx
index 385138d54..e5c3caa0a 100644
--- a/webapp/components/view_image.jsx
+++ b/webapp/components/view_image.jsx
@@ -185,7 +185,6 @@ export default class ViewImageModal extends React.Component {
<ImagePreview
fileInfo={fileInfo}
fileUrl={fileUrl}
- maxHeight={this.state.imgHeight}
/>
);
} else if (fileType === 'video' || fileType === 'audio') {
@@ -193,7 +192,6 @@ export default class ViewImageModal extends React.Component {
<AudioVideoPreview
fileInfo={fileInfo}
fileUrl={fileUrl}
- maxHeight={this.state.imgHeight}
/>
);
} else if (PDFPreview.supports(fileInfo)) {
@@ -344,7 +342,7 @@ LoadingImagePreview.propTypes = {
loading: React.PropTypes.string
};
-function ImagePreview({fileInfo, fileUrl, maxHeight}) {
+function ImagePreview({fileInfo, fileUrl}) {
let previewUrl;
if (fileInfo.has_preview_image) {
previewUrl = FileStore.getFilePreviewUrl(fileInfo.id);
@@ -359,16 +357,12 @@ function ImagePreview({fileInfo, fileUrl, maxHeight}) {
rel='noopener noreferrer'
download={true}
>
- <img
- style={{maxHeight}}
- src={previewUrl}
- />
+ <img src={previewUrl}/>
</a>
);
}
ImagePreview.propTypes = {
fileInfo: React.PropTypes.object.isRequired,
- fileUrl: React.PropTypes.string.isRequired,
- maxHeight: React.PropTypes.number.isRequired
+ fileUrl: React.PropTypes.string.isRequired
};
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 3ef467937..bc30b53e7 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1118,7 +1118,7 @@
"channel_modal.channel": "Channel",
"channel_modal.createNew": "Create New ",
"channel_modal.descriptionHelp": "Describe how this {term} should be used.",
- "channel_modal.displayNameError": "This field is required",
+ "channel_modal.displayNameError": "Channel name must be 2 or more characters",
"channel_modal.edit": "Edit",
"channel_modal.group": "Group",
"channel_modal.header": "Header",
@@ -1208,7 +1208,6 @@
"create_post.tutorialTip": "<h4>Sending Messages</h4><p>Type here to write a message and press <strong>ENTER</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>",
"create_post.write": "Write a message...",
"create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href={TermsOfServiceLink}>Terms of Service</a> and <a href={PrivacyPolicyLink}>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.",
- "create_team.display_name.back": "Back to previous step",
"create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.",
"create_team.display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.",
"create_team.display_name.next": "Next",
@@ -1324,7 +1323,6 @@
"flag_post.flag": "Flag for follow up",
"flag_post.unflag": "Unflag",
"general_tab.chooseDescription": "Please choose a new description for your team",
- "general_tab.chooseName": "Please choose a new name for your team",
"general_tab.codeDesc": "Click 'Edit' to regenerate Invite Code.",
"general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by {getTeamInviteLink} in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
"general_tab.codeTitle": "Invite Code",
@@ -1665,6 +1663,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",
@@ -1721,6 +1720,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",
@@ -1773,11 +1775,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>",
@@ -1787,6 +1792,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",
@@ -2238,6 +2246,8 @@
"user.settings.security.switchSaml": "Switch to using SAML SSO",
"user.settings.security.title": "Security Settings",
"user.settings.security.viewHistory": "View Access History",
+ "user.settings.security.active": "Active",
+ "user.settings.security.inactive": "Inactive",
"user_list.notFound": "No users found",
"user_profile.send.dm": "Send Message",
"user_profile.webrtc.call": "Start Video Call",
diff --git a/webapp/package.json b/webapp/package.json
index 216292f34..f0b1b8bb2 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -4,15 +4,15 @@
"version": "0.0.1",
"private": true,
"dependencies": {
- "autolinker": "1.4.0",
+ "autolinker": "1.4.2",
"bootstrap": "3.3.7",
- "bootstrap-colorpicker": "2.3.6",
- "chart.js": "2.4.0",
+ "bootstrap-colorpicker": "2.5.1",
+ "chart.js": "2.5.0",
"compass-mixins": "0.12.10",
"fastclick": "1.0.6",
"flux": "3.1.2",
"font-awesome": "4.7.0",
- "highlight.js": "9.9.0",
+ "highlight.js": "9.10.0",
"inobounce": "0.1.4",
"intl": "1.2.5",
"jasny-bootstrap": "3.1.3",
@@ -20,45 +20,45 @@
"marked": "mattermost/marked#8f5902fff9bad793cd6c66e0c44002c9e79e1317",
"match-at": "0.1.0",
"object-assign": "4.1.1",
- "pdfjs-dist": "1.7.235",
+ "pdfjs-dist": "1.7.363",
"perfect-scrollbar": "0.6.16",
"react": "15.4.2",
"react-addons-pure-render-mixin": "15.4.2",
- "react-bootstrap": "0.30.7",
- "react-custom-scrollbars": "4.0.1",
+ "react-bootstrap": "0.30.8",
+ "react-custom-scrollbars": "4.0.2",
"react-dom": "15.4.2",
"react-intl": "2.2.3",
"react-router": "2.8.1",
- "react-select": "1.0.0-rc.2",
- "superagent": "3.4.1",
- "twemoji": "2.2.3",
- "velocity-animate": "1.4.2",
- "webrtc-adapter": "3.1.0",
+ "react-select": "1.0.0-rc.3",
+ "superagent": "3.5.0",
+ "twemoji": "2.2.5",
+ "velocity-animate": "1.4.3",
+ "webrtc-adapter": "3.2.0",
"xregexp": "3.1.1"
},
"devDependencies": {
- "babel-core": "6.22.1",
- "babel-eslint": "7.1.0",
- "babel-loader": "6.2.10",
- "babel-plugin-transform-runtime": "6.22.0",
- "babel-polyfill": "6.22.0",
- "babel-preset-es2015": "6.22.0",
- "babel-preset-react": "6.22.0",
+ "babel-core": "6.24.0",
+ "babel-eslint": "7.1.1",
+ "babel-loader": "6.4.0",
+ "babel-plugin-transform-runtime": "6.23.0",
+ "babel-polyfill": "6.23.0",
+ "babel-preset-es2015": "6.24.0",
+ "babel-preset-react": "6.23.0",
"babel-preset-stage-0": "6.22.0",
"copy-webpack-plugin": "4.0.1",
- "cross-env": "3.1.4",
- "css-loader": "0.26.1",
- "eslint": "3.10.2",
- "eslint-plugin-react": "6.7.1",
- "exports-loader": "0.6.3",
- "extract-text-webpack-plugin": "1.0.1",
- "file-loader": "0.10.0",
- "html-loader": "0.4.4",
+ "cross-env": "3.2.3",
+ "css-loader": "0.27.3",
+ "eslint": "3.17.1",
+ "eslint-plugin-react": "6.10.0",
+ "exports-loader": "0.6.4",
+ "extract-text-webpack-plugin": "2.1.0",
+ "file-loader": "0.10.1",
+ "html-loader": "0.4.5",
"html-webpack-plugin": "2.28.0",
"image-webpack-loader": "3.2.0",
- "imports-loader": "0.7.0",
+ "imports-loader": "0.7.1",
"jquery-deferred": "0.3.1",
- "jsdom": "9.9.1",
+ "jsdom": "9.12.0",
"jsdom-global": "2.1.1",
"json-loader": "0.5.4",
"mocha": "3.2.0",
@@ -67,9 +67,9 @@
"node-sass": "4.5.0",
"raw-loader": "0.5.1",
"react-addons-test-utils": "15.4.2",
- "sass-loader": "4.1.1",
- "style-loader": "0.13.1",
- "url-loader": "0.5.7",
+ "sass-loader": "6.0.3",
+ "style-loader": "0.13.2",
+ "url-loader": "0.5.8",
"webpack": "2.2.1",
"webpack-node-externals": "1.5.4"
},
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index 93bd9fda4..bfc082ad3 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -345,7 +345,7 @@
}
img {
- max-height: 100%;
+ max-height: calc(100vh - 200px);
max-width: 100%;
}
diff --git a/webapp/sass/components/_popover.scss b/webapp/sass/components/_popover.scss
index 6b1c57725..93b567ad3 100644
--- a/webapp/sass/components/_popover.scss
+++ b/webapp/sass/components/_popover.scss
@@ -209,6 +209,15 @@
.more-modal__row {
min-height: inherit;
}
+
+ .more-modal__details {
+ line-height: 32px;
+ }
+
+ .more-modal__actions {
+ line-height: 31px;
+ margin: 0;
+ }
}
.popover-content {
diff --git a/webapp/sass/components/_tooltip.scss b/webapp/sass/components/_tooltip.scss
index 0049fe1b8..6953dad58 100644
--- a/webapp/sass/components/_tooltip.scss
+++ b/webapp/sass/components/_tooltip.scss
@@ -7,6 +7,12 @@
padding: 3px 10px 4px;
word-break: break-word;
}
+
+ &.text-nowrap {
+ .tooltip-inner {
+ white-space: nowrap;
+ }
+ }
}
#webrtcTooltip {
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/_forms.scss b/webapp/sass/layout/_forms.scss
index 7552290d8..64c74b0a5 100644
--- a/webapp/sass/layout/_forms.scss
+++ b/webapp/sass/layout/_forms.scss
@@ -62,7 +62,6 @@
.has-error {
.help-block,
- .control-label,
.radio,
.checkbox,
.radio-inline,
@@ -70,6 +69,10 @@
color: $red;
}
+ .control-label {
+ color: inherit;
+ }
+
&.radio,
&.checkbox,
&.radio-inline,
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 8ee6e8fdc..f8211d433 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 {
cursor: pointer;
- min-width: 60px;
- padding-right: 10px;
- text-align: right;
+ display: inline-block;
+ margin-left: 7px;
+ min-width: 30px;
+ 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;
}
}
@@ -48,7 +65,7 @@
}
&:last-child {
- padding-right: 8px;
+ padding-right: 6px;
width: 8.9%;
}
}
diff --git a/webapp/sass/layout/_post-right.scss b/webapp/sass/layout/_post-right.scss
index 455ed7fff..9a0f658a2 100644
--- a/webapp/sass/layout/_post-right.scss
+++ b/webapp/sass/layout/_post-right.scss
@@ -53,6 +53,12 @@
border: none;
}
+ .date-separator {
+ hr {
+ border-top: 1px solid #eee;
+ }
+ }
+
.post-create__container {
width: 100%;
@@ -147,7 +153,8 @@
@include flex(1 1 auto);
overflow: auto;
position: relative;
-
+ padding-top: 10px;
+
.file-preview__container {
margin-top: 5px;
}
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index 5ecd50468..1e1dd4b08 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -359,6 +359,9 @@
}
.post-create__container {
+ label {
+ font-weight: normal;
+ }
.custom-textarea {
overflow: hidden;
}
@@ -763,6 +766,7 @@
line-height: 1.6em;
margin: 0;
white-space: pre-wrap;
+ word-break: break-word;
}
.post__header--info {
@@ -800,7 +804,7 @@
.flag-icon__container {
left: 36px;
- margin-left: 5px;
+ margin-left: 7px;
position: absolute;
top: 8px;
}
@@ -1357,15 +1361,25 @@
}
}
-.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;
+ position: relative;
+ top: -1px;
+}
+
.permalink-text {
overflow: hidden;
}
diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss
index f3a8c6fd3..c36edb8a2 100644
--- a/webapp/sass/layout/_webhooks.scss
+++ b/webapp/sass/layout/_webhooks.scss
@@ -41,6 +41,7 @@
&.attachment--opengraph {
max-width: 800px;
}
+
.attachment__content {
border-radius: 4px;
border-style: solid;
@@ -71,16 +72,18 @@
&.attachment__container--danger {
border-left-color: #e40303;
}
+
&.attachment__container--opengraph {
display: table;
- table-layout: fixed;
- width: 100%;
margin: 0;
padding-bottom: 13px;
+ width: 100%;
+
div {
margin: 0;
}
}
+
.sitename {
color: #A3A3A3;
}
@@ -89,8 +92,8 @@
.attachment__body__wrap {
&.attachment__body__wrap--opengraph {
display: table-cell;
- width: 100%;
vertical-align: top;
+ width: 100%;
}
}
@@ -104,6 +107,7 @@
&.attachment__body--no_thumb {
width: 100%;
}
+
&.attachment__body--opengraph {
float: none;
padding-right: 0;
@@ -142,6 +146,7 @@
margin-top: 10px;
max-height: 200px;
max-width: 400px;
+ width: 100%;
&.loading {
height: 150px;
@@ -164,16 +169,17 @@
&.has-link {
color: #2f81b7;
- text-overflow: ellipsis;
overflow: hidden;
+ text-overflow: ellipsis;
white-space: nowrap;
}
&.attachment__title--opengraph {
height: auto;
word-wrap: break-word;
+
&.is-url {
- word-break: break-all
+ word-break: break-all;
}
}
}
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 891b0ed48..4fbec082a 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -1,6 +1,10 @@
@charset 'UTF-8';
@media screen and (max-width: 768px) {
+ .table-responsive {
+ border: none;
+ }
+
.multi-select__container {
.btn {
display: block;
@@ -253,6 +257,7 @@
}
}
}
+
blockquote {
margin-top: 0;
}
@@ -274,6 +279,7 @@
.post__header {
margin-bottom: 0;
+ padding-right: 70px;
.col__reply {
top: -3px;
@@ -1342,7 +1348,7 @@
a {
border-bottom: 1px solid;
- line-height: 50px;
+ line-height: 45px;
position: relative;
text-align: center;
}
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/tests/formatting_imgs.test.jsx b/webapp/tests/formatting_imgs.test.jsx
new file mode 100644
index 000000000..fac9a755f
--- /dev/null
+++ b/webapp/tests/formatting_imgs.test.jsx
@@ -0,0 +1,55 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import assert from 'assert';
+
+import * as Markdown from 'utils/markdown.jsx';
+
+describe('Markdown.Imgs', function() {
+ this.timeout(10000);
+
+ it('Inline mage', function(done) {
+ assert.equal(
+ Markdown.format('![Mattermost](/images/icon.png)').trim(),
+ '<p><img src="/images/icon.png" alt="Mattermost" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img" crossorigin="anonymous"></p>'
+ );
+
+ done();
+ });
+
+ it('Image with hover text', function(done) {
+ assert.equal(
+ Markdown.format('![Mattermost](/images/icon.png "Mattermost Icon")').trim(),
+ '<p><img src="/images/icon.png" alt="Mattermost" title="Mattermost Icon" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img" crossorigin="anonymous"></p>'
+ );
+
+ done();
+ });
+
+ it('Image with link', function(done) {
+ assert.equal(
+ Markdown.format('[![Mattermost](../../images/icon-76x76.png)](https://github.com/mattermost/platform)').trim(),
+ '<p><a class="theme markdown__link" href="https://github.com/mattermost/platform" rel="noreferrer" target="_blank"><img src="../../images/icon-76x76.png" alt="Mattermost" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img" crossorigin="anonymous"></a></p>'
+ );
+
+ done();
+ });
+
+ it('Image with width and height', function(done) {
+ assert.equal(
+ Markdown.format('![Mattermost](../../images/icon-76x76.png =50x76 "Mattermost Icon")').trim(),
+ '<p><img src="../../images/icon-76x76.png" alt="Mattermost" title="Mattermost Icon" width="50" height="76" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img" crossorigin="anonymous"></p>'
+ );
+
+ done();
+ });
+
+ it('Image with width', function(done) {
+ assert.equal(
+ Markdown.format('![Mattermost](../../images/icon-76x76.png =50 "Mattermost Icon")').trim(),
+ '<p><img src="../../images/icon-76x76.png" alt="Mattermost" title="Mattermost Icon" width="50" onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img" crossorigin="anonymous"></p>'
+ );
+
+ done();
+ });
+});
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 9ba853238..1fc19b5f2 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -137,7 +137,7 @@ export function getMyChannelMembers() {
(err) => {
callTracker.getMyChannelMembers = 0;
dispatchError(err, 'getMyChannelMembers');
- reject();
+ reject(new Error('Unable to getMyChannelMembers'));
}
);
});
@@ -166,7 +166,7 @@ export function getMyChannelMembersForTeam(teamId) {
(err) => {
callTracker[`getMyChannelMembers${teamId}`] = 0;
dispatchError(err, 'getMyChannelMembersForTeam');
- reject();
+ reject(new Error('Unable to getMyChannelMembersForTeam'));
}
);
});
@@ -308,7 +308,7 @@ export function getChannelMember(channelId, userId) {
(err) => {
callTracker[`getChannelMember${channelId}${userId}`] = 0;
dispatchError(err, 'getChannelMember');
- reject();
+ reject(new Error('Unable to getChannelMeber'));
}
);
});
@@ -1612,6 +1612,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 541fb48ec..d8fc169a3 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',
@@ -865,6 +868,8 @@ export const Constants = {
DEFAULT_MAX_NOTIFICATIONS_PER_CHANNEL: 1000,
MAX_TEAMNAME_LENGTH: 15,
MAX_TEAMDESCRIPTION_LENGTH: 50,
+ MIN_CHANNELNAME_LENGTH: 2,
+ MAX_CHANNELNAME_LENGTH: 22,
MIN_USERNAME_LENGTH: 3,
MAX_USERNAME_LENGTH: 22,
MAX_NICKNAME_LENGTH: 22,
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
index c84df0fa5..fa9c985c7 100644
--- a/webapp/utils/markdown.jsx
+++ b/webapp/utils/markdown.jsx
@@ -152,6 +152,7 @@ class MattermostMarkdownRenderer extends marked.Renderer {
out += ' height="' + dimensions[1] + '"';
}
out += ' onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"';
+ out += ' crossorigin="anonymous"';
out += this.options.xhtml ? '/>' : '>';
return out;
}
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index c860987af..b3370e88c 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -32,7 +32,7 @@ export function isMac() {
}
export function cmdOrCtrlPressed(e) {
- return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey);
+ return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey && !e.altKey);
}
export function isInRole(roles, inRole) {
@@ -179,7 +179,7 @@ export function displayTime(ticks, utc) {
ampm = ' PM';
}
- hours = hours % 12;
+ hours %= 12;
if (!hours) {
hours = '12';
}
@@ -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) {
@@ -1216,7 +1217,7 @@ export function isValidPassword(password) {
error = true;
}
- errorId = errorId + 'Lowercase';
+ errorId += 'Lowercase';
}
if (global.window.mm_config.PasswordRequireUppercase === 'true') {
@@ -1224,7 +1225,7 @@ export function isValidPassword(password) {
error = true;
}
- errorId = errorId + 'Uppercase';
+ errorId += 'Uppercase';
}
if (global.window.mm_config.PasswordRequireNumber === 'true') {
@@ -1232,7 +1233,7 @@ export function isValidPassword(password) {
error = true;
}
- errorId = errorId + 'Number';
+ errorId += 'Number';
}
if (global.window.mm_config.PasswordRequireSymbol === 'true') {
@@ -1240,7 +1241,7 @@ export function isValidPassword(password) {
error = true;
}
- errorId = errorId + 'Symbol';
+ errorId += 'Symbol';
}
minimumLength = global.window.mm_config.PasswordMinimumLength;
diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js
index f1742e3ae..32c5a322a 100644
--- a/webapp/webpack.config.js
+++ b/webapp/webpack.config.js
@@ -65,7 +65,16 @@ var config = {
},
{
test: /\.scss$/,
- loaders: ['style-loader', 'css-loader', 'sass-loader']
+ use: [{
+ loader: 'style-loader'
+ }, {
+ loader: 'css-loader'
+ }, {
+ loader: 'sass-loader',
+ options: {
+ includePaths: ['node_modules/compass-mixins/lib']
+ }
+ }]
},
{
test: /\.css$/,
@@ -92,13 +101,6 @@ var config = {
minimize: !DEV,
debug: false
}),
- new webpack.LoaderOptionsPlugin({
- options: {
- sassLoader: {
- includePaths: ['node_modules/compass-mixins/lib']
- }
- }
- }),
new webpack.optimize.CommonsChunkPlugin({
minChunks: 2,
children: true