summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/admin_console/email_settings.jsx4
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx67
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx34
-rw-r--r--web/react/components/admin_console/service_settings.jsx70
-rw-r--r--web/react/components/admin_console/sql_settings.jsx6
-rw-r--r--web/react/components/channel_notifications.jsx289
-rw-r--r--web/react/components/command_list.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar_dropdown.jsx14
-rw-r--r--web/react/components/notify_counts.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx26
-rw-r--r--web/react/components/post.jsx9
-rw-r--r--web/react/components/post_body.jsx122
-rw-r--r--web/react/components/post_header.jsx20
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx29
-rw-r--r--web/react/components/rename_channel_modal.jsx1
-rw-r--r--web/react/components/search_results_item.jsx7
-rw-r--r--web/react/components/setting_upload.jsx45
-rw-r--r--web/react/components/sidebar.jsx35
-rw-r--r--web/react/components/sidebar_header.jsx3
-rw-r--r--web/react/components/sidebar_right.jsx25
-rw-r--r--web/react/components/sidebar_right_menu.jsx15
-rw-r--r--web/react/components/team_signup_url_page.jsx4
-rw-r--r--web/react/components/team_signup_with_sso.jsx10
-rw-r--r--web/react/components/user_profile.jsx16
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx2
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx18
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx4
-rw-r--r--web/react/pages/authorize.jsx16
-rw-r--r--web/react/pages/channel.jsx4
-rw-r--r--web/react/utils/async_client.jsx16
-rw-r--r--web/react/utils/client.jsx20
-rw-r--r--web/react/utils/constants.jsx3
-rw-r--r--web/react/utils/text_formatting.jsx6
-rw-r--r--web/react/utils/utils.jsx229
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss1
-rw-r--r--web/sass-files/sass/partials/_base.scss28
-rw-r--r--web/sass-files/sass/partials/_command-box.scss17
-rw-r--r--web/sass-files/sass/partials/_headers.scss18
-rw-r--r--web/sass-files/sass/partials/_mentions.scss8
-rw-r--r--web/sass-files/sass/partials/_modal.scss17
-rw-r--r--web/sass-files/sass/partials/_oauth.scss31
-rw-r--r--web/sass-files/sass/partials/_post.scss8
-rw-r--r--web/sass-files/sass/partials/_responsive.scss6
-rw-r--r--web/sass-files/sass/partials/_settings.scss11
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss13
-rw-r--r--web/sass-files/sass/partials/_sidebar--menu.scss2
-rw-r--r--web/sass-files/sass/styles.scss3
-rw-r--r--web/static/images/themes/mattermost dark.pngbin75371 -> 104882 bytes
-rw-r--r--web/static/images/themes/mattermost.pngbin66828 -> 93862 bytes
-rw-r--r--web/static/images/themes/organization.pngbin86044 -> 127558 bytes
-rw-r--r--web/static/images/themes/slack.pngbin68603 -> 0 bytes
-rw-r--r--web/static/images/themes/windows dark.pngbin82784 -> 122145 bytes
-rw-r--r--web/templates/authorize.html40
-rw-r--r--web/templates/head.html19
-rw-r--r--web/web.go88
59 files changed, 929 insertions, 562 deletions
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 3b5ad2a1a..762a4ab26 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -288,7 +288,7 @@ export default class EmailSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='feedbackName'
>
- {'Feedback Name:'}
+ {'Notification Display Name:'}
</label>
<div className='col-sm-8'>
<input
@@ -310,7 +310,7 @@ export default class EmailSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='feedbackEmail'
>
- {'Feedback Email:'}
+ {'Notification Email Address:'}
</label>
<div className='col-sm-8'>
<input
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index eaea15b8f..759892ad3 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -40,7 +40,6 @@ export default class GitLabSettings extends React.Component {
config.GitLabSettings.Enable = React.findDOMNode(this.refs.Enable).checked;
config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
- config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
@@ -121,28 +120,6 @@ export default class GitLabSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='Secret'
- >
- {'Secret:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Secret'
- ref='Secret'
- placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- defaultValue={this.props.config.GitLabSettings.Secret}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
htmlFor='Id'
>
{'Id:'}
@@ -165,22 +142,22 @@ export default class GitLabSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='Scope'
+ htmlFor='Secret'
>
- {'Scope:'}
+ {'Secret:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='Scope'
- ref='Scope'
- placeholder='Not currently used by GitLab. Please leave blank'
- defaultValue={this.props.config.GitLabSettings.Scope}
+ id='Secret'
+ ref='Secret'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Secret}
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
</div>
</div>
@@ -202,7 +179,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize).'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -224,7 +201,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -246,7 +223,7 @@ export default class GitLabSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.Enable}
/>
- <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user.'}</p>
+ <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p>
</div>
</div>
@@ -272,6 +249,30 @@ export default class GitLabSettings extends React.Component {
}
}
+
+//config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+// <div className='form-group'>
+// <label
+// className='control-label col-sm-4'
+// htmlFor='Scope'
+// >
+// {'Scope:'}
+// </label>
+// <div className='col-sm-8'>
+// <input
+// type='text'
+// className='form-control'
+// id='Scope'
+// ref='Scope'
+// placeholder='Not currently used by GitLab. Please leave blank'
+// defaultValue={this.props.config.GitLabSettings.Scope}
+// onChange={this.handleChange}
+// disabled={!this.state.Allow}
+// />
+// <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+// </div>
+// </div>
+
GitLabSettings.propTypes = {
config: React.PropTypes.object
};
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index affd8ae11..c74d321e6 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -30,6 +30,7 @@ export default class PrivacySettings extends React.Component {
var config = this.props.config;
config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+ config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked;
Client.saveConfig(
config,
@@ -137,6 +138,39 @@ export default class PrivacySettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableDiagnostic'
+ >
+ {'Send Error and Diagnostic: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='true'
+ ref='EnableDiagnostic'
+ defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDiagnostic'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 245ffa871..b2d1b7b4d 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -37,6 +37,8 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
//config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
+ config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
var MaximumLoginAttempts = 10;
@@ -199,7 +201,73 @@ export default class ServiceSettings extends React.Component {
/>
{'false'}
</label>
- <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostUsernameOverride'
+ >
+ {'Enable Overriding Usernames from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='true'
+ ref='EnablePostUsernameOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostUsernameOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePostIconOverride'
+ >
+ {'Enable Overriding Icon from Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='true'
+ ref='EnablePostIconOverride'
+ defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePostIconOverride'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'}</p>
</div>
</div>
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index 430a7453b..0e0ceb9af 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -73,6 +73,12 @@ export default class SqlSettings extends React.Component {
handleGenerate(e) {
e.preventDefault();
+
+ var cfm = global.window.confirm('Warning: re-generating this salt may cause some columns in the database to return empty results.');
+ if (cfm === false) {
+ return;
+ }
+
React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
var s = {saveNeeded: true, serverError: this.state.serverError};
this.setState(s);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 9eda68b38..45981b295 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -15,14 +15,24 @@ export default class ChannelNotifications extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
- this.handleUpdate = this.handleUpdate.bind(this);
- this.handleRadioClick = this.handleRadioClick.bind(this);
- this.handleQuietToggle = this.handleQuietToggle.bind(this);
- this.createDesktopSection = this.createDesktopSection.bind(this);
- this.createQuietSection = this.createQuietSection.bind(this);
- this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''};
+ this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
+ this.handleUpdateNotifyLevel = this.handleUpdateNotifyLevel.bind(this);
+ this.createNotifyLevelSection = this.createNotifyLevelSection.bind(this);
+
+ this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
+ this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
+ this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
+
+ this.state = {
+ notifyLevel: '',
+ markUnreadLevel: '',
+ title: '',
+ channelId: '',
+ activeSection: ''
+ };
}
+
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
@@ -30,33 +40,34 @@ export default class ChannelNotifications extends React.Component {
var button = e.relatedTarget;
var channelId = button.getAttribute('data-channelid');
- var notifyLevel = ChannelStore.getMember(channelId).notify_level;
- var quietMode = false;
-
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
- this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId});
+ this.setState({
+ notifyLevel,
+ markUnreadLevel,
+ title: button.getAttribute('data-title'),
+ channelId: channelId
+ });
}.bind(this));
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
}
+
onListenerChange() {
if (!this.state.channelId) {
return;
}
- var notifyLevel = ChannelStore.getMember(this.state.channelId).notify_level;
- var quietMode = false;
- if (notifyLevel === 'quiet') {
- quietMode = true;
- }
+ const member = ChannelStore.getMember(this.state.channelId);
+ var notifyLevel = member.notify_props.desktop;
+ var markUnreadLevel = member.notify_props.mark_unread;
var newState = this.state;
newState.notifyLevel = notifyLevel;
- newState.quietMode = quietMode;
+ newState.markUnreadLevel = markUnreadLevel;
if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState);
@@ -65,53 +76,64 @@ export default class ChannelNotifications extends React.Component {
updateSection(section) {
this.setState({activeSection: section});
}
- handleUpdate() {
+
+ handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var notifyLevel = this.state.notifyLevel;
- if (this.state.quietMode) {
- notifyLevel = 'quiet';
+
+ if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
+ this.updateSection('');
+ return;
}
var data = {};
data.channel_id = channelId;
data.user_id = UserStore.getCurrentId();
- data.notify_level = notifyLevel;
-
- if (!data.notify_level || data.notify_level.length === 0) {
- return;
- }
+ data.desktop = notifyLevel;
- Client.updateNotifyLevel(data,
- function success() {
+ Client.updateNotifyProps(data,
+ () => {
var member = ChannelStore.getMember(channelId);
- member.notify_level = notifyLevel;
+ member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
this.updateSection('');
- }.bind(this),
- function error(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
- handleRadioClick(notifyLevel) {
- this.setState({notifyLevel: notifyLevel, quietMode: false});
- React.findDOMNode(this.refs.modal).focus();
- }
- handleQuietToggle(quietMode) {
- this.setState({notifyLevel: 'none', quietMode: quietMode});
+
+ handleUpdateNotifyLevel(notifyLevel) {
+ this.setState({notifyLevel});
React.findDOMNode(this.refs.modal).focus();
}
- createDesktopSection(serverError) {
+
+ createNotifyLevelSection(serverError) {
var handleUpdateSection;
+ const user = UserStore.getCurrentUser();
+ const globalNotifyLevel = user.notify_props.desktop;
+
+ let globalNotifyLevelName;
+ if (globalNotifyLevel === 'all') {
+ globalNotifyLevelName = 'For all activity';
+ } else if (globalNotifyLevel === 'mention') {
+ globalNotifyLevelName = 'Only for mentions';
+ } else {
+ globalNotifyLevelName = 'Never';
+ }
+
if (this.state.activeSection === 'desktop') {
- var notifyActive = [false, false, false];
- if (this.state.notifyLevel === 'mention') {
- notifyActive[1] = true;
- } else if (this.state.notifyLevel === 'all') {
+ var notifyActive = [false, false, false, false];
+ if (this.state.notifyLevel === 'default') {
notifyActive[0] = true;
- } else {
+ } else if (this.state.notifyLevel === 'all') {
+ notifyActive[1] = true;
+ } else if (this.state.notifyLevel === 'mention') {
notifyActive[2] = true;
+ } else {
+ notifyActive[3] = true;
}
var inputs = [];
@@ -123,9 +145,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[0]}
- onChange={this.handleRadioClick.bind(this, 'all')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
>
- For all activity
+ {`Global default (${globalNotifyLevelName})`}
</input>
</label>
<br/>
@@ -135,9 +157,9 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[1]}
- onChange={this.handleRadioClick.bind(this, 'mention')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
>
- Only for mentions
+ {'For all activity'}
</input>
</label>
<br/>
@@ -147,9 +169,21 @@ export default class ChannelNotifications extends React.Component {
<input
type='radio'
checked={notifyActive[2]}
- onChange={this.handleRadioClick.bind(this, 'none')}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
+ >
+ {'Only for mentions'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={notifyActive[3]}
+ onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
>
- Never
+ {'Never'}
</input>
</label>
</div>
@@ -162,30 +196,19 @@ export default class ChannelNotifications extends React.Component {
e.preventDefault();
}.bind(this);
- let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (
+ const extraInfo = (
<span>
- These settings will override the global notification settings.
+ {'Selecting an option other than "Default" will override the global notification settings.'}
<br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
+ {'Desktop notifications are available on Firefox, Safari, and Chrome.'}
</span>
);
- if (curChannel && curChannel.display_name) {
- extraInfo = (
- <span>
- These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
- <br/>
- Desktop notifications are available on Firefox, Safari, and Chrome.
- </span>
- );
- }
-
return (
<SettingItemMax
title='Send desktop notifications'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitNotifyLevel}
server_error={serverError}
updateSection={handleUpdateSection}
extraInfo={extraInfo}
@@ -194,7 +217,9 @@ export default class ChannelNotifications extends React.Component {
}
var describe;
- if (this.state.notifyLevel === 'mention') {
+ if (this.state.notifyLevel === 'default') {
+ describe = `Global default (${globalNotifyLevelName})`;
+ } else if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
@@ -215,101 +240,123 @@ export default class ChannelNotifications extends React.Component {
/>
);
}
- createQuietSection(serverError) {
- var handleUpdateSection;
- if (this.state.activeSection === 'quiet') {
- var quietActive = [false, false];
- if (this.state.quietMode) {
- quietActive[0] = true;
- } else {
- quietActive[1] = true;
+
+ handleSubmitMarkUnreadLevel() {
+ const channelId = this.state.channelId;
+ const markUnreadLevel = this.state.markUnreadLevel;
+
+ if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
+ this.updateSection('');
+ return;
+ }
+
+ const data = {
+ channel_id: channelId,
+ user_id: UserStore.getCurrentId(),
+ mark_unread: markUnreadLevel
+ };
+
+ Client.updateNotifyProps(data,
+ () => {
+ var member = ChannelStore.getMember(channelId);
+ member.notify_props.mark_unread = markUnreadLevel;
+ ChannelStore.setChannelMember(member);
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
}
+ );
+ }
- var inputs = [];
+ handleUpdateMarkUnreadLevel(markUnreadLevel) {
+ this.setState({markUnreadLevel});
+ React.findDOMNode(this.refs.modal).focus();
+ }
- inputs.push(
+ createMarkUnreadLevelSection(serverError) {
+ let content;
+
+ if (this.state.activeSection === 'markUnreadLevel') {
+ const inputs = [(
<div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[0]}
- onChange={this.handleQuietToggle.bind(this, true)}
+ checked={this.state.markUnreadLevel === 'all'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
>
- On
+ {'For all unread messages'}
</input>
</label>
- <br/>
+ <br />
</div>
<div className='radio'>
<label>
<input
type='radio'
- checked={quietActive[1]}
- onChange={this.handleQuietToggle.bind(this, false)}
+ checked={this.state.markUnreadLevel === 'mention'}
+ onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
>
- Off
+ {'Only for mentions'}
</input>
</label>
- <br/>
+ <br />
</div>
</div>
- );
-
- inputs.push(
- <div>
- <br/>
- Enabling quiet mode will turn off desktop notifications and only mark the channel as unread if you have been mentioned.
- </div>
- );
+ )];
- handleUpdateSection = function updateSection(e) {
+ const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
}.bind(this);
- return (
+ const extraInfo = <span>{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'}</span>;
+
+ content = (
<SettingItemMax
- title='Quiet mode'
+ title='Mark Channel Unread'
inputs={inputs}
- submit={this.handleUpdate}
+ submit={this.handleSubmitMarkUnreadLevel}
server_error={serverError}
updateSection={handleUpdateSection}
+ extraInfo={extraInfo}
/>
);
- }
-
- var describe;
- if (this.state.quietMode) {
- describe = 'On';
} else {
- describe = 'Off';
- }
+ let describe;
- handleUpdateSection = function updateSection(e) {
- this.updateSection('quiet');
- e.preventDefault();
- }.bind(this);
+ if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
+ describe = 'For all unread messages';
+ } else {
+ describe = 'Only for mentions';
+ }
- return (
- <SettingItemMin
- title='Quiet mode'
- describe={describe}
- updateSection={handleUpdateSection}
- />
- );
+ const handleUpdateSection = function handleUpdateSection(e) {
+ this.updateSection('markUnreadLevel');
+ e.preventDefault();
+ }.bind(this);
+
+ content = (
+ <SettingItemMin
+ title='Mark Channel Unread'
+ describe={describe}
+ updateSection={handleUpdateSection}
+ />
+ );
+ }
+
+ return content;
}
+
render() {
var serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
- var desktopSection = this.createDesktopSection(serverError);
-
- var quietSection = this.createQuietSection(serverError);
-
return (
<div
className='modal fade'
@@ -341,9 +388,9 @@ export default class ChannelNotifications extends React.Component {
>
<br/>
<div className='divider-dark first'/>
- {desktopSection}
+ {this.createNotifyLevelSection(serverError)}
<div className='divider-light'/>
- {quietSection}
+ {this.createMarkUnreadLevelSection(serverError)}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
index 63bd57c2a..fea7085b7 100644
--- a/web/react/components/command_list.jsx
+++ b/web/react/components/command_list.jsx
@@ -96,4 +96,4 @@ CommandList.defaultProps = {
CommandList.propTypes = {
addCommand: React.PropTypes.func,
channelId: React.PropTypes.string
-}; \ No newline at end of file
+};
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index b7bce9b34..54d77c358 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -43,7 +43,7 @@ export default class MoreDirectChannels extends React.Component {
handleClick = function clickHandler(e) {
e.preventDefault();
- utils.switchChannel(channel, channel.teammate_username);
+ utils.switchChannel(channel);
$(React.findDOMNode(self.refs.modal)).modal('hide');
};
} else {
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 4c01d2c43..78057d10b 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -9,7 +9,7 @@ var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
function getStateFromStores() {
- return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()};
+ return {teams: UserStore.getTeams()};
}
export default class NavbarDropdown extends React.Component {
@@ -62,7 +62,7 @@ export default class NavbarDropdown extends React.Component {
if (currentUser != null) {
isAdmin = Utils.isAdmin(currentUser.roles);
- isSystemAdmin = Utils.isInRole(currentUser.roles, 'system_admin');
+ isSystemAdmin = Utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
@@ -142,10 +142,10 @@ export default class NavbarDropdown extends React.Component {
>
</li>
);
- if (this.state.teams.length > 1 && this.state.currentTeam) {
- var curTeamName = this.state.currentTeam.name;
+
+ if (this.state.teams.length > 1) {
this.state.teams.forEach((teamName) => {
- if (teamName !== curTeamName) {
+ if (teamName !== this.props.teamName) {
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
}
});
@@ -234,5 +234,7 @@ NavbarDropdown.defaultProps = {
teamType: ''
};
NavbarDropdown.propTypes = {
- teamType: React.PropTypes.string
+ teamType: React.PropTypes.string,
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx
index 0b7c41b62..f34b4669f 100644
--- a/web/react/components/notify_counts.jsx
+++ b/web/react/components/notify_counts.jsx
@@ -15,7 +15,7 @@ function getCountsStateFromStores() {
count += channel.total_msg_count - channelMember.msg_count;
} else if (channelMember.mention_count > 0) {
count += channelMember.mention_count;
- } else if (channelMember.notify_level !== 'quiet' && channel.total_msg_count - channelMember.msg_count > 0) {
+ } else if (channelMember.notify_props.mark_unread !== 'mention' && channel.total_msg_count - channelMember.msg_count > 0) {
count += 1;
}
});
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index a2ca8b00f..95a88c3d6 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var UserStore = require('../stores/user_store.jsx');
+
export default class PopoverListMembers extends React.Component {
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
@@ -32,22 +34,28 @@ export default class PopoverListMembers extends React.Component {
}
render() {
let popoverHtml = '';
+ let count = 0;
+ let countText = '-';
const members = this.props.members;
- let count;
- if (members.length > 20) {
- count = '20+';
- } else {
- count = members.length || '-';
- }
+ const teamMembers = UserStore.getProfilesUsernameMap();
- if (members) {
+ if (members && teamMembers) {
members.sort(function compareByLocal(a, b) {
return a.username.localeCompare(b.username);
});
members.forEach(function addMemberElement(m) {
- popoverHtml += `<div class='text--nowrap'>${m.username}</div>`;
+ if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
+ popoverHtml += `<div class='text--nowrap'>${m.username}</div>`;
+ count++;
+ }
});
+
+ if (count > 20) {
+ countText = '20+';
+ } else if (count > 0) {
+ countText = count.toString();
+ }
}
return (
@@ -63,7 +71,7 @@ export default class PopoverListMembers extends React.Component {
data-toggle='tooltip'
title='View Channel Members'
>
- {count}
+ {countText}
<span
className='fa fa-user'
aria-hidden='true'
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 9127f00de..ac9c9252e 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -158,11 +158,18 @@ export default class Post extends React.Component {
var profilePic = null;
if (!this.props.hideProfilePic) {
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
+ if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') {
+ if (post.props.override_icon_url) {
+ src = post.props.override_icon_url;
+ }
+ }
+
profilePic = (
<div className='post-profile-img__container'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
+ src={src}
height='36'
width='36'
/>
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 6e98e4aba..6cfd243de 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -12,7 +12,10 @@ export default class PostBody extends React.Component {
constructor(props) {
super(props);
+ this.receivedYoutubeData = false;
+
this.parseEmojis = this.parseEmojis.bind(this);
+ this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this);
const linkData = Utils.extractLinks(this.props.post.message);
this.state = {links: linkData.links, message: linkData.text};
@@ -50,6 +53,123 @@ export default class PostBody extends React.Component {
this.setState({links: linkData.links, message: linkData.text});
}
+ handleYoutubeTime(link) {
+ const timeRegex = /[\\?&]t=([0-9hms]+)/;
+
+ const time = link.trim().match(timeRegex);
+ if (!time || !time[1]) {
+ return '';
+ }
+
+ const hours = time[1].match(/([0-9]+)h/);
+ const minutes = time[1].match(/([0-9]+)m/);
+ const seconds = time[1].match(/([0-9]+)s/);
+
+ let ticks = 0;
+
+ if (hours && hours[1]) {
+ ticks += parseInt(hours[1], 10) * 3600;
+ }
+
+ if (minutes && minutes[1]) {
+ ticks += parseInt(minutes[1], 10) * 60;
+ }
+
+ if (seconds && seconds[1]) {
+ ticks += parseInt(seconds[1], 10);
+ }
+
+ return '&start=' + ticks.toString();
+ }
+
+ createYoutubeEmbed(link) {
+ const ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
+
+ const match = link.trim().match(ytRegex);
+ if (!match || match[1].length !== 11) {
+ return null;
+ }
+
+ const youtubeId = match[1];
+ const time = this.handleYoutubeTime(link);
+
+ function onClick(e) {
+ var div = $(e.target).closest('.video-thumbnail__container')[0];
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('src',
+ 'https://www.youtube.com/embed/' +
+ div.id +
+ '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
+ time);
+ iframe.setAttribute('width', '480px');
+ iframe.setAttribute('height', '360px');
+ iframe.setAttribute('type', 'text/html');
+ iframe.setAttribute('frameborder', '0');
+ iframe.setAttribute('allowfullscreen', 'allowfullscreen');
+
+ div.parentNode.replaceChild(iframe, div);
+ }
+
+ function success(data) {
+ if (!data.items.length || !data.items[0].snippet) {
+ return null;
+ }
+ var metadata = data.items[0].snippet;
+ this.receivedYoutubeData = true;
+ this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title});
+ }
+
+ if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) {
+ $.ajax({
+ async: true,
+ url: 'https://www.googleapis.com/youtube/v3/videos',
+ type: 'GET',
+ data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
+ success: success.bind(this)
+ });
+ }
+
+ let header = 'Youtube';
+ if (this.state.youtubeTitle) {
+ header = header + ' - ';
+ }
+
+ let uploader = this.state.youtubeUploader;
+ if (!uploader) {
+ uploader = 'unknown';
+ }
+
+ return (
+ <div className='post-comment'>
+ <h4>
+ <span className='video-type'>{header}</span>
+ <span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
+ </h4>
+ <h4 className='video-uploader'>{uploader}</h4>
+ <div
+ className='video-div embed-responsive-item'
+ id={youtubeId}
+ onClick={onClick}
+ >
+ <div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
+ <div
+ id={youtubeId}
+ className='video-thumbnail__container'
+ >
+ <img
+ className='video-thumbnail'
+ src={'https://i.ytimg.com/vi/' + youtubeId + '/hqdefault.jpg'}
+ />
+ <div className='block'>
+ <span className='play-button'><span/></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
render() {
const post = this.props.post;
const filenames = this.props.post.filenames;
@@ -133,7 +253,7 @@ export default class PostBody extends React.Component {
let embed;
if (filenames.length === 0 && this.state.links) {
- embed = Utils.getEmbed(this.state.links[0]);
+ embed = this.createYoutubeEmbed(this.state.links[0]);
}
let fileAttachmentHolder = '';
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 9dc525e03..dd79b3e36 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -12,9 +12,27 @@ export default class PostHeader extends React.Component {
render() {
var post = this.props.post;
+ let userProfile = <UserProfile userId={post.user_id} />;
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') {
+ userProfile = (
+ <UserProfile
+ userId={post.user_id}
+ overwriteName={post.props.override_username}
+ disablePopover={true}
+ />
+ );
+ }
+
+ botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ }
+
return (
<ul className='post-header post-header-post'>
- <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li>
+ <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li>
+ {botIndicator}
<li className='post-info--hidden'>
<PostInfo
post={post}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 824e7ef39..dba75ac5f 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,7 +150,7 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
- let tooltip = <Tooltip>{utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}</Tooltip>;
+ let tooltip = <Tooltip id={post.id + 'tooltip'}>{`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}</Tooltip>;
return (
<ul className='post-header post-info'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 3e1e075bb..b90197ac4 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -128,6 +128,15 @@ export default class PostList extends React.Component {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
+
+ $('.top-visible-post').removeClass('top-visible-post');
+
+ $(React.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
+ if ($(this).position().top + $(this).height() / 2 > 0) {
+ $(this).addClass('top-visible-post');
+ return false;
+ }
+ });
});
$('.post-list__content div .post').removeClass('post--last');
@@ -507,8 +516,19 @@ export default class PostList extends React.Component {
sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
- // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post
- hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post);
+ // hide the profile pic if:
+ // the previous post was made by the same user as the current post,
+ // the previous post is not a comment,
+ // the current post is not a comment,
+ // the current post is not from a webhook
+ // and the previous post is not from a webhook
+ if ((prevPost.user_id === post.user_id) &&
+ !utils.isComment(prevPost) &&
+ !utils.isComment(post) &&
+ (!post.props || !post.props.from_webhook) &&
+ (!prevPost.props || !prevPost.props.from_webhook)) {
+ hideProfilePic = true;
+ }
}
// check if it's the last comment in a consecutive string of comments on the same post
@@ -665,7 +685,10 @@ export default class PostList extends React.Component {
className={'post-list-holder-by-time ' + activeClass}
>
<div className='post-list__table'>
- <div className='post-list__content'>
+ <div
+ ref='postlistcontent'
+ className='post-list__content'
+ >
{moreMessages}
{postCtls}
</div>
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 37958b649..9d514c741 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -78,7 +78,6 @@ export default class RenameChannelModal extends React.Component {
$(React.findDOMNode(this.refs.modal)).modal('hide');
AsyncClient.getChannel(channel.id);
- Utils.updateTabTitle(channel.display_name);
Utils.updateAddressBar(channel.name);
React.findDOMNode(this.refs.displayName).value = '';
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 32b521560..bdefdbee8 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -47,13 +47,8 @@ export default class SearchResultsItem extends React.Component {
);
var postChannel = ChannelStore.get(this.props.post.channel_id);
- var teammate = '';
- if (postChannel.type === 'D') {
- teammate = utils.getDirectTeammate(this.props.post.channel_id).username;
- }
-
- utils.switchChannel(postChannel, teammate);
+ utils.switchChannel(postChannel);
}
render() {
diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx
index fad27b355..ccb26cc58 100644
--- a/web/react/components/setting_upload.jsx
+++ b/web/react/components/setting_upload.jsx
@@ -7,11 +7,11 @@ export default class SettingsUpload extends React.Component {
this.doFileSelect = this.doFileSelect.bind(this);
this.doSubmit = this.doSubmit.bind(this);
- this.onFileSelect = this.onFileSelect.bind(this);
this.state = {
clientError: this.props.clientError,
- serverError: this.props.serverError
+ serverError: this.props.serverError,
+ filename: ''
};
}
@@ -24,9 +24,14 @@ export default class SettingsUpload extends React.Component {
doFileSelect(e) {
e.preventDefault();
+ var filename = $(e.target).val();
+ if (filename.substring(3, 11) === 'fakepath') {
+ filename = filename.substring(12);
+ }
this.setState({
clientError: '',
- serverError: ''
+ serverError: '',
+ filename
});
}
@@ -40,28 +45,28 @@ export default class SettingsUpload extends React.Component {
}
}
- onFileSelect(e) {
- var filename = $(e.target).val();
- if (filename.substring(3, 11) === 'fakepath') {
- filename = filename.substring(12);
- }
- $(e.target).closest('li').find('.file-status').addClass('hide');
- $(e.target).closest('li').find('.file-name').removeClass('hide').html(filename);
- }
-
render() {
- var clientError = null;
+ let clientError = null;
if (this.state.clientError) {
clientError = (
<div className='file-status'>{this.state.clientError}</div>
);
}
- var serverError = null;
+ let serverError = null;
if (this.state.serverError) {
serverError = (
<div className='file-status'>{this.state.serverError}</div>
);
}
+ let fileNameText = null;
+ let submitButtonClass = 'btn btn-sm btn-primary disabled';
+ if (this.state.filename) {
+ fileNameText = (
+ <div className='file-status file-name'>{this.state.filename}</div>
+ );
+ submitButtonClass = 'btn btn-sm btn-primary';
+ }
+
return (
<ul className='section-max'>
<li className='col-sm-12 section-title'>{this.props.title}</li>
@@ -70,21 +75,21 @@ export default class SettingsUpload extends React.Component {
<ul className='setting-list'>
<li className='setting-list-item'>
<span className='btn btn-sm btn-primary btn-file sel-btn'>
- Select file
+ {'Select file'}
<input
ref='uploadinput'
accept={this.props.fileTypesAccepted}
type='file'
- onChange={this.onFileSelect}
+ onChange={this.doFileSelect}
/>
</span>
<a
- className={'btn btn-sm btn-primary'}
+ className={submitButtonClass}
onClick={this.doSubmit}
>
- Import
+ {'Import'}
</a>
- <div className='file-status file-name hide'></div>
+ {fileNameText}
{serverError}
{clientError}
</li>
@@ -102,4 +107,4 @@ SettingsUpload.propTypes = {
clientError: React.PropTypes.string,
serverError: React.PropTypes.string,
helpText: React.PropTypes.object
-};
+}; \ No newline at end of file
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 14664ed4d..c0841a508 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -200,13 +200,17 @@ export default class Sidebar extends React.Component {
}
var channel = ChannelStore.get(msg.channel_id);
- var user = UserStore.getCurrentUser();
- if (user.notify_props && ((user.notify_props.desktop === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') || user.notify_props.desktop === 'none')) {
- return;
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(msg.channel_id);
+
+ var notifyLevel = member.notify_props.desktop;
+ if (notifyLevel === 'default') {
+ notifyLevel = user.notify_props.desktop;
}
- var member = ChannelStore.getMember(msg.channel_id);
- if ((member.notify_level === 'mention' && mentions.indexOf(user.id) === -1) || member.notify_level === 'none' || member.notify_level === 'quiet') {
+ if (notifyLevel === 'none') {
+ return;
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
return;
}
@@ -268,14 +272,19 @@ export default class Sidebar extends React.Component {
}
}
updateTitle() {
- var channel = ChannelStore.getCurrent();
+ const channel = ChannelStore.getCurrent();
if (channel) {
+ let currentSiteName = '';
+ if (global.window.config.SiteName != null) {
+ currentSiteName = global.window.config.SiteName;
+ }
+
+ let currentChannelName = channel.display_name;
if (channel.type === 'D') {
- var teammateUsername = Utils.getDirectTeammate(channel.id).username;
- document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-'));
- } else {
- document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-'));
+ currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
+
+ document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
}
}
onScroll() {
@@ -325,7 +334,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
- unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0;
+ unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
var titleClass = '';
@@ -503,6 +512,7 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
teamType={this.props.teamType}
/>
<SearchBox />
@@ -582,5 +592,6 @@ Sidebar.defaultProps = {
};
Sidebar.propTypes = {
teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 072c14e0a..33de35064 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -52,6 +52,8 @@ export default class SidebarHeader extends React.Component {
<NavbarDropdown
ref='dropdown'
teamType={this.props.teamType}
+ teamDisplayName={this.props.teamDisplayName}
+ teamName={this.props.teamName}
/>
</div>
);
@@ -64,5 +66,6 @@ SidebarHeader.defaultProps = {
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
+ teamName: React.PropTypes.string,
teamType: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 913715154..708cd04cb 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -14,9 +14,10 @@ export default class SidebarRight extends React.Component {
constructor(props) {
super(props);
+ this.plScrolledToBottom = true;
+
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
- this.resize = this.resize.bind(this);
this.state = getStateFromStores();
}
@@ -28,6 +29,14 @@ export default class SidebarRight extends React.Component {
PostStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
+ componentDidUpdate() {
+ if (!this.plScrolledToBottom) {
+ $('.top-visible-post')[0].scrollIntoView();
+ } else {
+ var postHolder = $('.post-list-holder-by-time').not('.inactive');
+ postHolder.scrollTop(postHolder[0].scrollHeight);
+ }
+ }
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
newState.from_search = fromSearch;
@@ -41,15 +50,15 @@ export default class SidebarRight extends React.Component {
this.setState(newState);
}
}
- resize() {
- var postHolder = $('.post-list-holder-by-time');
- postHolder[0].scrollTop = postHolder[0].scrollHeight - 224;
- }
render() {
+ var postHolder = $('.post-list-holder-by-time').not('.inactive');
+ const position = postHolder.scrollTop() + postHolder.height() + 14;
+ const bottom = postHolder[0].scrollHeight;
+ this.plScrolledToBottom = position >= bottom;
+
if (!(this.state.search_visible || this.state.post_right_visible)) {
$('.inner__wrap').removeClass('move--left').removeClass('move--right');
$('.sidebar--right').removeClass('move--left');
- this.resize();
return (
<div></div>
);
@@ -59,8 +68,8 @@ export default class SidebarRight extends React.Component {
$('.sidebar--left').removeClass('move--right');
$('.sidebar--right').addClass('move--left');
$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
- this.resize();
- setTimeout(function overlayTimer() {
+
+ setTimeout(() => {
$('.sidebar__overlay').fadeOut('200', function fadeOverlay() {
$(this).remove();
});
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index f1341d9d7..2df2c8ffd 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -26,11 +26,14 @@ export default class SidebarRightMenu extends React.Component {
var inviteLink = '';
var teamSettingsLink = '';
var manageLink = '';
+ var consoleLink = '';
var currentUser = UserStore.getCurrentUser();
var isAdmin = false;
+ var isSystemAdmin = false;
if (currentUser != null) {
isAdmin = utils.isAdmin(currentUser.roles);
+ isSystemAdmin = utils.isSystemAdmin(currentUser.roles);
inviteLink = (
<li>
@@ -77,6 +80,17 @@ export default class SidebarRightMenu extends React.Component {
);
}
+ if (isSystemAdmin) {
+ consoleLink = (
+ <li>
+ <a
+ href='/admin_console'
+ >
+ <i className='glyphicon glyphicon-wrench'></i>System Console</a>
+ </li>
+ );
+ }
+
var siteName = '';
if (global.window.config.SiteName != null) {
siteName = global.window.config.SiteName;
@@ -107,6 +121,7 @@ export default class SidebarRightMenu extends React.Component {
{inviteLink}
{teamLink}
{manageLink}
+ {consoleLink}
<li>
<a
href='#'
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 1b722d611..a3f89a217 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -35,8 +35,8 @@ export default class TeamSignupUrlPage extends React.Component {
if (cleanedName !== name || !urlRegex.test(name)) {
this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."});
return;
- } else if (cleanedName.length <= 3 || cleanedName.length > 15) {
- this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
+ } else if (cleanedName.length <= 2 || cleanedName.length > 15) {
+ this.setState({nameError: 'Name must be 3 or more characters up to a maximum of 15'});
return;
}
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index a4972dd8d..14f281f7a 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -23,12 +23,14 @@ export default class SSOSignUpPage extends React.Component {
team.display_name = this.state.name;
- if (team.display_name.length <= 3) {
+ if (!team.display_name) {
+ state.nameError = 'Please enter a team name';
+ this.setState(state);
return;
}
- if (!team.display_name) {
- state.nameError = 'Please enter a team name';
+ if (team.display_name.length <= 2) {
+ state.nameError = 'Name must be 3 or more characters up to a maximum of 15';
this.setState(state);
return;
}
@@ -68,7 +70,7 @@ export default class SSOSignUpPage extends React.Component {
}
var disabled = false;
- if (this.state.name.length <= 3) {
+ if (this.state.name.length <= 2) {
disabled = true;
}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index c5d028d31..ceb8f52a7 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -31,8 +31,10 @@ export default class UserProfile extends React.Component {
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
- $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
- $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ if (!this.props.disablePopover) {
+ $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
+ $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
+ }
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
@@ -56,6 +58,10 @@ export default class UserProfile extends React.Component {
name = this.props.overwriteName;
}
+ if (this.props.disablePopover) {
+ return <div>{name}</div>;
+ }
+
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
@@ -79,9 +85,11 @@ export default class UserProfile extends React.Component {
UserProfile.defaultProps = {
userId: '',
- overwriteName: ''
+ overwriteName: '',
+ disablePopover: false
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
- overwriteName: React.PropTypes.string
+ overwriteName: React.PropTypes.string,
+ disablePopover: React.PropTypes.bool
};
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 48be83afe..0f5cb59f5 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -140,7 +140,7 @@ export default class ImportThemeModal extends React.Component {
>
<Modal.Body>
<p>
- {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
+ {'To import a theme, go to a Slack team and look for “Preferences -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
</p>
<div className='form-group less'>
<div className='col-sm-9'>
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index 1bbfbd162..899dbcd05 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -21,7 +21,7 @@ export default class ManageIncomingHooks extends React.Component {
this.getHooks();
}
addNewHook() {
- let hook = {}; //eslint-disable-line prefer-const
+ const hook = {};
hook.channel_id = this.state.channelId;
Client.addIncomingHook(
@@ -40,13 +40,13 @@ export default class ManageIncomingHooks extends React.Component {
);
}
removeHook(id) {
- let data = {}; //eslint-disable-line prefer-const
+ const data = {};
data.id = id;
Client.deleteIncomingHook(
data,
() => {
- let hooks = this.state.hooks; //eslint-disable-line prefer-const
+ const hooks = this.state.hooks;
let index = -1;
for (let i = 0; i < hooks.length; i++) {
if (hooks[i].id === id) {
@@ -69,7 +69,7 @@ export default class ManageIncomingHooks extends React.Component {
getHooks() {
Client.listIncomingHooks(
(data) => {
- let state = this.state; //eslint-disable-line prefer-const
+ const state = this.state;
if (data) {
state.hooks = data;
@@ -93,9 +93,11 @@ export default class ManageIncomingHooks extends React.Component {
}
const channels = ChannelStore.getAll();
- let options = []; //eslint-disable-line prefer-const
+ const options = [];
channels.forEach((channel) => {
- options.push(<option value={channel.id}>{channel.name}</option>);
+ if (channel.type !== Constants.DM_CHANNEL) {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ }
});
let disableButton = '';
@@ -103,7 +105,7 @@ export default class ManageIncomingHooks extends React.Component {
disableButton = ' disable';
}
- let hooks = []; //eslint-disable-line prefer-const
+ const hooks = [];
this.state.hooks.forEach((hook) => {
const c = ChannelStore.get(hook.channel_id);
hooks.push(
@@ -146,6 +148,8 @@ export default class ManageIncomingHooks extends React.Component {
return (
<div key='addIncomingHook'>
+ {'Create webhook URLs for channels and private groups. These URLs can be used by outside applications to create posts in any channels or private groups you have access to. The specified channel will be used as the default.'}
+ <br/>
<label className='control-label'>{'Add a new incoming webhook'}</label>
<div className='padding-top'>
<select
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index f8f916bd0..8116bffcc 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -24,7 +24,7 @@ export default class PremadeThemeChooser extends React.Component {
premadeThemes.push(
<div
- className='col-sm-3 premade-themes'
+ className='col-xs-6 col-sm-3 premade-themes'
key={'premade-theme-key' + k}
>
<div
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 4372069e7..c4a137ed8 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -220,7 +220,7 @@ export default class UserSettingsAppearance extends React.Component {
className='theme'
onClick={this.handleImportModal}
>
- {'Import from Slack'}
+ {'Import theme colors from Slack'}
</a>
</div>
);
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index ba14f019f..e83f18aab 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -17,7 +17,7 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.desktop_sound) {
sound = user.notify_props.desktop_sound;
}
- var desktop = 'all';
+ var desktop = 'default';
if (user.notify_props && user.notify_props.desktop) {
desktop = user.notify_props.desktop;
}
@@ -420,7 +420,7 @@ export default class NotificationsTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
+ <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.config.SiteName + ' for more than 5 minutes.'}</div>
</div>
);
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
index db42c8266..8ea8b13eb 100644
--- a/web/react/pages/authorize.jsx
+++ b/web/react/pages/authorize.jsx
@@ -3,16 +3,16 @@
var Authorize = require('../components/authorize.jsx');
-function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
+function setupAuthorizePage(props) {
React.render(
<Authorize
- teamName={teamName}
- appName={appName}
- responseType={responseType}
- clientId={clientId}
- redirectUri={redirectUri}
- scope={scope}
- state={state}
+ teamName={props.TeamName}
+ appName={props.AppName}
+ responseType={props.ResponseType}
+ clientId={props.ClientId}
+ redirectUri={props.RedirectUri}
+ scope={props.Scope}
+ state={props.State}
/>,
document.getElementById('authorize')
);
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 74259194a..c333fd57d 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -36,11 +36,14 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal.
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
function setupChannelPage(props) {
+ TeamStore.setCurrentId(props.TeamId);
+
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: props.ChannelName,
@@ -71,6 +74,7 @@ function setupChannelPage(props) {
React.render(
<Sidebar
teamDisplayName={props.TeamDisplayName}
+ teamName={props.TeamName}
teamType={props.TeamType}
/>,
document.getElementById('sidebar-left')
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ab2965000..7db3ef30d 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -152,21 +152,23 @@ export function getChannel(id) {
}
export function updateLastViewedAt() {
- if (isCallInProgress('updateLastViewed')) {
+ const channelId = ChannelStore.getCurrentId();
+
+ if (channelId === null) {
return;
}
- if (ChannelStore.getCurrentId() == null) {
+ if (isCallInProgress(`updateLastViewed${channelId}`)) {
return;
}
- callTracker.updateLastViewed = utils.getTimestamp();
+ callTracker[`updateLastViewed${channelId}`] = utils.getTimestamp();
client.updateLastViewedAt(
- ChannelStore.getCurrentId(),
- function updateLastViewedAtSuccess() {
+ channelId,
+ () => {
callTracker.updateLastViewed = 0;
},
- function updateLastViewdAtFailure(err) {
+ (err) => {
callTracker.updateLastViewed = 0;
dispatchError(err, 'updateLastViewedAt');
}
@@ -634,4 +636,4 @@ export function getMyTeam() {
dispatchError(err, 'getMyTeam');
}
);
-} \ No newline at end of file
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 715e26197..5cb165b4c 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -332,6 +332,20 @@ export function saveConfig(config, success, error) {
});
}
+export function logClientError(msg) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = msg;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+}
+
export function testEmail(config, success, error) {
$.ajax({
url: '/api/v1/admin/test_email',
@@ -568,16 +582,16 @@ export function updateChannelDesc(data, success, error) {
track('api', 'api_channels_desc');
}
-export function updateNotifyLevel(data, success, error) {
+export function updateNotifyProps(data, success, error) {
$.ajax({
- url: '/api/v1/channels/update_notify_level',
+ url: '/api/v1/channels/update_notify_props',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
- var e = handleError('updateNotifyLevel', xhr, status, err);
+ var e = handleError('updateNotifyProps', xhr, status, err);
error(e);
}
});
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 75e80bc7e..67414dc3b 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -111,6 +111,7 @@ module.exports = {
],
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
+ DM_CHANNEL: 'D',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
@@ -178,7 +179,7 @@ module.exports = {
centerChannelColor: '#DDDDDD',
newMessageSeparator: '#5de5da',
linkColor: '#A4FFEB',
- buttonBg: '#1dacfc',
+ buttonBg: '#4CBBA4',
buttonColor: '#FFFFFF'
},
windows10: {
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 56bf49c3f..34e42cbae 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -87,8 +87,10 @@ function autolinkUrls(text, tokens) {
const linkText = match.getMatchedText();
let url = linkText;
- if (url.lastIndexOf('http', 0) !== 0) {
- url = `http://${linkText}`;
+ if (match.getType() === 'email') {
+ url = `mailto:${url}`;
+ } else if (url.lastIndexOf('http', 0) !== 0) {
+ url = `http://${url}`;
}
const index = tokens.size;
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 556f3967c..1bc082175 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -78,6 +78,14 @@ export function isAdmin(roles) {
return false;
}
+export function isSystemAdmin(roles) {
+ if (isInRole(roles, 'system_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
export function getDomainWithOutSub() {
var parts = window.location.host.split('.');
@@ -255,163 +263,6 @@ export function escapeRegExp(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
}
-function handleYoutubeTime(link) {
- var timeRegex = /[\\?&]t=([0-9hms]+)/;
-
- var time = link.trim().match(timeRegex);
- if (!time || !time[1]) {
- return '';
- }
-
- var hours = time[1].match(/([0-9]+)h/);
- var minutes = time[1].match(/([0-9]+)m/);
- var seconds = time[1].match(/([0-9]+)s/);
-
- var ticks = 0;
-
- if (hours && hours[1]) {
- ticks += parseInt(hours[1], 10) * 3600;
- }
-
- if (minutes && minutes[1]) {
- ticks += parseInt(minutes[1], 10) * 60;
- }
-
- if (seconds && seconds[1]) {
- ticks += parseInt(seconds[1], 10);
- }
-
- return '&start=' + ticks.toString();
-}
-
-function getYoutubeEmbed(link) {
- var regex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
-
- var youtubeId = link.trim().match(regex)[1];
- var time = handleYoutubeTime(link);
-
- function onClick(e) {
- var div = $(e.target).closest('.video-thumbnail__container')[0];
- var iframe = document.createElement('iframe');
- iframe.setAttribute('src',
- 'https://www.youtube.com/embed/' +
- div.id +
- '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' +
- time);
- iframe.setAttribute('width', '480px');
- iframe.setAttribute('height', '360px');
- iframe.setAttribute('type', 'text/html');
- iframe.setAttribute('frameborder', '0');
- iframe.setAttribute('allowfullscreen', 'allowfullscreen');
-
- div.parentNode.replaceChild(iframe, div);
- }
-
- function success(data) {
- if (!data.items.length || !data.items[0].snippet) {
- return;
- }
- var metadata = data.items[0].snippet;
- $('.video-type.' + youtubeId).html('Youtube - ');
- $('.video-uploader.' + youtubeId).html(metadata.channelTitle);
- $('.video-title.' + youtubeId).find('a').html(metadata.title);
- }
-
- if (global.window.config.GoogleDeveloperKey) {
- $.ajax({
- async: true,
- url: 'https://www.googleapis.com/youtube/v3/videos',
- type: 'GET',
- data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
- success: success
- });
- }
-
- return (
- <div className='post-comment'>
- <h4>
- <span className={'video-type ' + youtubeId}>YouTube</span>
- <span className={'video-title ' + youtubeId}><a href={link}></a></span>
- </h4>
- <h4 className={'video-uploader ' + youtubeId}></h4>
- <div
- className='video-div embed-responsive-item'
- id={youtubeId}
- onClick={onClick}
- >
- <div className='embed-responsive embed-responsive-4by3 video-div__placeholder'>
- <div
- id={youtubeId}
- className='video-thumbnail__container'
- >
- <img
- className='video-thumbnail'
- src={'https://i.ytimg.com/vi/' + youtubeId + '/hqdefault.jpg'}
- />
- <div className='block'>
- <span className='play-button'><span/></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
-}
-
-export function getEmbed(link) {
- var ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
-
- var match = link.trim().match(ytRegex);
- if (match && match[1].length === 11) {
- return getYoutubeEmbed(link);
- }
-
- // Generl embed feature turned off for now
- return '';
-
- // NEEDS REFACTORING WHEN TURNED BACK ON
- /*
- var id = parseInt((Math.random() * 1000000) + 1);
-
- $.ajax({
- type: 'GET',
- url: 'https://query.yahooapis.com/v1/public/yql',
- data: {
- q: 'select * from html where url="' + link + "\" and xpath='html/head'",
- format: 'json'
- },
- async: true
- }).done(function(data) {
- if(!data.query.results) {
- return;
- }
-
- var headerData = data.query.results.head;
-
- var description = ''
- for(var i = 0; i < headerData.meta.length; i++) {
- if(headerData.meta[i].name && (headerData.meta[i].name === 'description' || headerData.meta[i].name === 'Description')){
- description = headerData.meta[i].content;
- break;
- }
- }
-
- $('.embed-title.'+id).html(headerData.title);
- $('.embed-description.'+id).html(description);
- })
-
- return (
- <div className='post-comment'>
- <div className={'web-embed-data'}>
- <p className={'embed-title ' + id} />
- <p className={'embed-description ' + id} />
- <p className={'embed-link ' + id}>{link}</p>
- </div>
- </div>
- );
- */
-}
-
export function areStatesEqual(state1, state2) {
return JSON.stringify(state1) === JSON.stringify(state2);
}
@@ -544,35 +395,39 @@ export function toTitleCase(str) {
export function applyTheme(theme) {
if (theme.sidebarBg) {
- changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
+ changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
}
if (theme.sidebarText) {
- changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
- changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
+ changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + theme.sidebarText, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
}
if (theme.sidebarUnreadText) {
- changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
+ changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2);
}
if (theme.sidebarTextHoverBg) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
if (theme.sidebarTextHoverColor) {
- changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'color:' + theme.sidebarTextHoverColor, 2);
+ changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'color:' + theme.sidebarTextHoverColor, 2);
}
if (theme.sidebarTextActiveBg) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1);
}
if (theme.sidebarTextActiveColor) {
- changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
+ changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
}
if (theme.sidebarHeaderBg) {
@@ -607,41 +462,51 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1);
- changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
- changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
+ changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
+ changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1);
+ changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
+ changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
+ changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 1);
changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
- changeCss('.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'background: transparent; color:' + theme.centerChannelColor, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
- changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
- changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
+ changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
}
@@ -652,7 +517,7 @@ export function applyTheme(theme) {
}
if (theme.linkColor) {
- changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
+ changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1);
changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
}
@@ -789,16 +654,12 @@ export function isValidUsername(name) {
return error;
}
-export function updateTabTitle(name) {
- document.title = name + ' ' + document.title.substring(document.title.lastIndexOf('-'));
-}
-
export function updateAddressBar(channelName) {
var teamURL = window.location.href.split('/channels')[0];
history.replaceState('data', '', teamURL + '/channels/' + channelName);
}
-export function switchChannel(channel, teammateName) {
+export function switchChannel(channel) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: channel.name,
@@ -807,12 +668,6 @@ export function switchChannel(channel, teammateName) {
updateAddressBar(channel.name);
- if (channel.type === 'D' && teammateName) {
- updateTabTitle(teammateName);
- } else {
- updateTabTitle(channel.display_name);
- }
-
AsyncClient.getChannels(true, true, true);
AsyncClient.getChannelExtraInfo(true);
AsyncClient.getPosts(channel.id);
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 11794a269..5037da415 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -144,7 +144,6 @@
}
.popover {
border-radius: 3px;
- border: 1px solid #ccc;
width: 100%;
font-size: 0.95em;
}
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 87d9b8200..fa465ff91 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -42,6 +42,21 @@ body {
color: $primary-color;
}
}
+ .popover-title {
+ background: rgba(black, 0.1);
+ }
+}
+
+.dropdown-menu {
+ .divider {
+ @include opacity(0.15);
+ }
+ > li > a {
+ color: inherit;
+ &:focus, &:hover {
+ color: inherit;
+ }
+ }
}
.word-break--all {
@@ -68,6 +83,19 @@ a:focus, a:hover {
margin: 0;
}
+.text-danger {
+ color: #E05F5D;
+}
+
+.btn {
+ &.btn-danger {
+ color: #fff;
+ &:hover, &:active {
+ color: #fff;
+ }
+ }
+}
+
.form-control {
@include border-radius(2px);
&.no-resize {
diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss
index 44eb9b8df..f1aa4dca2 100644
--- a/web/sass-files/sass/partials/_command-box.scss
+++ b/web/sass-files/sass/partials/_command-box.scss
@@ -4,20 +4,29 @@
width: 100%;
border: $border-gray;
bottom: 38px;
+ overflow: auto;
@extend %popover-box-shadow;
+ .sidebar--right & {
+ bottom: 100px;
+ }
}
.command-name {
position: relative;
width: 100%;
- background-color: #fff;
- height: 37px;
- line-height: 37px;
- padding: 2px 10px 2px 5px;
+ line-height: 24px;
+ padding: 5px 10px 8px;
z-index: 101;
+ font-size: 0.95em;
+ border-bottom: 1px solid #ddd;
&:hover {
background-color: #e8eaed;
}
+ .command__desc {
+ margin-left: 5px;
+ @include opacity(0.5);
+ line-height: normal;
+ }
}
.command-desc {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index b5fcb6145..9b9e5f573 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -114,23 +114,6 @@
}
}
}
- &.theme--black {
- &:hover {
- &:before {
- background: rgba(white, 0.2);
- }
- }
- }
- &.theme--gray {
- &:hover {
- &:before {
- background: rgba(white, 0.1);
- }
- }
- }
- a {
- color: #fff;
- }
.navbar-right {
font-size: 0.85em;
position: absolute;
@@ -145,7 +128,6 @@
.dropdown-menu {
li a {
padding: 3px 20px;
- color: #555;
text-overflow: ellipsis;
overflow: hidden;
}
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index 48746ba01..aff31e418 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -33,13 +33,6 @@
line-height: 36px;
font-size: 13px;
cursor: pointer;
- &:hover {
- background-color: #E6F2FA;
- }
-}
-
-.mentions-focus {
- background-color: #E6F2FA;
}
.mentions-text {
@@ -54,7 +47,6 @@
display: block;
font-size: 20px;
text-align: center;
- color: #555;
@include border-radius(32px);
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index e4e8b20b6..96b26f251 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -9,12 +9,6 @@
@include opacity(0.7);
}
}
- a, a:focus, a:hover {
- color: #2389D7;
- &.text-danger {
- color: #a94442;
- }
- }
.custom-textarea {
color: inherit;
border-color: #ccc;
@@ -25,12 +19,9 @@
}
.btn {
font-size: 13px;
- &.btn-primary {
- background: #4285f4;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- color: #fff;
- }
+ &.btn-default {
+ border: none;
+ background: transparent;
}
}
.info__label {
@@ -70,7 +61,7 @@
background: $primary-color;
color: #FFF;
padding: 15px 15px 11px;
- border: none;
+ border: 1px solid #ddd;
min-height: 56px;
@include clearfix;
.modal-title {
diff --git a/web/sass-files/sass/partials/_oauth.scss b/web/sass-files/sass/partials/_oauth.scss
new file mode 100644
index 000000000..35d3a95e3
--- /dev/null
+++ b/web/sass-files/sass/partials/_oauth.scss
@@ -0,0 +1,31 @@
+.oauth-prompt {
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 1em 2em 0;
+ margin: 50px auto;
+ max-width: 90%;
+ width: 600px;
+ .prompt__heading {
+ font-size: em(20px);
+ line-height: normal;
+ margin: 1em 0;
+ display: table;
+ width: 100%;
+ > div {
+ display: table-cell;
+ vertical-align: top;
+ }
+ img {
+ margin-right: 15px;
+ }
+ }
+ .prompt__allow {
+ margin: 1em 0;
+ font-size: em(24px);
+ }
+ .prompt__buttons {
+ text-align: right;
+ border-top: 1px solid #ddd;
+ padding: 1.5em 0;
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 7532875d6..8bf4b0534 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -509,3 +509,11 @@ body.ios {
}
}
}
+
+.bot-indicator {
+ background-color: lightgrey;
+ border-radius:2px;
+ padding-left:2px;
+ padding-right:2px;
+ font-family:"Courier New"
+}
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 9e8d0dc7d..82ec1811a 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -402,12 +402,6 @@
margin-left: 7px;
}
}
- &.active, &:hover {
- a {
- color: #555;
- background: #fff;
- }
- }
}
}
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 3aab05d70..1f785f63c 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -2,7 +2,6 @@
@import "activity-log";
.user-settings {
- background: #fff;
min-height:300px;
.table-responsive {
max-width: 560px;
@@ -71,7 +70,7 @@
}
.section-max {
- background: #f2f2f2;
+ background: rgba(black, 0.05);
padding: 1em 0 1.3em;
margin-bottom: 0;
@include clearfix;
@@ -121,7 +120,7 @@
.fa {
margin-right: 7px;
font-size: 12px;
- color: #aaa;
+ @include opacity(0.5);
display: none;
}
}
@@ -131,7 +130,7 @@
}
.section-describe {
- color:grey;
+ @include opacity(0.7);
}
.divider-dark {
@@ -167,15 +166,11 @@
}
}
.control-label {
- color: #555;
font-weight: 600;
&.text-left {
text-align: left;
}
}
- hr {
- border-color: #ccc;
- }
}
.file-status {
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 6a418e270..73d702fef 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -16,12 +16,6 @@
overflow-y: auto;
max-width: 200px;
width: 200px;
- a {
- color: #262626 !important;
- &:hover, &:focus {
- background: #f5f5f5 !important;
- }
- }
}
.search__form {
margin: 0;
@@ -75,7 +69,7 @@
top: 66px;
}
.nav-pills__unread-indicator-bottom {
- bottom: 0px;
+ bottom: 10px;
}
.nav {
@@ -98,7 +92,6 @@
padding: 3px 10px 3px 25px;
line-height: 1.5;
border-radius: 0;
- color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -109,12 +102,8 @@
text-decoration: underline;
}
&.unread-title {
- color: #333;
font-weight: 600;
}
- &:hover, &:focus {
- background: #e6f2fa;
- }
}
&.active {
a, a:hover, a:focus {
diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss
index 4366b1a6c..6f4a0cc38 100644
--- a/web/sass-files/sass/partials/_sidebar--menu.scss
+++ b/web/sass-files/sass/partials/_sidebar--menu.scss
@@ -54,7 +54,7 @@
> a {
font-size: 15px;
background: none !important;
- color: #444;
+ color: inherit;
line-height: 40px;
padding: 0 15px;
.glyphicon {
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index e704536a8..c614052da 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -41,3 +41,6 @@
// Responsive Css
@import "partials/responsive";
+
+// Standalone Css
+@import "partials/oauth";
diff --git a/web/static/images/themes/mattermost dark.png b/web/static/images/themes/mattermost dark.png
index 832f64d2e..1ab44e104 100644
--- a/web/static/images/themes/mattermost dark.png
+++ b/web/static/images/themes/mattermost dark.png
Binary files differ
diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png
index 4a321adcb..8dea37ff6 100644
--- a/web/static/images/themes/mattermost.png
+++ b/web/static/images/themes/mattermost.png
Binary files differ
diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png
index 1a38bfb34..3066cca0b 100644
--- a/web/static/images/themes/organization.png
+++ b/web/static/images/themes/organization.png
Binary files differ
diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png
deleted file mode 100644
index dc70c7dc2..000000000
--- a/web/static/images/themes/slack.png
+++ /dev/null
Binary files differ
diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png
index d65304820..1f665d882 100644
--- a/web/static/images/themes/windows dark.png
+++ b/web/static/images/themes/windows dark.png
Binary files differ
diff --git a/web/templates/authorize.html b/web/templates/authorize.html
index 3392c1b1e..b0fa3e475 100644
--- a/web/templates/authorize.html
+++ b/web/templates/authorize.html
@@ -1,26 +1,26 @@
{{define "authorize"}}
<html>
{{template "head" . }}
-<body class="white">
- <div class="container-fluid">
- <div class="inner__wrap">
- <div class="row content">
- <div class="signup-header">
- {{.Props.TeamName}}
- </div>
- <div class="col-sm-12">
- <div id="authorize"></div>
- </div>
- <div class="footer-push"></div>
- </div>
- <div class="row footer">
- {{template "footer" . }}
- </div>
- </div>
- </div>
- <script>
- window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' );
- </script>
+<body>
+ <div class="container-fluid">
+ <div class="oauth-prompt">
+ <div class="prompt__heading">
+ <div class="prompt__app-icon">
+ <img src="/static/images/icon50x50.gif" width="50" height="50" alt="">
+ </div>
+ <div class="text">An application would like to connect to your {{.Props.TeamName}} account.</div>
+ </div>
+ <p>The app <strong>{{.Props.AppName}}</strong> would like the ability to access Mattermost on your behalf.</p>
+ <h2 class="prompt__allow">Allow <strong>{{.Props.AppName}}</strong> access?</h2>
+ <div class="prompt__buttons">
+ <input type="button" class="btn btn-link" value="Deny">
+ <input type="button" class="btn btn-primary" value="Allow">
+ </div>
+ </div>
+ </div>
+ <script>
+ window.setup_authorize_page('{{.Props}}');
+ </script>
</body>
</html>
{{end}}
diff --git a/web/templates/head.html b/web/templates/head.html
index 2b83119d8..faac4975a 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -43,13 +43,32 @@
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
+
+ <script>
+ window.onerror = function(msg, url, line, column, stack) {
+ var l = {};
+ l.level = 'ERROR';
+ l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url;
+
+ $.ajax({
+ url: '/api/v1/admin/log_client',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(l)
+ });
+ }
+ </script>
+
<script src="/static/js/bundle.js"></script>
+
<script type="text/javascript">
if (self === top) {
var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker);
}
</script>
+
<script type="text/javascript">
if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") {
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
diff --git a/web/web.go b/web/web.go
index da7eff13d..a1bbf5a81 100644
--- a/web/web.go
+++ b/web/web.go
@@ -189,9 +189,40 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
+ // We still might be able to switch to this team because we've logged in before
+ if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil {
+ multiToken := multiCookie.Value
+
+ if len(multiToken) > 0 {
+ tokens := strings.Split(multiToken, " ")
+
+ for _, token := range tokens {
+ if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil {
+ s := sr.Data.(*model.Session)
+
+ if !s.IsExpired() && s.TeamId == team.Id {
+ w.Header().Set(model.HEADER_TOKEN, s.Token)
+ sessionCookie := &http.Cookie{
+ Name: model.SESSION_TOKEN,
+ Value: s.Token,
+ Path: "/",
+ MaxAge: model.SESSION_TIME_WEB_IN_SECS,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, sessionCookie)
+
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect)
+ return
+ }
+ }
+ }
+ }
+ }
+
page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName
- page.Props["TeamName"] = teamName
+ page.Props["TeamName"] = team.Name
page.Render(c, w)
}
@@ -288,6 +319,10 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) {
func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
name := params["channelname"]
+ teamName := params["team"]
+
+ var team *model.Team
+ teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
var channelId string
if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil {
@@ -297,6 +332,19 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelId = result.Data.(string)
}
+ if tResult := <-teamChan; tResult.Err != nil {
+ c.Err = tResult.Err
+ return
+ } else {
+ team = tResult.Data.(*model.Team)
+ }
+
+ if team.Name != teamName {
+ l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName)
+ http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound)
+ return
+ }
+
if len(channelId) == 0 {
if strings.Index(name, "__") > 0 {
// It's a direct message channel that doesn't exist yet so let's create it
@@ -319,7 +367,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
// lets make sure the user is valid
if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
- c.RemoveSessionCookie(w)
+ c.RemoveSessionCookie(w, r)
l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId)
return
}
@@ -332,18 +380,10 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
}
- var team *model.Team
-
- if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil {
- c.Err = tResult.Err
- return
- } else {
- team = tResult.Data.(*model.Team)
- }
-
page := NewHtmlTemplatePage("channel", "")
page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = team.DisplayName
+ page.Props["TeamName"] = team.Name
page.Props["TeamType"] = team.Type
page.Props["TeamId"] = team.Id
page.Props["ChannelName"] = name
@@ -451,6 +491,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("password_reset", "")
page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"]
page.Props["TeamDisplayName"] = teamDisplayName
+ page.Props["TeamName"] = teamName
page.Props["Hash"] = hash
page.Props["Data"] = data
page.Props["TeamName"] = teamName
@@ -843,6 +884,12 @@ func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("incomingWebhook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
params := mux.Vars(r)
id := params["id"]
@@ -850,7 +897,12 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
- props := model.MapFromJson(strings.NewReader(r.FormValue("payload")))
+ var props map[string]string
+ if r.Header.Get("Content-Type") == "application/json" {
+ props = model.MapFromJson(r.Body)
+ } else {
+ props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
+ }
text := props["text"]
if len(text) == 0 {
@@ -860,6 +912,9 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"]
+ overrideUsername := props["username"]
+ overrideIconUrl := props["icon_url"]
+
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message)
@@ -905,6 +960,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
+ post.AddProp("from_webhook", "true")
+
+ if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ post.AddProp("override_username", overrideUsername)
+ }
+
+ if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ }
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")