summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/about_build_modal.jsx7
-rw-r--r--webapp/components/admin_console/admin_settings.jsx5
-rw-r--r--webapp/components/admin_console/custom_brand_settings.jsx2
-rw-r--r--webapp/components/admin_console/external_service_settings.jsx2
-rw-r--r--webapp/components/admin_console/team_users.jsx2
-rw-r--r--webapp/components/admin_console/text_setting.jsx6
-rw-r--r--webapp/components/admin_console/user_item.jsx7
-rw-r--r--webapp/components/admin_console/webhook_settings.jsx6
-rw-r--r--webapp/components/channel_info_modal.jsx46
-rw-r--r--webapp/components/channel_switch_modal.jsx8
-rw-r--r--webapp/components/create_comment.jsx69
-rw-r--r--webapp/components/create_post.jsx58
-rw-r--r--webapp/components/edit_post_modal.jsx125
-rw-r--r--webapp/components/emoji/components/add_emoji.jsx5
-rw-r--r--webapp/components/file_upload.jsx3
-rw-r--r--webapp/components/form_error.jsx11
-rw-r--r--webapp/components/integrations/components/add_command.jsx7
-rw-r--r--webapp/components/integrations/components/add_incoming_webhook.jsx5
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook.jsx5
-rw-r--r--webapp/components/invite_member_modal.jsx10
-rw-r--r--webapp/components/logged_in.jsx12
-rw-r--r--webapp/components/msg_typing.jsx2
-rw-r--r--webapp/components/navbar.jsx2
-rw-r--r--webapp/components/needs_team.jsx37
-rw-r--r--webapp/components/new_channel_flow.jsx6
-rw-r--r--webapp/components/new_channel_modal.jsx44
-rw-r--r--webapp/components/post_view/components/post.jsx9
-rw-r--r--webapp/components/post_view/components/post_list.jsx35
-rw-r--r--webapp/components/post_view/post_view_controller.jsx14
-rw-r--r--webapp/components/removed_from_channel_modal.jsx2
-rw-r--r--webapp/components/rename_channel_modal.jsx2
-rw-r--r--webapp/components/search_bar.jsx51
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx52
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx2
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx8
-rw-r--r--webapp/components/team_export_tab.jsx127
-rw-r--r--webapp/components/team_general_tab.jsx2
-rw-r--r--webapp/components/team_members_dropdown.jsx2
-rw-r--r--webapp/components/team_settings.jsx8
-rw-r--r--webapp/components/team_settings_modal.jsx7
-rw-r--r--webapp/components/textbox.jsx14
-rw-r--r--webapp/components/user_settings/import_theme_modal.jsx91
-rw-r--r--webapp/components/user_settings/premade_theme_chooser.jsx16
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx13
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx142
46 files changed, 569 insertions, 524 deletions
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index 197179191..1f41d76f9 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -104,6 +104,11 @@ export default class AboutBuildModal extends React.Component {
}
}
+ let version = '\u00a0' + config.Version;
+ if (config.BuildNumber !== config.Version) {
+ version += '\u00a0 (' + config.BuildNumber + ')';
+ }
+
return (
<Modal
dialogClassName='about-modal'
@@ -135,7 +140,7 @@ export default class AboutBuildModal extends React.Component {
id='about.version'
defaultMessage='Version:'
/>
- {'\u00a0' + config.Version + '\u00a0 (' + config.BuildNumber + ')'}
+ {version}
</div>
<div>
<FormattedMessage
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
index e11d843a7..e29be33d1 100644
--- a/webapp/components/admin_console/admin_settings.jsx
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -106,11 +106,6 @@ export default class AdminSettings extends React.Component {
}
render() {
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass += 'btn-primary';
- }
-
return (
<div className='wrapper--fixed'>
{this.renderTitle()}
diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx
index 193889ea9..b4026c4a9 100644
--- a/webapp/components/admin_console/custom_brand_settings.jsx
+++ b/webapp/components/admin_console/custom_brand_settings.jsx
@@ -11,6 +11,7 @@ import BrandImageSetting from './brand_image_setting.jsx';
import {FormattedMessage} from 'react-intl';
import SettingsGroup from './settings_group.jsx';
import TextSetting from './text_setting.jsx';
+import Constants from 'utils/constants.jsx';
export default class CustomBrandSettings extends AdminSettings {
constructor(props) {
@@ -115,6 +116,7 @@ export default class CustomBrandSettings extends AdminSettings {
defaultMessage='Site Name:'
/>
}
+ maxLength={Constants.MAX_SITENAME_LENGTH}
placeholder={Utils.localizeMessage('admin.team.siteNameExample', 'Ex "Mattermost"')}
helpText={
<FormattedMessage
diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx
index ebeb78332..59a129fc0 100644
--- a/webapp/components/admin_console/external_service_settings.jsx
+++ b/webapp/components/admin_console/external_service_settings.jsx
@@ -77,7 +77,7 @@ export default class ExternalServiceSettings extends AdminSettings {
helpText={
<FormattedHTMLMessage
id='admin.service.googleDescription'
- defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
+ defaultMessage='Set this key to enable the display of titles for embedded YouTube video previews. Without the key, YouTube previews will still be created based on hyperlinks appearing in messages or comments but they will not show the video title. View a <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">Google Developers Tutorial</a> for instructions on how to obtain a key.'
/>
}
value={this.state.googleDeveloperKey}
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 3ec375627..f82c20a86 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -186,7 +186,7 @@ export default class UserList extends React.Component {
var memberList = this.state.users.map((user) => {
var teamMember = this.getTeamMemberForUser(user.id);
- if (teamMember.delete_at > 0) {
+ if (!teamMember || teamMember.delete_at > 0) {
return null;
}
diff --git a/webapp/components/admin_console/text_setting.jsx b/webapp/components/admin_console/text_setting.jsx
index bb37f8e29..a5844aca7 100644
--- a/webapp/components/admin_console/text_setting.jsx
+++ b/webapp/components/admin_console/text_setting.jsx
@@ -4,6 +4,7 @@
import React from 'react';
import Setting from './setting.jsx';
+import Constants from 'utils/constants.jsx';
export default class TextSetting extends React.Component {
static get propTypes() {
@@ -16,6 +17,7 @@ export default class TextSetting extends React.Component {
React.PropTypes.string,
React.PropTypes.number
]).isRequired,
+ maxLength: React.PropTypes.number,
onChange: React.PropTypes.func.isRequired,
disabled: React.PropTypes.bool,
type: React.PropTypes.oneOf([
@@ -27,7 +29,8 @@ export default class TextSetting extends React.Component {
static get defaultProps() {
return {
- type: 'input'
+ type: 'input',
+ maxLength: Constants.MAX_TEXTSETTING_LENGTH
};
}
@@ -51,6 +54,7 @@ export default class TextSetting extends React.Component {
type='text'
placeholder={this.props.placeholder}
value={this.props.value}
+ maxLength={this.props.maxLength}
onChange={this.handleChange}
disabled={this.props.disabled}
/>
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index e6c4f637c..79dbd5639 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -505,6 +505,11 @@ export default class UserItem extends React.Component {
);
}
+ let displayedName = Utils.getDisplayName(user);
+ if (displayedName !== user.username) {
+ displayedName += ' (@' + user.username + ')';
+ }
+
return (
<div className='more-modal__row'>
<img
@@ -514,7 +519,7 @@ export default class UserItem extends React.Component {
width='36'
/>
<div className='more-modal__details'>
- <div className='more-modal__name'>{Utils.getDisplayName(user)}</div>
+ <div className='more-modal__name'>{displayedName}</div>
<div className='more-modal__description'>
<FormattedHTMLMessage
id='admin.user_item.emailTitle'
diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/webhook_settings.jsx
index 18a3ed7ad..3c8ea5466 100644
--- a/webapp/components/admin_console/webhook_settings.jsx
+++ b/webapp/components/admin_console/webhook_settings.jsx
@@ -64,7 +64,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.webhooksDescription'
- defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
+ defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag. See <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableIncomingWebhooks}
@@ -81,7 +81,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.outWebhooksDesc'
- defaultMessage='When true, outgoing webhooks will be allowed.'
+ defaultMessage='When true, outgoing webhooks will be allowed. See <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableOutgoingWebhooks}
@@ -98,7 +98,7 @@ export default class WebhookSettings extends AdminSettings {
helpText={
<FormattedMessage
id='admin.service.cmdsDesc'
- defaultMessage='When true, user created slash commands will be allowed.'
+ defaultMessage='When true, custom slash commands will be allowed. See <a href="http://docs.mattermost.com/developer/slash-commands.html" target="_blank">documentation</a> to learn more.'
/>
}
value={this.state.enableCommands}
diff --git a/webapp/components/channel_info_modal.jsx b/webapp/components/channel_info_modal.jsx
index 7bd004411..b0e2c63fa 100644
--- a/webapp/components/channel_info_modal.jsx
+++ b/webapp/components/channel_info_modal.jsx
@@ -5,6 +5,7 @@ import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
+import * as TextFormatting from 'utils/text_formatting.jsx';
import React from 'react';
@@ -32,6 +33,7 @@ export default class ChannelInfoModal extends React.Component {
display_name: notFound,
name: notFound,
purpose: notFound,
+ header: notFound,
id: notFound
};
}
@@ -44,6 +46,39 @@ export default class ChannelInfoModal extends React.Component {
const channelURL = Utils.getTeamURLFromAddressBar() + '/channels/' + channel.name;
+ let channelPurpose = null;
+ if (channel.purpose) {
+ channelPurpose = (
+ <div className='form-group'>
+ <div className='info__label'>
+ <FormattedMessage
+ id='channel_info.purpose'
+ defaultMessage='Purpose:'
+ />
+ </div>
+ <div className='info__value'>{channel.purpose}</div>
+ </div>
+ );
+ }
+
+ let channelHeader = null;
+ if (channel.header) {
+ channelHeader = (
+ <div className='form-group'>
+ <div className='info__label'>
+ <FormattedMessage
+ id='channel_info.header'
+ defaultMessage='Header:'
+ />
+ </div>
+ <div
+ className='info__value'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: false, mentionHighlight: false})}}
+ />
+ </div>
+ );
+ }
+
return (
<Modal
dialogClassName='about-modal'
@@ -60,15 +95,8 @@ export default class ChannelInfoModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body ref='modalBody'>
- <div className='form-group'>
- <div className='info__label'>
- <FormattedMessage
- id='channel_info.purpose'
- defaultMessage='Purpose:'
- />
- </div>
- <div className='info__value'>{channel.purpose}</div>
- </div>
+ {channelPurpose}
+ {channelHeader}
<div className='form-group'>
<div className='info__label'>
<FormattedMessage
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
index 9bb98d74d..18e0f9f59 100644
--- a/webapp/components/channel_switch_modal.jsx
+++ b/webapp/components/channel_switch_modal.jsx
@@ -21,7 +21,7 @@ export default class SwitchChannelModal extends React.Component {
constructor() {
super();
- this.onUserInput = this.onUserInput.bind(this);
+ this.onInput = this.onInput.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -57,8 +57,8 @@ export default class SwitchChannelModal extends React.Component {
this.props.onHide();
}
- onUserInput(message) {
- this.setState({text: message});
+ onInput(e) {
+ this.setState({text: e.target.value});
}
handleKeyDown(e) {
@@ -122,7 +122,7 @@ export default class SwitchChannelModal extends React.Component {
ref='search'
className='form-control focused'
type='input'
- onUserInput={this.onUserInput}
+ onInput={this.onInput}
value={this.state.text}
onKeyDown={this.handleKeyDown}
listComponent={SuggestionList}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index f7564f396..bf23f7b44 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -19,33 +19,14 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- commentLength: {
- id: 'create_comment.commentLength',
- defaultMessage: 'Comment length must be less than {max} characters.'
- },
- comment: {
- id: 'create_comment.comment',
- defaultMessage: 'Add Comment'
- },
- addComment: {
- id: 'create_comment.addComment',
- defaultMessage: 'Add a comment...'
- },
- commentTitle: {
- id: 'create_comment.commentTitle',
- defaultMessage: 'Comment'
- }
-});
-
import React from 'react';
-class CreateComment extends React.Component {
+export default class CreateComment extends React.Component {
constructor(props) {
super(props);
@@ -53,7 +34,7 @@ class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
@@ -76,8 +57,7 @@ class CreateComment extends React.Component {
previews: draft.previews,
submitting: false,
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
- showPostDeletedModal: false,
- typing: false
+ showPostDeletedModal: false
};
}
@@ -126,7 +106,15 @@ class CreateComment extends React.Component {
}
if (post.message.length > Constants.CHARACTER_LIMIT) {
- this.setState({postError: this.props.intl.formatMessage(holders.commentLength, {max: Constants.CHARACTER_LIMIT})});
+ this.setState({
+ postError: (
+ <FormattedMessage
+ id='create_comment.commentLength'
+ defaultMessage='Comment length must be less than {max} characters.'
+ values={{max: Constants.CHARACTER_LIMIT}}
+ />
+ )
+ });
return;
}
@@ -175,13 +163,12 @@ class CreateComment extends React.Component {
submitting: false,
postError: null,
previews: [],
- serverError: null,
- typing: false
+ serverError: null
});
}
commentMsgKeyPress(e) {
- if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
+ if ((this.state.ctrlSend && e.ctrlKey) || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
@@ -192,15 +179,16 @@ class CreateComment extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
- handleUserInput(messageText) {
+ handleInput(e) {
+ const messageText = e.target.value;
+
const draft = PostStore.getCommentDraft(this.props.rootId);
draft.message = messageText;
PostStore.storeCommentDraft(this.props.rootId, draft);
$('.post-right__scroll').parent().scrollTop($('.post-right__scroll')[0].scrollHeight);
- const typing = messageText !== '';
- this.setState({messageText, typing});
+ this.setState({messageText});
}
handleKeyDown(e) {
@@ -220,7 +208,7 @@ class CreateComment extends React.Component {
AppDispatcher.handleViewAction({
type: ActionTypes.RECEIVED_EDIT_POST,
refocusId: '#reply_textbox',
- title: this.props.intl.formatMessage(holders.commentTitle),
+ title: Utils.localizeMessage('create_comment.commentTitle', 'Comment'),
message: lastPost.message,
postId: lastPost.id,
channelId: lastPost.channel_id,
@@ -313,13 +301,13 @@ class CreateComment extends React.Component {
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCommentDraft(this.props.rootId, draft);
- this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({previews, uploadsInProgress});
}
componentWillReceiveProps(newProps) {
if (newProps.rootId !== this.props.rootId) {
const draft = PostStore.getCommentDraft(newProps.rootId);
- this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, typing: false});
+ this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
}
@@ -395,7 +383,6 @@ class CreateComment extends React.Component {
);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='post-create'>
@@ -405,12 +392,11 @@ class CreateComment extends React.Component {
>
<div className='post-body__cell'>
<Textbox
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
onKeyPress={this.commentMsgKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
- typing={this.state.typing}
- createMessage={formatMessage(holders.addComment)}
+ createMessage={Utils.localizeMessage('create_comment.addComment', 'Add a comment...')}
initialText=''
supportsCommands={false}
id='reply_textbox'
@@ -436,7 +422,7 @@ class CreateComment extends React.Component {
<input
type='button'
className='btn btn-primary comment-btn pull-right'
- value={formatMessage(holders.comment)}
+ value={Utils.localizeMessage('create_comment.comment', 'Add Comment')}
onClick={this.handleSubmit}
/>
{uploadsInProgressText}
@@ -455,9 +441,6 @@ class CreateComment extends React.Component {
}
CreateComment.propTypes = {
- intl: intlShape.isRequired,
channelId: React.PropTypes.string.isRequired,
rootId: React.PropTypes.string.isRequired
-};
-
-export default injectIntl(CreateComment);
+}; \ No newline at end of file
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 508fb36cb..9b61cca24 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -23,7 +23,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedHTMLMessage} from 'react-intl';
+import {FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const Preferences = Constants.Preferences;
@@ -31,24 +31,9 @@ const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- comment: {
- id: 'create_post.comment',
- defaultMessage: 'Comment'
- },
- post: {
- id: 'create_post.post',
- defaultMessage: 'Post'
- },
- write: {
- id: 'create_post.write',
- defaultMessage: 'Write a message...'
- }
-});
-
import React from 'react';
-class CreatePost extends React.Component {
+export default class CreatePost extends React.Component {
constructor(props) {
super(props);
@@ -57,7 +42,7 @@ class CreatePost extends React.Component {
this.getCurrentDraft = this.getCurrentDraft.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
@@ -87,8 +72,7 @@ class CreatePost extends React.Component {
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
showTutorialTip: false,
- showPostDeletedModal: false,
- typing: false
+ showPostDeletedModal: false
};
}
@@ -133,7 +117,7 @@ class CreatePost extends React.Component {
MessageHistoryStore.storeMessageInHistory(this.state.messageText);
- this.setState({submitting: true, serverError: null, typing: false});
+ this.setState({submitting: true, serverError: null});
if (post.message.indexOf('/') === 0) {
ChannelActions.executeCommand(
@@ -212,7 +196,7 @@ class CreatePost extends React.Component {
}
postMsgKeyPress(e) {
- if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
+ if ((this.state.ctrlSend && e.ctrlKey) || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
@@ -223,9 +207,9 @@ class CreatePost extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.state.channelId, '');
}
- handleUserInput(messageText) {
- const typing = messageText !== '';
- this.setState({messageText, typing});
+ handleInput(e) {
+ const messageText = e.target.value;
+ this.setState({messageText});
const draft = PostStore.getCurrentDraft();
draft.message = messageText;
@@ -372,7 +356,7 @@ class CreatePost extends React.Component {
if (this.state.channelId !== channelId) {
const draft = this.getCurrentDraft();
- this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, typing: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
+ this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
@@ -408,8 +392,13 @@ class CreatePost extends React.Component {
if (!lastPost) {
return;
}
- const {formatMessage} = this.props.intl;
- var type = (lastPost.root_id && lastPost.root_id.length > 0) ? formatMessage(holders.comment) : formatMessage(holders.post);
+
+ let type;
+ if (lastPost.root_id && lastPost.root_id.length > 0) {
+ type = Utils.localizeMessage('create_post.comment', 'Comment');
+ } else {
+ type = Utils.localizeMessage('create_post.post', 'Post');
+ }
AppDispatcher.handleViewAction({
type: ActionTypes.RECEIVED_EDIT_POST,
@@ -519,12 +508,11 @@ class CreatePost extends React.Component {
<div className='post-create-body'>
<div className='post-body__cell'>
<Textbox
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
onKeyPress={this.postMsgKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
- typing={this.state.typing}
- createMessage={this.props.intl.formatMessage(holders.write)}
+ createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')}
channelId={this.state.channelId}
id='post_textbox'
ref='textbox'
@@ -565,10 +553,4 @@ class CreatePost extends React.Component {
</form>
);
}
-}
-
-CreatePost.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(CreatePost);
+} \ No newline at end of file
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index 4bd23a26d..8be0ba243 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -11,35 +11,35 @@ import BrowserStore from 'stores/browser_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
var KeyCodes = Constants.KeyCodes;
-const holders = defineMessages({
- editPost: {
- id: 'edit_post.editPost',
- defaultMessage: 'Edit the post...'
- }
-});
-
import React from 'react';
-class EditPostModal extends React.Component {
+export default class EditPostModal extends React.Component {
constructor(props) {
super(props);
this.handleEdit = this.handleEdit.bind(this);
- this.handleEditInput = this.handleEditInput.bind(this);
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onModalShow = this.onModalShow.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHide = this.onModalHide.bind(this);
+ this.onModalKeyDown = this.onModalKeyDown.bind(this);
- this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: '', typing: false};
+ this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
+
handleEdit() {
var updatedPost = {};
updatedPost.message = this.state.editText.trim();
@@ -77,10 +77,13 @@ class EditPostModal extends React.Component {
$('#edit_post').modal('hide');
}
- handleEditInput(editMessage) {
- const typing = editMessage !== '';
- this.setState({editText: editMessage, typing});
+
+ handleInput(e) {
+ this.setState({
+ editText: e.target.value
+ });
}
+
handleEditKeyPress(e) {
if (!this.state.ctrlSend && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
@@ -92,6 +95,7 @@ class EditPostModal extends React.Component {
this.handleSubmit(e);
}
}
+
handleEditPostEvent(options) {
this.setState({
editText: options.message || '',
@@ -100,65 +104,83 @@ class EditPostModal extends React.Component {
post_id: options.postId || '',
channel_id: options.channelId || '',
comments: options.comments || 0,
- refocusId: options.refocusId || '',
- typing: false
+ refocusId: options.refocusId || ''
});
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
+
handleKeyDown(e) {
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
this.handleEdit(e);
}
}
+
onPreferenceChange() {
this.setState({
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter')
});
}
- componentDidMount() {
- var self = this;
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => {
- self.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false});
- });
+ onModalHidden() {
+ this.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false});
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
- var button = e.relatedTarget;
- if (!button) {
- return;
- }
- self.setState({
- editText: $(button).attr('data-message'),
- originalText: $(button).attr('data-message'),
- title: $(button).attr('data-title'),
- channel_id: $(button).attr('data-channelid'),
- post_id: $(button).attr('data-postid'),
- comments: $(button).attr('data-comments'),
- refocusId: $(button).attr('data-refocusid'),
- typing: false
- });
+ onModalShow(e) {
+ var button = e.relatedTarget;
+ if (!button) {
+ return;
+ }
+ this.setState({
+ editText: $(button).attr('data-message'),
+ originalText: $(button).attr('data-message'),
+ title: $(button).attr('data-title'),
+ channel_id: $(button).attr('data-channelid'),
+ post_id: $(button).attr('data-postid'),
+ comments: $(button).attr('data-comments'),
+ refocusId: $(button).attr('data-refocusid'),
+ typing: false
});
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => {
- self.refs.editbox.focus();
- });
+ onModalShown() {
+ this.refs.editbox.focus();
+ }
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', () => {
- if (self.state.refocusId !== '') {
- setTimeout(() => {
- $(self.state.refocusId).get(0).focus();
- });
- }
- });
+ onModalHide() {
+ if (this.state.refocusId !== '') {
+ setTimeout(() => {
+ $(this.state.refocusId).get(0).focus();
+ });
+ }
+ }
+ onModalKeyDown(e) {
+ if (e.which === Constants.KeyCodes.ESCAPE) {
+ e.stopPropagation();
+ }
+ }
+
+ componentDidMount() {
+ $(this.refs.modal).on('hidden.bs.modal', this.onModalHidden);
+ $(this.refs.modal).on('show.bs.modal', this.onModalShow);
+ $(this.refs.modal).on('shown.bs.modal', this.onModalShown);
+ $(this.refs.modal).on('hide.bs.modal', this.onModalHide);
+ $(this.refs.modal).on('keydown', this.onModalKeyDown);
PostStore.addEditPostListener(this.handleEditPostEvent);
PreferenceStore.addChangeListener(this.onPreferenceChange);
}
+
componentWillUnmount() {
+ $(this.refs.modal).off('hidden.bs.modal', this.onModalHidden);
+ $(this.refs.modal).off('show.bs.modal', this.onModalShow);
+ $(this.refs.modal).off('shown.bs.modal', this.onModalShown);
+ $(this.refs.modal).off('hide.bs.modal', this.onModalHide);
+ $(this.refs.modal).off('keydown', this.onModalKeyDown);
PostStore.removeEditPostListner(this.handleEditPostEvent);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
+
render() {
var error = (<div className='form-group'><br/></div>);
if (this.state.error) {
@@ -198,12 +220,11 @@ class EditPostModal extends React.Component {
</div>
<div className='edit-modal-body modal-body'>
<Textbox
- onUserInput={this.handleEditInput}
+ onInput={this.handleInput}
onKeyPress={this.handleEditKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
- typing={this.state.typing}
- createMessage={this.props.intl.formatMessage(holders.editPost)}
+ createMessage={Utils.localizeMessage('edit_post.editPost', 'Edit the post...')}
supportsCommands={false}
id='edit_textbox'
ref='editbox'
@@ -238,9 +259,3 @@ class EditPostModal extends React.Component {
);
}
}
-
-EditPostModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(EditPostModal);
diff --git a/webapp/components/emoji/components/add_emoji.jsx b/webapp/components/emoji/components/add_emoji.jsx
index 46f345476..c3d61d32c 100644
--- a/webapp/components/emoji/components/add_emoji.jsx
+++ b/webapp/components/emoji/components/add_emoji.jsx
@@ -277,7 +277,10 @@ export default class AddEmoji extends React.Component {
</div>
{preview}
<div className='backstage-form__footer'>
- <FormError error={this.state.error}/>
+ <FormError
+ type='backstage'
+ error={this.state.error}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/emoji'}
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 2f485d4d3..1a3c6eadc 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -282,7 +282,8 @@ class FileUpload extends React.Component {
keyUpload(e) {
if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.U) {
e.preventDefault();
- if (this.props.postType === 'post' && document.activeElement.id === 'post_textbox' || this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox') {
+ if ((this.props.postType === 'post' && document.activeElement.id === 'post_textbox') ||
+ (this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox')) {
$(this.refs.fileInput).focus().trigger('click');
}
}
diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx
index 047595ef2..df6fa3ab0 100644
--- a/webapp/components/form_error.jsx
+++ b/webapp/components/form_error.jsx
@@ -7,6 +7,7 @@ export default class FormError extends React.Component {
static get propTypes() {
// accepts either a single error or an array of errors
return {
+ type: React.PropTypes.node,
error: React.PropTypes.node,
margin: React.PropTypes.node,
errors: React.PropTypes.arrayOf(React.PropTypes.node)
@@ -40,6 +41,16 @@ export default class FormError extends React.Component {
return null;
}
+ if (this.props.type === 'backstage') {
+ return (
+ <div className='pull-left has-error'>
+ <label className='control-label'>
+ {message}
+ </label>
+ </div>
+ );
+ }
+
if (this.props.margin) {
return (
<div className='form-group has-error'>
diff --git a/webapp/components/integrations/components/add_command.jsx b/webapp/components/integrations/components/add_command.jsx
index e72670e47..d24acd70d 100644
--- a/webapp/components/integrations/components/add_command.jsx
+++ b/webapp/components/integrations/components/add_command.jsx
@@ -72,7 +72,7 @@ export default class AddCommand extends React.Component {
const command = {
display_name: this.state.displayName,
description: this.state.description,
- trigger: this.state.trigger.trim(),
+ trigger: this.state.trigger.trim().toLowerCase(),
url: this.state.url.trim(),
method: this.state.method,
username: this.state.username,
@@ -537,7 +537,10 @@ export default class AddCommand extends React.Component {
</div>
{autocompleteFields}
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/commands'}
diff --git a/webapp/components/integrations/components/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx
index 122600c90..a213a805f 100644
--- a/webapp/components/integrations/components/add_incoming_webhook.jsx
+++ b/webapp/components/integrations/components/add_incoming_webhook.jsx
@@ -186,7 +186,10 @@ export default class AddIncomingWebhook extends React.Component {
</div>
</div>
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}
diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx
index bd49fedc9..d6c0242a5 100644
--- a/webapp/components/integrations/components/add_outgoing_webhook.jsx
+++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx
@@ -319,7 +319,10 @@ export default class AddOutgoingWebhook extends React.Component {
</div>
</div>
<div className='backstage-form__footer'>
- <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index 265a421b6..68a7b7b15 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -131,7 +131,7 @@ class InviteMemberModal extends React.Component {
invites.push(invite);
}
- this.setState({emailErrors: emailErrors, firstNameErrors: firstNameErrors, lastNameErrors: lastNameErrors});
+ this.setState({emailErrors, firstNameErrors, lastNameErrors});
if (!valid || invites.length === 0) {
return;
@@ -151,7 +151,7 @@ class InviteMemberModal extends React.Component {
(err) => {
if (err.id === 'api.team.invite_members.already.app_error') {
emailErrors[err.detailed_error] = err.message;
- this.setState({emailErrors: emailErrors});
+ this.setState({emailErrors});
} else {
this.setState({serverError: err.message});
}
@@ -193,7 +193,7 @@ class InviteMemberModal extends React.Component {
var count = this.state.idCount + 1;
var inviteIds = this.state.inviteIds;
inviteIds.push(count);
- this.setState({inviteIds: inviteIds, idCount: count});
+ this.setState({inviteIds, idCount: count});
}
clearFields() {
@@ -225,7 +225,7 @@ class InviteMemberModal extends React.Component {
if (!inviteIds.length) {
inviteIds.push(++count);
}
- this.setState({inviteIds: inviteIds, idCount: count});
+ this.setState({inviteIds, idCount: count});
}
showGetTeamInviteLinkModal() {
@@ -435,7 +435,7 @@ class InviteMemberModal extends React.Component {
id='invite_member.teamInviteLink'
defaultMessage='You can also invite people using the {link}.'
values={{
- link: (link)
+ link
}}
/>
</p>
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 484164e56..2ac858dfb 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -9,6 +9,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as Websockets from 'actions/websocket_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
@@ -71,6 +72,8 @@ export default class LoggedIn extends React.Component {
if (this.state.user) {
this.setupUser(this.state.user);
+ } else {
+ GlobalActions.emitUserLoggedOutEvent('/login');
}
}
@@ -89,15 +92,6 @@ export default class LoggedIn extends React.Component {
id: user.id
});
}
-
- // Update CSS classes to match user theme
- if (user) {
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- Utils.applyTheme(user.theme_props);
- } else {
- Utils.applyTheme(Constants.THEMES.default);
- }
- }
}
onUserChanged() {
diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx
index 631eea78d..f6a6d12b2 100644
--- a/webapp/components/msg_typing.jsx
+++ b/webapp/components/msg_typing.jsx
@@ -71,7 +71,7 @@ class MsgTyping extends React.Component {
defaultMessage='{users} and {last} are typing...'
values={{
users: (users.join(', ')),
- last: (last)
+ last
}}
/>
);
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 44730f4e2..c2d262819 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -636,7 +636,7 @@ export default class Navbar extends React.Component {
defaultMessage='No channel header yet.{newline}{link} to add one.'
values={{
newline: (<br/>),
- link: (link)
+ link
}}
/>
</div>
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 07b90636d..a8c7b3508 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -41,19 +41,34 @@ export default class NeedsTeam extends React.Component {
constructor(params) {
super(params);
- this.onChanged = this.onChanged.bind(this);
+ this.onTeamChanged = this.onTeamChanged.bind(this);
+ this.onPreferencesChanged = this.onPreferencesChanged.bind(this);
+
+ const team = TeamStore.getCurrent();
this.state = {
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
};
}
- onChanged() {
+ onTeamChanged() {
+ const team = TeamStore.getCurrent();
+
this.setState({
- team: TeamStore.getCurrent()
+ team,
+ theme: PreferenceStore.getTheme(team.id)
});
}
+ onPreferencesChanged(category) {
+ if (!category || category === Preferences.CATEGORY_THEME) {
+ this.setState({
+ theme: PreferenceStore.getTheme(this.state.team.id)
+ });
+ }
+ }
+
componentWillMount() {
// Go to tutorial if we are first arriving
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
@@ -63,7 +78,8 @@ export default class NeedsTeam extends React.Component {
}
componentDidMount() {
- TeamStore.addChangeListener(this.onChanged);
+ TeamStore.addChangeListener(this.onTeamChanged);
+ PreferenceStore.addChangeListener(this.onPreferencesChanged);
// Emit view action
GlobalActions.viewLoggedIn();
@@ -80,10 +96,19 @@ export default class NeedsTeam extends React.Component {
$(window).on('blur', () => {
window.isActive = false;
});
+
+ Utils.applyTheme(this.state.theme);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (!Utils.areObjectsEqual(prevState.theme, this.state.theme)) {
+ Utils.applyTheme(this.state.theme);
+ }
}
componentWillUnmount() {
- TeamStore.removeChangeListener(this.onChanged);
+ TeamStore.removeChangeListener(this.onTeamChanged);
+ PreferenceStore.removeChangeListener(this.onPreferencesChanged);
$(window).off('focus');
$(window).off('blur');
}
diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx
index db06cf0be..f6e91afc4 100644
--- a/webapp/components/new_channel_flow.jsx
+++ b/webapp/components/new_channel_flow.jsx
@@ -65,6 +65,7 @@ class NewChannelFlow extends React.Component {
channelDisplayName: '',
channelName: '',
channelPurpose: '',
+ channelHeader: '',
nameModified: false
};
}
@@ -78,6 +79,7 @@ class NewChannelFlow extends React.Component {
channelDisplayName: '',
channelName: '',
channelPurpose: '',
+ channelHeader: '',
nameModified: false
});
}
@@ -99,6 +101,7 @@ class NewChannelFlow extends React.Component {
name: this.state.channelName,
display_name: this.state.channelDisplayName,
purpose: this.state.channelPurpose,
+ header: this.state.channelHeader,
type: this.state.channelType
};
Client.createChannel(
@@ -153,7 +156,8 @@ class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
- channelPurpose: data.purpose
+ channelPurpose: data.purpose,
+ channelHeader: data.header
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index 23eee625d..1198335ca 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -89,8 +89,9 @@ class NewChannelModal extends React.Component {
handleChange() {
const newData = {
- displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
- purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
+ displayName: this.refs.display_name.value,
+ header: this.refs.channel_header.value,
+ purpose: this.refs.channel_purpose.value
};
this.props.onDataChanged(newData);
}
@@ -258,7 +259,7 @@ class NewChannelModal extends React.Component {
</p>
</div>
</div>
- <div className='form-group less'>
+ <div className='form-group'>
<div className='col-sm-3'>
<label className='form__label control-label'>
<FormattedMessage
@@ -293,6 +294,43 @@ class NewChannelModal extends React.Component {
}}
/>
</p>
+ </div>
+ </div>
+ <div className='form-group less'>
+ <div className='col-sm-3'>
+ <label className='form__label control-label'>
+ <FormattedMessage
+ id='channel_modal.header'
+ defaultMessage='Header'
+ />
+ </label>
+ <label className='form__label light'>
+ <FormattedMessage
+ id='channel_modal.optional'
+ defaultMessage='(optional)'
+ />
+ </label>
+ </div>
+ <div className='col-sm-9'>
+ <textarea
+ className='form-control no-resize'
+ ref='channel_header'
+ rows='4'
+ placeholder={this.props.intl.formatMessage({id: 'channel_modal.header'})}
+ maxLength='128'
+ value={this.props.channelData.header}
+ onChange={this.handleChange}
+ tabIndex='2'
+ />
+ <p className='input__help'>
+ <FormattedMessage
+ id='channel_modal.headerHelp'
+ defaultMessage='Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).'
+ values={{
+ term: (channelTerm)
+ }}
+ />
+ </p>
{serverError}
</div>
</div>
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index 21d335a51..ff443e355 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import * as PostActions from 'actions/post_actions.jsx';
import React from 'react';
@@ -20,6 +21,7 @@ export default class Post extends React.Component {
this.handleCommentClick = this.handleCommentClick.bind(this);
this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
+ this.handlePostClick = this.handlePostClick.bind(this);
this.state = {
dropdownOpened: false
@@ -47,6 +49,12 @@ export default class Post extends React.Component {
this.refs.info.forceUpdate();
this.refs.header.forceUpdate();
}
+ handlePostClick(e) {
+ if (e.altKey) {
+ e.preventDefault();
+ PostActions.setUnreadPost(this.props.post.channel_id, this.props.post.id);
+ }
+ }
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
@@ -213,6 +221,7 @@ export default class Post extends React.Component {
<div
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass + ' ' + hideControls + ' ' + dropdownOpenedClass}
+ onClick={this.handlePostClick}
>
<div className={'post__content ' + centerClass}>
{profilePicContainer}
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index 17e29da2e..70107c838 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -15,14 +15,20 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
+import * as ChannelActions from 'actions/channel_actions.jsx';
+
import Constants from 'utils/constants.jsx';
const ScrollTypes = Constants.ScrollTypes;
+import PreferenceStore from 'stores/preference_store.jsx';
+
import {FormattedDate, FormattedMessage} from 'react-intl';
import React from 'react';
import ReactDOM from 'react-dom';
+const Preferences = Constants.Preferences;
+
export default class PostList extends React.Component {
constructor(props) {
super(props);
@@ -37,6 +43,7 @@ export default class PostList extends React.Component {
this.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -44,16 +51,24 @@ export default class PostList extends React.Component {
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
+ this.state = {
+ isScrolling: false,
+ fullWidthIntro: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
+ topPostId: null
+ };
+
if (props.channel) {
- this.introText = createChannelIntroMessage(props.channel);
+ this.introText = createChannelIntroMessage(props.channel, this.state.fullWidthIntro);
} else {
this.introText = this.getArchivesIntroMessage();
}
+ }
- this.state = {
- isScrolling: false,
- topPostId: null
- };
+ handleKeyDown(e) {
+ if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) {
+ e.preventDefault();
+ ChannelActions.setChannelAsRead();
+ }
}
isAtBottom() {
@@ -292,7 +307,7 @@ export default class PostList extends React.Component {
);
}
- if (postUserId !== userId &&
+ if ((postUserId !== userId || this.props.ownNewMessage) &&
this.props.lastViewed !== 0 &&
post.create_at > this.props.lastViewed &&
!renderedLastViewed) {
@@ -395,7 +410,7 @@ export default class PostList extends React.Component {
getArchivesIntroMessage() {
return (
- <div className='channel-intro'>
+ <div className={'channel-intro'}>
<h4 className='channel-intro__title'>
<FormattedMessage
id='post_focus_view.beginning'
@@ -412,10 +427,12 @@ export default class PostList extends React.Component {
}
window.addEventListener('resize', this.handleResize);
+ window.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ window.removeEventListener('keydown', this.handleKeyDown);
this.scrollStopAction.cancel();
}
@@ -510,7 +527,8 @@ export default class PostList extends React.Component {
}
PostList.defaultProps = {
- lastViewed: 0
+ lastViewed: 0,
+ ownNewMessage: false
};
PostList.propTypes = {
@@ -524,6 +542,7 @@ PostList.propTypes = {
showMoreMessagesTop: React.PropTypes.bool,
showMoreMessagesBottom: React.PropTypes.bool,
lastViewed: React.PropTypes.number,
+ ownNewMessage: React.PropTypes.bool,
postsToHighlight: React.PropTypes.object,
displayNameType: React.PropTypes.string,
displayPostsInCenter: React.PropTypes.bool,
diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
index e5743e657..a7583fa38 100644
--- a/webapp/components/post_view/post_view_controller.jsx
+++ b/webapp/components/post_view/post_view_controller.jsx
@@ -27,6 +27,7 @@ export default class PostViewController extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this);
this.onEmojisChange = this.onEmojisChange.bind(this);
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
+ this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this);
this.onPostListScroll = this.onPostListScroll.bind(this);
this.onActivate = this.onActivate.bind(this);
this.onDeactivate = this.onDeactivate.bind(this);
@@ -50,6 +51,7 @@ export default class PostViewController extends React.Component {
profiles,
atTop: PostStore.getVisibilityAtTop(channel.id),
lastViewed,
+ ownNewMessage: false,
scrollType: ScrollTypes.NEW_MESSAGE,
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
@@ -117,6 +119,7 @@ export default class PostViewController extends React.Component {
PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.addChangeListener(this.onEmojisChange);
+ ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator);
}
onDeactivate() {
@@ -125,6 +128,7 @@ export default class PostViewController extends React.Component {
PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
EmojiStore.removeChangeListener(this.onEmojisChange);
+ ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator);
}
componentWillReceiveProps(nextProps) {
@@ -149,6 +153,7 @@ export default class PostViewController extends React.Component {
this.setState({
channel,
lastViewed,
+ ownNewMessage: false,
profiles: JSON.parse(JSON.stringify(profiles)),
postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))),
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
@@ -178,6 +183,10 @@ export default class PostViewController extends React.Component {
}
}
+ onSetNewMessageIndicator(lastViewed, ownNewMessage) {
+ this.setState({lastViewed, ownNewMessage});
+ }
+
onPostListScroll(atBottom) {
if (atBottom) {
this.setState({scrollType: ScrollTypes.BOTTOM});
@@ -219,6 +228,10 @@ export default class PostViewController extends React.Component {
return true;
}
+ if (nextState.ownNewMessage !== this.state.ownNewMessage) {
+ return true;
+ }
+
if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) {
return true;
}
@@ -281,6 +294,7 @@ export default class PostViewController extends React.Component {
useMilitaryTime={this.state.useMilitaryTime}
lastViewed={this.state.lastViewed}
emojis={this.state.emojis}
+ ownNewMessage={this.state.ownNewMessage}
/>
);
}
diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx
index 3164e4e3f..3bdceadf7 100644
--- a/webapp/components/removed_from_channel_modal.jsx
+++ b/webapp/components/removed_from_channel_modal.jsx
@@ -116,7 +116,7 @@ export default class RemovedFromChannelModal extends React.Component {
id='removed_channel.remover'
defaultMessage='{remover} removed you from {channel}'
values={{
- remover: (remover),
+ remover,
channel: (channelName)
}}
/>
diff --git a/webapp/components/rename_channel_modal.jsx b/webapp/components/rename_channel_modal.jsx
index df08bdbc6..4dc84d971 100644
--- a/webapp/components/rename_channel_modal.jsx
+++ b/webapp/components/rename_channel_modal.jsx
@@ -203,7 +203,7 @@ export default class RenameChannelModal extends React.Component {
const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
const channelName = Utils.cleanUpUrlable(displayName);
ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
- this.setState({channelName: channelName});
+ this.setState({channelName});
}
}
diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx
index d8725a7aa..290572612 100644
--- a/webapp/components/search_bar.jsx
+++ b/webapp/components/search_bar.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import client from 'utils/web_client.jsx';
+import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import SearchStore from 'stores/search_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -11,30 +11,23 @@ import SuggestionBox from './suggestion/suggestion_box.jsx';
import SearchChannelProvider from './suggestion/search_channel_provider.jsx';
import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
import SearchUserProvider from './suggestion/search_user_provider.jsx';
-import * as utils from 'utils/utils.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
var ActionTypes = Constants.ActionTypes;
import {Popover} from 'react-bootstrap';
-const holders = defineMessages({
- search: {
- id: 'search_bar.search',
- defaultMessage: 'Search'
- }
-});
-
import React from 'react';
-class SearchBar extends React.Component {
+export default class SearchBar extends React.Component {
constructor() {
super();
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
@@ -46,24 +39,28 @@ class SearchBar extends React.Component {
this.suggestionProviders = [new SearchChannelProvider(), new SearchUserProvider()];
}
+
getSearchTermStateFromStores() {
var term = SearchStore.getSearchTerm() || '';
return {
searchTerm: term
};
}
+
componentDidMount() {
SearchStore.addSearchTermChangeListener(this.onListenerChange);
this.mounted = true;
}
+
componentWillUnmount() {
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
this.mounted = false;
}
+
onListenerChange(doSearch, isMentionSearch) {
if (this.mounted) {
var newState = this.getSearchTermStateFromStores();
- if (!utils.areObjectsEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
if (doSearch) {
@@ -71,9 +68,11 @@ class SearchBar extends React.Component {
}
}
}
+
clearFocus() {
$('.search-bar__container').removeClass('focused');
}
+
handleClose(e) {
e.preventDefault();
@@ -94,30 +93,34 @@ class SearchBar extends React.Component {
postId: null
});
}
- handleUserInput(text) {
- var term = text;
+
+ handleInput(e) {
+ var term = e.target.value;
SearchStore.storeSearchTerm(term);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
}
+
handleUserBlur() {
this.setState({focused: false});
}
+
handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
}
+
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
- client.search(
+ Client.search(
terms,
isMentionSearch,
(data) => {
this.setState({isSearching: false});
- if (utils.isMobile()) {
+ if (Utils.isMobile()) {
ReactDOM.findDOMNode(this.refs.search).value = '';
}
@@ -134,6 +137,7 @@ class SearchBar extends React.Component {
);
}
}
+
handleSubmit(e) {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
@@ -178,11 +182,11 @@ class SearchBar extends React.Component {
<SuggestionBox
ref='search'
className='form-control search-bar'
- placeholder={this.props.intl.formatMessage(holders.search)}
+ placeholder={Utils.localizeMessage('search_bar.search', 'Search')}
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
- onUserInput={this.handleUserInput}
+ onInput={this.handleInput}
listComponent={SearchSuggestionList}
providers={this.suggestionProviders}
type='search'
@@ -202,11 +206,4 @@ class SearchBar extends React.Component {
</div>
);
}
-}
-
-SearchBar.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(SearchBar);
-
+} \ No newline at end of file
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index ec496a765..ad765a7d6 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {
</li>
<li className='setting-list-item'>
<hr/>
+ {this.props.submitExtra}
{serverError}
{clientError}
{submit}
@@ -113,5 +114,6 @@ SettingItemMax.propTypes = {
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
title: React.PropTypes.node,
- width: React.PropTypes.string
+ width: React.PropTypes.string,
+ submitExtra: React.PropTypes.node
};
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index 2184b9fab..d4b150787 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -21,8 +21,8 @@ export default class SuggestionBox extends React.Component {
this.handleDocumentClick = this.handleDocumentClick.bind(this);
- this.handleChange = this.handleChange.bind(this);
this.handleCompleteWord = this.handleCompleteWord.bind(this);
+ this.handleInput = this.handleInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handlePretextChanged = this.handlePretextChanged.bind(this);
@@ -70,27 +70,24 @@ export default class SuggestionBox extends React.Component {
}
}
- handleChange(e) {
+ handleInput(e) {
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
const caret = Utils.getCaretPosition(textbox);
const pretext = textbox.value.substring(0, caret);
GlobalActions.emitSuggestionPretextChanged(this.suggestionId, pretext);
- if (this.props.onUserInput) {
- this.props.onUserInput(textbox.value);
- }
-
- if (this.props.onChange) {
- this.props.onChange(e);
+ if (this.props.onInput) {
+ this.props.onInput(e);
}
}
handleCompleteWord(term, matchedPretext) {
- const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const textbox = this.refs.textbox;
const caret = Utils.getCaretPosition(textbox);
const text = textbox.value;
const pretext = text.substring(0, caret);
+
let prefix;
if (pretext.endsWith(matchedPretext)) {
prefix = pretext.substring(0, pretext.length - matchedPretext.length);
@@ -104,10 +101,17 @@ export default class SuggestionBox extends React.Component {
const suffix = text.substring(caret);
- if (this.props.onUserInput) {
- this.props.onUserInput(prefix + term + ' ' + suffix);
+ this.refs.textbox.value = prefix + term + ' ' + suffix;
+
+ if (this.props.onInput) {
+ // fake an input event to send back to parent components
+ const e = {
+ target: this.refs.textbox
+ };
+
+ // don't call handleInput or we'll get into an event loop
+ this.props.onInput(e);
}
- this.refs.textbox.value = (prefix + term + ' ' + suffix);
// set the caret position after the next rendering
window.requestAnimationFrame(() => {
@@ -128,6 +132,7 @@ export default class SuggestionBox extends React.Component {
e.preventDefault();
} else if (e.which === KeyCodes.ESCAPE) {
GlobalActions.emitClearSuggestions(this.suggestionId);
+ e.stopPropagation();
} else if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
@@ -143,18 +148,15 @@ export default class SuggestionBox extends React.Component {
}
render() {
- const newProps = Object.assign({}, this.props, {
- onChange: this.handleChange,
- onKeyDown: this.handleKeyDown
- });
-
let textbox = null;
if (this.props.type === 'input') {
textbox = (
<input
ref='textbox'
type='text'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
} else if (this.props.type === 'search') {
@@ -162,7 +164,9 @@ export default class SuggestionBox extends React.Component {
<input
ref='textbox'
type='search'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
} else if (this.props.type === 'textarea') {
@@ -170,7 +174,9 @@ export default class SuggestionBox extends React.Component {
<TextareaAutosize
id={this.suggestionId}
ref='textbox'
- {...newProps}
+ {...this.props}
+ onInput={this.handleInput}
+ onKeyDown={this.handleKeyDown}
/>
);
}
@@ -212,12 +218,10 @@ SuggestionBox.propTypes = {
listComponent: React.PropTypes.func.isRequired,
type: React.PropTypes.oneOf(['input', 'textarea', 'search']).isRequired,
value: React.PropTypes.string.isRequired,
- onUserInput: React.PropTypes.func,
providers: React.PropTypes.arrayOf(React.PropTypes.object),
listStyle: React.PropTypes.string,
// explicitly name any input event handlers we override and need to manually call
- onChange: React.PropTypes.func,
- onKeyDown: React.PropTypes.func,
- onHeightChange: React.PropTypes.func
+ onInput: React.PropTypes.func,
+ onKeyDown: React.PropTypes.func
};
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index f1cccf8aa..52b85b2f5 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -87,7 +87,7 @@ export default class SuggestionList extends React.Component {
content.scrollTop(itemTop - contentTopPadding);
} else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
// the item has gone off the bottom of the visible space
- content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ content.scrollTop((itemBottom - visibleContentHeight) + contentTopPadding + contentBottomPadding);
}
}
}
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index c12918c51..e092d9b5c 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -57,7 +57,13 @@ export default class SwitchChannelProvider {
}
}
- channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
+ channels.sort((a, b) => {
+ if (a.display_name === b.display_name) {
+ return a.name.localeCompare(b.name);
+ }
+ return a.display_name.localeCompare(b.display_name);
+ });
+
const channelNames = channels.map((channel) => channel.name);
SuggestionStore.addSuggestions(suggestionId, channelNames, channels, SwitchChannelSuggestion, channelPrefix);
diff --git a/webapp/components/team_export_tab.jsx b/webapp/components/team_export_tab.jsx
deleted file mode 100644
index 15c131489..000000000
--- a/webapp/components/team_export_tab.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Client from 'utils/web_client.jsx';
-
-import {FormattedMessage} from 'react-intl';
-
-import React from 'react';
-import {Link} from 'react-router/es6';
-
-export default class TeamExportTab extends React.Component {
- constructor(props) {
- super(props);
- this.state = {status: 'request', link: '', err: ''};
-
- this.onExportSuccess = this.onExportSuccess.bind(this);
- this.onExportFailure = this.onExportFailure.bind(this);
- this.doExport = this.doExport.bind(this);
- }
- onExportSuccess(data) {
- this.setState({status: 'ready', link: data.link, err: ''});
- }
- onExportFailure(e) {
- this.setState({status: 'failure', link: '', err: e.message});
- }
- doExport() {
- if (this.state.status === 'in-progress') {
- return;
- }
- this.setState({status: 'in-progress'});
- Client.exportTeam(this.onExportSuccess, this.onExportFailure);
- }
- render() {
- var messageSection = '';
- switch (this.state.status) {
- case 'request':
- messageSection = '';
- break;
- case 'in-progress':
- messageSection = (
- <p className='confirm-import alert alert-warning'>
- <i className='fa fa-spinner fa-pulse'/>
- <FormattedMessage
- id='team_export_tab.exporting'
- defaultMessage=' Exporting...'
- />
- </p>
- );
- break;
- case 'ready':
- messageSection = (
- <p className='confirm-import alert alert-success'>
- <i className='fa fa-check'/>
- <FormattedMessage
- id='team_export_tab.ready'
- defaultMessage=' Ready for '
- />
- <Link
- to={this.state.link}
- download={true}
- >
- <FormattedMessage
- id='team_export_tab.download'
- defaultMessage='download'
- />
- </Link>
- </p>
- );
- break;
- case 'failure':
- messageSection = (
- <p className='confirm-import alert alert-warning'>
- <i className='fa fa-warning'/>
- <FormattedMessage
- id='team_export_tab.unable'
- defaultMessage=' Unable to export: {error}'
- values={{
- error: this.state.err
- }}
- />
- </p>
- );
- break;
- }
-
- return (
- <div
- ref='wrapper'
- className='user-settings'
- >
- <h3 className='tab-header'>
- <FormattedMessage
- id='team_export_tab.export'
- defaultMessage='Export'
- />
- </h3>
- <div className='divider-dark first'/>
- <ul className='section-max'>
- <li className='col-xs-12 section-title'>
- <FormattedMessage
- id='team_export_tab.exportTeam'
- defaultMessage='Export your team'
- />
- </li>
- <li className='col-xs-offset-3 col-xs-8'>
- <ul className='setting-list'>
- <li className='setting-list-item'>
- <a
- className='btn btn-sm btn-primary btn-file sel-btn'
- href='#'
- onClick={this.doExport}
- >
- <FormattedMessage
- id='team_export_tab.export'
- defaultMessage='Export'
- />
- </a>
- </li>
- </ul>
- </li>
- </ul>
- <div className='divider-dark'/>
- {messageSection}
- </div>
- );
- }
-}
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 2814119c6..ac50c69a0 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -255,12 +255,10 @@ class GeneralTab extends React.Component {
}
updateName(e) {
- e.preventDefault();
this.setState({name: e.target.value});
}
updateInviteId(e) {
- e.preventDefault();
this.setState({invite_id: e.target.value});
}
diff --git a/webapp/components/team_members_dropdown.jsx b/webapp/components/team_members_dropdown.jsx
index 43449635d..f8c217aed 100644
--- a/webapp/components/team_members_dropdown.jsx
+++ b/webapp/components/team_members_dropdown.jsx
@@ -186,7 +186,7 @@ export default class TeamMembersDropdown extends React.Component {
}
const me = UserStore.getCurrentUser();
- let showMakeMember = teamMember.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeMember = teamMember.roles === 'admin' && user.roles !== 'system_admin';
let showMakeAdmin = teamMember.roles === '' && user.roles !== 'system_admin';
let showMakeActive = false;
let showMakeNotActive = user.roles !== 'system_admin';
diff --git a/webapp/components/team_settings.jsx b/webapp/components/team_settings.jsx
index 210d1f541..0725f9fe5 100644
--- a/webapp/components/team_settings.jsx
+++ b/webapp/components/team_settings.jsx
@@ -3,7 +3,6 @@
import TeamStore from 'stores/team_store.jsx';
import ImportTab from './team_import_tab.jsx';
-import ExportTab from './team_export_tab.jsx';
import GeneralTab from './team_general_tab.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -58,13 +57,6 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'export':
- result = (
- <div>
- <ExportTab/>
- </div>
- );
- break;
default:
result = (
<div/>
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 8ac924cf8..aa7b0831e 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -17,10 +17,6 @@ const holders = defineMessages({
importTab: {
id: 'team_settings_modal.importTab',
defaultMessage: 'Import'
- },
- exportTab: {
- id: 'team_settings_modal.exportTab',
- defaultMessage: 'Export'
}
});
@@ -71,9 +67,6 @@ class TeamSettingsModal extends React.Component {
tabs.push({name: 'general', uiName: formatMessage(holders.generalTab), icon: 'icon fa fa-cog'});
tabs.push({name: 'import', uiName: formatMessage(holders.importTab), icon: 'icon fa fa-upload'});
- // To enable export uncomment this line
- //tabs.push({name: 'export', uiName: formatMessage(holders.exportTab), icon: 'fa fa-download'});
-
return (
<div
className='modal fade'
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 40e6aec4a..24f58f43e 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -176,11 +176,6 @@ export default class Textbox extends React.Component {
</div>
);
- const otherProps = {};
- if (!this.props.typing) {
- otherProps.value = this.props.messageText;
- }
-
return (
<div
ref='wrapper'
@@ -194,7 +189,7 @@ export default class Textbox extends React.Component {
spellCheck='true'
maxLength={Constants.MAX_POST_LEN}
placeholder={this.props.createMessage}
- onUserInput={this.props.onUserInput}
+ onInput={this.props.onInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onHeightChange={this.handleHeightChange}
@@ -202,7 +197,7 @@ export default class Textbox extends React.Component {
listComponent={SuggestionList}
providers={this.suggestionProviders}
channelId={this.props.channelId}
- {...otherProps}
+ value={this.props.messageText}
/>
<div
ref='preview'
@@ -239,10 +234,9 @@ Textbox.propTypes = {
id: React.PropTypes.string.isRequired,
channelId: React.PropTypes.string,
messageText: React.PropTypes.string.isRequired,
- onUserInput: React.PropTypes.func.isRequired,
+ onInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
createMessage: React.PropTypes.string.isRequired,
onKeyDown: React.PropTypes.func,
- supportsCommands: React.PropTypes.bool.isRequired,
- typing: React.PropTypes.bool.isRequired
+ supportsCommands: React.PropTypes.bool.isRequired
};
diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx
index 552659c4c..32c6837e8 100644
--- a/webapp/components/user_settings/import_theme_modal.jsx
+++ b/webapp/components/user_settings/import_theme_modal.jsx
@@ -1,30 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
import ModalStore from 'stores/modal_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
import {Modal} from 'react-bootstrap';
-import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- submitError: {
- id: 'user.settings.import_theme.submitError',
- defaultMessage: 'Invalid format, please try copying and pasting in again.'
- }
-});
+import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
import React from 'react';
-class ImportThemeModal extends React.Component {
+export default class ImportThemeModal extends React.Component {
constructor(props) {
super(props);
@@ -33,26 +21,42 @@ class ImportThemeModal extends React.Component {
this.handleChange = this.handleChange.bind(this);
this.state = {
+ value: '',
inputError: '',
- show: false
+ show: false,
+ callback: null
};
}
+
componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
+
componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow);
}
- updateShow(show) {
- this.setState({show});
+
+ updateShow(show, args) {
+ this.setState({
+ show,
+ callback: args.callback
+ });
}
+
handleSubmit(e) {
e.preventDefault();
- const text = ReactDOM.findDOMNode(this.refs.input).value;
+ const text = this.state.value;
if (!this.isInputValid(text)) {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
return;
}
@@ -81,26 +85,13 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github';
- const user = UserStore.getCurrentUser();
- user.theme_props = theme;
-
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
-
- this.setState({show: false});
- Utils.applyTheme(theme);
- },
- (err) => {
- var state = this.getStateFromStores();
- state.serverError = err;
- this.setState(state);
- }
- );
+ this.state.callback(theme);
+ this.setState({
+ show: false,
+ callback: null
+ });
}
+
isInputValid(text) {
if (text.length === 0) {
return false;
@@ -134,13 +125,25 @@ class ImportThemeModal extends React.Component {
return true;
}
+
handleChange(e) {
- if (this.isInputValid(e.target.value)) {
+ const value = e.target.value;
+ this.setState({value});
+
+ if (this.isInputValid(value)) {
this.setState({inputError: null});
} else {
- this.setState({inputError: this.props.intl.formatMessage(holders.submitError)});
+ this.setState({
+ inputError: (
+ <FormattedMessage
+ id='user.settings.import_theme.submitError'
+ defaultMessage='Invalid format, please try copying and pasting in again.'
+ />
+ )
+ });
}
}
+
render() {
return (
<span>
@@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
<div className='form-group less'>
<div className='col-sm-9'>
<input
- ref='input'
type='text'
className='form-control'
+ value={this.state.value}
onChange={this.handleChange}
/>
<div className='input__help'>
@@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
);
}
}
-
-ImportThemeModal.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ImportThemeModal);
diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx
index 9552c686d..03ea56449 100644
--- a/webapp/components/user_settings/premade_theme_chooser.jsx
+++ b/webapp/components/user_settings/premade_theme_chooser.jsx
@@ -7,8 +7,6 @@ import Constants from 'utils/constants.jsx';
import React from 'react';
-import {FormattedMessage} from 'react-intl';
-
export default class PremadeThemeChooser extends React.Component {
constructor(props) {
super(props);
@@ -54,20 +52,6 @@ export default class PremadeThemeChooser extends React.Component {
<div className='clearfix'>
{premadeThemes}
</div>
- <div className='clearfix'>
- <div className='col-sm-12 padding-bottom x2'>
- <a
- href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
- target='_blank'
- rel='noopener noreferrer'
- >
- <FormattedMessage
- id='user.settings.display.theme.otherThemes'
- defaultMessage='See other themes'
- />
- </a>
- </div>
- </div>
</div>
);
}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index a449c7d01..9b0e6a204 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -295,8 +295,17 @@ class UserSettingsGeneralTab extends React.Component {
setupInitialState(props) {
const user = props.user;
- return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
- email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false};
+ return {
+ username: user.username,
+ firstName: user.first_name,
+ lastName: user.last_name,
+ nickname: user.nickname,
+ email: user.email,
+ confirmEmail: '',
+ picture: null,
+ loadingPicture: false,
+ emailChangeInProgress: false
+ };
}
createEmailSection() {
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index 94516ec8c..d12a7689a 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -8,28 +8,18 @@ import PremadeThemeChooser from './premade_theme_chooser.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
-import Client from 'utils/web_client.jsx';
-import * as Utils from 'utils/utils.jsx';
-
-import Constants from 'utils/constants.jsx';
+import * as UserActions from 'actions/user_actions.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-const ActionTypes = Constants.ActionTypes;
+import {FormattedMessage} from 'react-intl';
-const holders = defineMessages({
- themeTitle: {
- id: 'user.settings.display.theme.title',
- defaultMessage: 'Theme'
- },
- themeDescribe: {
- id: 'user.settings.display.theme.describe',
- defaultMessage: 'Open to manage your theme'
- }
-});
+import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx';
import React from 'react';
@@ -47,6 +37,7 @@ export default class ThemeSetting extends React.Component {
this.originalTheme = Object.assign({}, this.state.theme);
}
+
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -54,17 +45,20 @@ export default class ThemeSetting extends React.Component {
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentDidUpdate() {
if (this.props.selected) {
$('.color-btn').removeClass('active-border');
$(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
+
componentWillReceiveProps(nextProps) {
if (this.props.selected && !nextProps.selected) {
this.resetFields();
}
}
+
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -73,27 +67,35 @@ export default class ThemeSetting extends React.Component {
Utils.applyTheme(state.theme);
}
}
+
getStateFromStores() {
- const user = UserStore.getCurrentUser();
- let theme = null;
+ const teamId = TeamStore.getCurrentId();
- if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
- theme = Object.assign({}, user.theme_props);
- } else {
- theme = $.extend(true, {}, Constants.THEMES.default);
+ const theme = PreferenceStore.getTheme(teamId);
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
- let type = 'premade';
- if (theme.type === 'custom') {
- type = 'custom';
- }
+ let showAllTeamsCheckbox = false;
+ let applyToAllTeams = true;
- if (!theme.codeTheme) {
- theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true') {
+ // show the "apply to all teams" checkbox if the user is on more than one team
+ showAllTeamsCheckbox = Object.keys(TeamStore.getAll()).length > 1;
+
+ // check the "apply to all teams" checkbox by default if the user has any team-specific themes
+ applyToAllTeams = PreferenceStore.getCategory(Preferences.CATEGORY_THEME).size <= 1;
}
- return {theme, type};
+ return {
+ teamId: TeamStore.getCurrentId(),
+ theme,
+ type: theme.type || 'premade',
+ showAllTeamsCheckbox,
+ applyToAllTeams
+ };
}
+
onChange() {
const newState = this.getStateFromStores();
@@ -103,21 +105,20 @@ export default class ThemeSetting extends React.Component {
this.props.setEnforceFocus(true);
}
+
scrollToTop() {
$('.ps-container.modal-body').scrollTop(0);
}
+
submitTheme(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- user.theme_props = this.state.theme;
- Client.updateUser(user, Constants.UserUpdateEvents.THEME,
- (data) => {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_ME,
- me: data
- });
+ const teamId = this.state.applyToAllTeams ? '' : this.state.teamId;
+ UserActions.saveTheme(
+ teamId,
+ this.state.theme,
+ () => {
this.props.setRequireConfirm(false);
this.originalTheme = Object.assign({}, this.state.theme);
this.scrollToTop();
@@ -130,6 +131,7 @@ export default class ThemeSetting extends React.Component {
}
);
}
+
updateTheme(theme) {
let themeChanged = this.state.theme.length === theme.length;
if (!themeChanged) {
@@ -148,9 +150,11 @@ export default class ThemeSetting extends React.Component {
this.setState({theme});
Utils.applyTheme(theme);
}
+
updateType(type) {
this.setState({type});
}
+
resetFields() {
const state = this.getStateFromStores();
state.serverError = null;
@@ -161,17 +165,18 @@ export default class ThemeSetting extends React.Component {
this.props.setRequireConfirm(false);
}
+
handleImportModal() {
AppDispatcher.handleViewAction({
type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
- value: true
+ value: true,
+ callback: this.updateTheme
});
this.props.setEnforceFocus(false);
}
- render() {
- const {formatMessage} = this.props.intl;
+ render() {
var serverError;
if (this.state.serverError) {
serverError = this.state.serverError;
@@ -252,9 +257,27 @@ export default class ThemeSetting extends React.Component {
inputs.push(custom);
inputs.push(
- <div key='importSlackThemeButton'>
+ <div>
<br/>
<a
+ href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ <FormattedMessage
+ id='user.settings.display.theme.otherThemes'
+ defaultMessage='See other themes'
+ />
+ </a>
+ </div>
+ );
+
+ inputs.push(
+ <div
+ key='importSlackThemeButton'
+ className='padding-top'
+ >
+ <a
className='theme'
onClick={this.handleImportModal}
>
@@ -266,9 +289,29 @@ export default class ThemeSetting extends React.Component {
</div>
);
+ let allTeamsCheckbox = null;
+ if (this.state.showAllTeamsCheckbox) {
+ allTeamsCheckbox = (
+ <div className='checkbox user-settings__submit-checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.applyToAllTeams}
+ onChange={(e) => this.setState({applyToAllTeams: e.target.checked})}
+ />
+ <FormattedMessage
+ id='user.settings.display.theme.applyToAllTeams'
+ defaultMessage='Apply New Theme to All Teams'
+ />
+ </label>
+ </div>
+ );
+ }
+
themeUI = (
<SettingItemMax
inputs={inputs}
+ submitExtra={allTeamsCheckbox}
submit={this.submitTheme}
server_error={serverError}
width='full'
@@ -281,8 +324,18 @@ export default class ThemeSetting extends React.Component {
} else {
themeUI = (
<SettingItemMin
- title={formatMessage(holders.themeTitle)}
- describe={formatMessage(holders.themeDescribe)}
+ title={
+ <FormattedMessage
+ id='user.settings.display.theme.title'
+ defaultMessage='Theme'
+ />
+ }
+ describe={
+ <FormattedMessage
+ id='user.settings.display.theme.describe'
+ defaultMessage='Open to manage your theme'
+ />
+ }
updateSection={() => {
this.props.updateSection('theme');
}}
@@ -295,11 +348,8 @@ export default class ThemeSetting extends React.Component {
}
ThemeSetting.propTypes = {
- intl: intlShape.isRequired,
selected: React.PropTypes.bool.isRequired,
updateSection: React.PropTypes.func.isRequired,
setRequireConfirm: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired
};
-
-export default injectIntl(ThemeSetting);