summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/about_build_modal.jsx8
-rw-r--r--web/react/components/admin_console/admin_controller.jsx3
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx2
-rw-r--r--web/react/components/admin_console/ldap_settings.jsx394
-rw-r--r--web/react/components/admin_console/rate_settings.jsx2
-rw-r--r--web/react/components/login.jsx146
-rw-r--r--web/react/components/login_email.jsx137
-rw-r--r--web/react/components/login_ldap.jsx110
-rw-r--r--web/react/components/rhs_root_post.jsx1
-rw-r--r--web/react/components/settings_sidebar.jsx7
-rw-r--r--web/react/components/sidebar.jsx10
-rw-r--r--web/react/components/sidebar_right.jsx19
-rw-r--r--web/react/components/signup_team.jsx11
-rw-r--r--web/react/components/signup_user_complete.jsx14
-rw-r--r--web/react/components/suggestion/search_channel_provider.jsx2
-rw-r--r--web/react/components/suggestion/search_user_provider.jsx2
-rw-r--r--web/react/components/team_signup_choose_auth.jsx18
-rw-r--r--web/react/components/team_signup_with_sso.jsx12
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx2
-rw-r--r--web/react/components/view_image.jsx356
-rw-r--r--web/react/stores/file_store.jsx60
-rw-r--r--web/react/utils/async_client.jsx28
-rw-r--r--web/react/utils/client.jsx26
-rw-r--r--web/react/utils/constants.jsx2
-rw-r--r--web/react/utils/emoticons.jsx42
-rw-r--r--web/react/utils/utils.jsx10
26 files changed, 1063 insertions, 361 deletions
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx
index 6962876d4..f71e1c9ab 100644
--- a/web/react/components/about_build_modal.jsx
+++ b/web/react/components/about_build_modal.jsx
@@ -33,10 +33,14 @@ export default class AboutBuildModal extends React.Component {
<div className='col-sm-3 info__label'>{'Build Date:'}</div>
<div className='col-sm-9'>{config.BuildDate}</div>
</div>
- <div className='row'>
+ <div className='row form-group'>
<div className='col-sm-3 info__label'>{'Build Hash:'}</div>
<div className='col-sm-9'>{config.BuildHash}</div>
</div>
+ <div className='row'>
+ <div className='col-sm-3 info__label'>{'Enterprise Ready:'}</div>
+ <div className='col-sm-9'>{config.BuildEnterpriseReady}</div>
+ </div>
</Modal.Body>
<Modal.Footer>
<button
@@ -59,4 +63,4 @@ AboutBuildModal.defaultProps = {
AboutBuildModal.propTypes = {
show: React.PropTypes.bool.isRequired,
onModalDismissed: React.PropTypes.func.isRequired
-}; \ No newline at end of file
+};
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index e587c4f84..32b2e9bb7 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -21,6 +21,7 @@ import ServiceSettingsTab from './service_settings.jsx';
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx';
+import LdapSettingsTab from './ldap_settings.jsx';
export default class AdminController extends React.Component {
constructor(props) {
@@ -151,6 +152,8 @@ export default class AdminController extends React.Component {
tab = <ServiceSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'legal_and_support_settings') {
tab = <LegalAndSupportSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'ldap_settings') {
+ tab = <LdapSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_users') {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index da445da37..1279f4d22 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -334,4 +334,4 @@ AdminSidebar.propTypes = {
selected: React.PropTypes.string,
selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
-}; \ No newline at end of file
+};
diff --git a/web/react/components/admin_console/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx
new file mode 100644
index 000000000..6e3da2f72
--- /dev/null
+++ b/web/react/components/admin_console/ldap_settings.jsx
@@ -0,0 +1,394 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+
+const DEFAULT_LDAP_PORT = 389;
+const DEFAULT_QUERY_TIMEOUT = 60;
+
+export default class LdapSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleEnable = this.handleEnable.bind(this);
+ this.handleDisable = this.handleDisable.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null,
+ enable: this.props.config.LdapSettings.Enable
+ };
+ }
+ handleChange() {
+ this.setState({saveNeeded: true});
+ }
+ handleEnable() {
+ this.setState({saveNeeded: true, enable: true});
+ }
+ handleDisable() {
+ this.setState({saveNeeded: true, enable: false});
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ const config = this.props.config;
+ config.LdapSettings.Enable = this.refs.Enable.checked;
+ config.LdapSettings.LdapServer = this.refs.LdapServer.value.trim();
+
+ let LdapPort = DEFAULT_LDAP_PORT;
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10))) {
+ LdapPort = parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10);
+ }
+ config.LdapSettings.LdapPort = LdapPort;
+
+ config.LdapSettings.BaseDN = this.refs.BaseDN.value.trim();
+ config.LdapSettings.BindUsername = this.refs.BindUsername.value.trim();
+ config.LdapSettings.BindPassword = this.refs.BindPassword.value.trim();
+ config.LdapSettings.FirstNameAttribute = this.refs.FirstNameAttribute.value.trim();
+ config.LdapSettings.LastNameAttribute = this.refs.LastNameAttribute.value.trim();
+ config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim();
+ config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim();
+ config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim();
+
+ let QueryTimeout = DEFAULT_QUERY_TIMEOUT;
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) {
+ QueryTimeout = parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10);
+ }
+ config.LdapSettings.QueryTimeout = QueryTimeout;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+ render() {
+ let serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ let saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'}</p>
+ </div>
+ </div>
+ <h3>{'LDAP Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Enable'
+ >
+ {'Enable Login With LDAP:'}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='true'
+ ref='Enable'
+ defaultChecked={this.props.config.LdapSettings.Enable}
+ onChange={this.handleEnable}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='false'
+ defaultChecked={!this.props.config.LdapSettings.Enable}
+ onChange={this.handleDisable}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, Mattermost allows login using LDAP'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='LdapServer'
+ >
+ {'LDAP Server:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='LdapServer'
+ ref='LdapServer'
+ placeholder='Ex "10.0.0.23"'
+ defaultValue={this.props.config.LdapSettings.LdapServer}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The domain or IP address of LDAP server.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='LdapPort'
+ >
+ {'LDAP Port:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='number'
+ className='form-control'
+ id='LdapPort'
+ ref='LdapPort'
+ placeholder='Ex "389"'
+ defaultValue={this.props.config.LdapSettings.LdapPort}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The port Mattermost will use to connect to the LDAP server. Default is 389.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='BaseDN'
+ >
+ {'BaseDN:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='BaseDN'
+ ref='BaseDN'
+ placeholder='Ex "dc=mydomain,dc=com"'
+ defaultValue={this.props.config.LdapSettings.BaseDN}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='BindUsername'
+ >
+ {'Bind Username:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='BindUsername'
+ ref='BindUsername'
+ placeholder=''
+ defaultValue={this.props.config.LdapSettings.BindUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='BindPassword'
+ >
+ {'Bind Password:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='password'
+ className='form-control'
+ id='BindPassword'
+ ref='BindPassword'
+ placeholder=''
+ defaultValue={this.props.config.LdapSettings.BindPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'Password of the user given in "Bind Username".'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='FirstNameAttribute'
+ >
+ {'First Name Attrubute'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='FirstNameAttribute'
+ ref='FirstNameAttribute'
+ placeholder='Ex "givenName"'
+ defaultValue={this.props.config.LdapSettings.FirstNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='LastNameAttribute'
+ >
+ {'Last Name Attribute:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='LastNameAttribute'
+ ref='LastNameAttribute'
+ placeholder='Ex "sn"'
+ defaultValue={this.props.config.LdapSettings.LastNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EmailAttribute'
+ >
+ {'Email Attribute:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='EmailAttribute'
+ ref='EmailAttribute'
+ placeholder='Ex "mail"'
+ defaultValue={this.props.config.LdapSettings.EmailAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='UsernameAttribute'
+ >
+ {'Username Attribute:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='UsernameAttribute'
+ ref='UsernameAttribute'
+ placeholder='Ex "sAMAccountName"'
+ defaultValue={this.props.config.LdapSettings.UsernameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='IdAttribute'
+ >
+ {'Id Attribute: '}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='IdAttribute'
+ ref='IdAttribute'
+ placeholder='Ex "sAMAccountName"'
+ defaultValue={this.props.config.LdapSettings.IdAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='QueryTimeout'
+ >
+ {'Query Timeout (seconds):'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='number'
+ className='form-control'
+ id='QueryTimeout'
+ ref='QueryTimeout'
+ placeholder='Ex "60"'
+ defaultValue={this.props.config.LdapSettings.QueryTimeout}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>{'The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
+LdapSettings.defaultProps = {
+};
+
+LdapSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
index ca9fcb074..aabb24326 100644
--- a/web/react/components/admin_console/rate_settings.jsx
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -241,7 +241,7 @@ export default class RateSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
/>
- <p className='help-text'>{'When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring Ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</p>
+ <p className='help-text'>{'When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</p>
</div>
</div>
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index d87bd20ad..9afaa8b0d 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -2,97 +2,19 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
-import * as Client from '../utils/client.jsx';
-import UserStore from '../stores/user_store.jsx';
-import BrowserStore from '../stores/browser_store.jsx';
+import LoginEmail from './login_email.jsx';
+import LoginLdap from './login_ldap.jsx';
export default class Login extends React.Component {
constructor(props) {
super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
-
this.state = {};
}
- handleSubmit(e) {
- e.preventDefault();
- var state = {};
-
- const name = this.props.teamName;
- if (!name) {
- state.serverError = 'Bad team name';
- this.setState(state);
- return;
- }
-
- const email = ReactDOM.findDOMNode(this.refs.email).value.trim();
- if (!email) {
- state.serverError = 'An email is required';
- this.setState(state);
- return;
- }
-
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
- if (!password) {
- state.serverError = 'A password is required';
- this.setState(state);
- return;
- }
-
- if (!BrowserStore.isLocalStorageSupported()) {
- state.serverError = 'This service requires local storage to be enabled. Please enable it or exit private browsing.';
- this.setState(state);
- return;
- }
-
- state.serverError = '';
- this.setState(state);
-
- Client.loginByEmail(name, email, password,
- () => {
- UserStore.setLastEmail(email);
-
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- window.location.href = decodeURIComponent(redirect);
- } else {
- window.location.href = '/' + name + '/channels/town-square';
- }
- },
- (err) => {
- if (err.message === 'Login failed because email address has not been verified') {
- window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email);
- return;
- }
- state.serverError = err.message;
- this.valid = false;
- this.setState(state);
- }
- );
- }
render() {
- let serverError;
- if (this.state.serverError) {
- serverError = <label className='control-label'>{this.state.serverError}</label>;
- }
- let priorEmail = UserStore.getLastEmail();
-
- const emailParam = Utils.getUrlParameter('email');
- if (emailParam) {
- priorEmail = decodeURIComponent(emailParam);
- }
-
const teamDisplayName = this.props.teamDisplayName;
const teamName = this.props.teamName;
- let focusEmail = false;
- let focusPassword = false;
- if (priorEmail === '') {
- focusEmail = true;
- } else {
- focusPassword = true;
- }
-
let loginMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
@@ -106,9 +28,16 @@ export default class Login extends React.Component {
);
}
- let errorClass = '';
- if (serverError) {
- errorClass = ' has-error';
+ if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
+ loginMessage.push(
+ <a
+ className='btn btn-custom-login google'
+ href={'/' + teamName + '/login/google'}
+ >
+ <span className='icon' />
+ <span>{'with Google Apps'}</span>
+ </a>
+ );
}
const verifiedParam = Utils.getUrlParameter('verified');
@@ -125,39 +54,9 @@ export default class Login extends React.Component {
let emailSignup;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
- <div className='signup__email-container'>
- <div className={'form-group' + errorClass}>
- <input
- autoFocus={focusEmail}
- type='email'
- className='form-control'
- name='email'
- defaultValue={priorEmail}
- ref='email'
- placeholder='Email'
- spellCheck='false'
- />
- </div>
- <div className={'form-group' + errorClass}>
- <input
- autoFocus={focusPassword}
- type='password'
- className='form-control'
- name='password'
- ref='password'
- placeholder='Password'
- spellCheck='false'
- />
- </div>
- <div className='form-group'>
- <button
- type='submit'
- className='btn btn-primary'
- >
- {'Sign in'}
- </button>
- </div>
- </div>
+ <LoginEmail
+ teamName={this.props.teamName}
+ />
);
}
@@ -211,25 +110,30 @@ export default class Login extends React.Component {
);
}
+ let ldapLogin = null;
+ if (global.window.mm_config.EnableLdap === 'true') {
+ ldapLogin = (
+ <LoginLdap
+ teamName={this.props.teamName}
+ />
+ );
+ }
+
return (
<div className='signup-team__container'>
<h5 className='margin--less'>{'Sign in to:'}</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2>
- <form onSubmit={this.handleSubmit}>
{verifiedBox}
- <div className={'form-group' + errorClass}>
- {serverError}
- </div>
{loginMessage}
{emailSignup}
+ {ldapLogin}
{userSignUp}
<div className='form-group margin--extra form-group--small'>
<span><a href='/find_team'>{'Find your other teams'}</a></span>
</div>
{forgotPassword}
{teamSignUp}
- </form>
</div>
);
}
diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx
new file mode 100644
index 000000000..cfe34d1c7
--- /dev/null
+++ b/web/react/components/login_email.jsx
@@ -0,0 +1,137 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+
+export default class LoginEmail extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ var state = {};
+
+ const name = this.props.teamName;
+ if (!name) {
+ state.serverError = 'Bad team name';
+ this.setState(state);
+ return;
+ }
+
+ const email = this.refs.email.value.trim();
+ if (!email) {
+ state.serverError = 'An email is required';
+ this.setState(state);
+ return;
+ }
+
+ const password = this.refs.password.value.trim();
+ if (!password) {
+ state.serverError = 'A password is required';
+ this.setState(state);
+ return;
+ }
+
+ state.serverError = '';
+ this.setState(state);
+
+ Client.loginByEmail(name, email, password,
+ () => {
+ UserStore.setLastEmail(email);
+
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ window.location.href = decodeURIComponent(redirect);
+ } else {
+ window.location.href = '/' + name + '/channels/town-square';
+ }
+ },
+ (err) => {
+ if (err.message === 'Login failed because email address has not been verified') {
+ window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email);
+ return;
+ }
+ state.serverError = err.message;
+ this.valid = false;
+ this.setState(state);
+ }
+ );
+ }
+ render() {
+ let serverError;
+ let errorClass = '';
+ if (this.state.serverError) {
+ serverError = <label className='control-label'>{this.state.serverError}</label>;
+ errorClass = ' has-error';
+ }
+
+ let priorEmail = UserStore.getLastEmail();
+ let focusEmail = false;
+ let focusPassword = false;
+ if (priorEmail === '') {
+ focusEmail = true;
+ } else {
+ focusPassword = true;
+ }
+
+ const emailParam = Utils.getUrlParameter('email');
+ if (emailParam) {
+ priorEmail = decodeURIComponent(emailParam);
+ }
+
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <div className='signup__email-container'>
+ <div className={'form-group' + errorClass}>
+ {serverError}
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ autoFocus={focusEmail}
+ type='email'
+ className='form-control'
+ name='email'
+ defaultValue={priorEmail}
+ ref='email'
+ placeholder='Email'
+ spellCheck='false'
+ />
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ autoFocus={focusPassword}
+ type='password'
+ className='form-control'
+ name='password'
+ ref='password'
+ placeholder='Password'
+ spellCheck='false'
+ />
+ </div>
+ <div className='form-group'>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ {'Sign in'}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
+LoginEmail.defaultProps = {
+};
+
+LoginEmail.propTypes = {
+ teamName: React.PropTypes.string.isRequired
+};
diff --git a/web/react/components/login_ldap.jsx b/web/react/components/login_ldap.jsx
new file mode 100644
index 000000000..1e0e32f4f
--- /dev/null
+++ b/web/react/components/login_ldap.jsx
@@ -0,0 +1,110 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+
+export default class LoginLdap extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ var state = {};
+
+ const teamName = this.props.teamName;
+ if (!teamName) {
+ state.serverError = 'Bad team name';
+ this.setState(state);
+ return;
+ }
+
+ const id = this.refs.id.value.trim();
+ if (!id) {
+ state.serverError = 'An LDAP ID is required';
+ this.setState(state);
+ return;
+ }
+
+ const password = this.refs.password.value.trim();
+ if (!password) {
+ state.serverError = 'An LDAP password is required';
+ this.setState(state);
+ return;
+ }
+
+ state.serverError = '';
+ this.setState(state);
+
+ Client.loginByLdap(teamName, id, password,
+ () => {
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ window.location.href = decodeURIComponent(redirect);
+ } else {
+ window.location.href = '/' + teamName + '/channels/town-square';
+ }
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+ render() {
+ let serverError;
+ let errorClass = '';
+ if (this.state.serverError) {
+ serverError = <label className='control-label'>{this.state.serverError}</label>;
+ errorClass = ' has-error';
+ }
+
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <div className='signup__email-container'>
+ <div className={'form-group' + errorClass}>
+ {serverError}
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ autoFocus={true}
+ className='form-control'
+ ref='id'
+ placeholder='LDAP Username'
+ spellCheck='false'
+ />
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ type='password'
+ className='form-control'
+ ref='password'
+ placeholder='LDAP Password'
+ spellCheck='false'
+ />
+ </div>
+ <div className='form-group'>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ {'Sign in'}
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
+LoginLdap.defaultProps = {
+};
+
+LoginLdap.propTypes = {
+ teamName: React.PropTypes.string.isRequired
+};
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index dd9a793be..cd7f6766c 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -227,7 +227,6 @@ export default class RhsRootPost extends React.Component {
</div>
</div>
</div>
- <hr />
</div>
);
}
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index 4af46c35a..271ea9a41 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import * as Utils from '../utils/utils.jsx';
+
export default class SettingsSidebar extends React.Component {
componentDidUpdate() {
$('.settings-modal').find('.modal-body').scrollTop(0);
@@ -16,6 +18,11 @@ export default class SettingsSidebar extends React.Component {
this.props.updateTab(tab.name);
$(e.target).closest('.settings-modal').addClass('display--content');
}
+ componentDidMount() {
+ if (Utils.isBrowserFirefox()) {
+ $('.settings-modal .settings-table .nav').addClass('position--top');
+ }
+ }
render() {
let tabList = this.props.tabs.map(function makeTab(tab) {
let key = `${tab.name}_li`;
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index cc2279b57..18c360cb8 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -39,6 +39,7 @@ export default class Sidebar extends React.Component {
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.showMoreChannelsModal = this.showMoreChannelsModal.bind(this);
this.showNewChannelModal = this.showNewChannelModal.bind(this);
this.hideNewChannelModal = this.hideNewChannelModal.bind(this);
this.showMoreDirectChannelsModal = this.showMoreDirectChannelsModal.bind(this);
@@ -250,6 +251,11 @@ export default class Sidebar extends React.Component {
return a.display_name.localeCompare(b.display_name);
}
+ showMoreChannelsModal() {
+ // manually show the modal because using data-toggle messes with keyboard focus when the modal is dismissed
+ $('#more_channels').modal({'data-channeltype': 'O'}).modal('show');
+ }
+
showNewChannelModal(type) {
this.setState({newChannelModalType: type});
}
@@ -594,10 +600,8 @@ export default class Sidebar extends React.Component {
<li>
<a
href='#'
- data-toggle='modal'
className='nav-more'
- data-target='#more_channels'
- data-channeltype='O'
+ onClick={this.showMoreChannelsModal}
>
{'More...'}
</a>
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index ac1049da0..ee247265d 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -52,14 +52,29 @@ export default class SidebarRight extends React.Component {
doStrangeThings() {
// We should have a better way to do this stuff
// Hence the function name.
+ var windowWidth = $(window).outerWidth();
+ var sidebarRightWidth = $('.sidebar--right').outerWidth();
+
$('.inner__wrap').removeClass('.move--right');
$('.inner__wrap').addClass('move--left');
$('.sidebar--left').removeClass('move--right');
$('.sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
-
- if (!(this.state.search_visible || this.state.post_right_visible)) {
+ if (this.state.search_visible || this.state.post_right_visible) {
+ if (windowWidth > 960) {
+ $('.inner__wrap').velocity({marginRight: sidebarRightWidth}, {duration: 500, easing: 'easeOutSine'});
+ $('.sidebar--right').velocity({translateX: 0}, {duration: 500, easing: 'easeOutSine'});
+ } else {
+ $('.inner__wrap, .sidebar--right').attr('style', '');
+ }
+ } else {
+ if (windowWidth > 960) {
+ $('.inner__wrap').velocity({marginRight: 0}, {duration: 500, easing: 'easeOutSine'});
+ $('.sidebar--right').velocity({translateX: sidebarRightWidth}, {duration: 500, easing: 'easeOutSine'});
+ } else {
+ $('.inner__wrap, .sidebar--right').attr('style', '');
+ }
$('.inner__wrap').removeClass('move--left').removeClass('move--right');
$('.sidebar--right').removeClass('move--left');
return (
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 0ac837326..a554427d5 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -28,6 +28,8 @@ export default class TeamSignUp extends React.Component {
this.state = {page: 'email'};
} else if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
this.state = {page: 'gitlab'};
+ } else {
+ this.state = {page: 'none'};
}
}
@@ -112,6 +114,15 @@ export default class TeamSignUp extends React.Component {
<SSOSignupPage service={Constants.GITLAB_SERVICE} />
</div>
);
+ } else if (this.state.page === 'google') {
+ return (
+ <div>
+ {teamListing}
+ <SSOSignupPage service={Constants.GOOGLE_SERVICE} />
+ </div>
+ );
+ } else if (this.state.page === 'none') {
+ return (<div>{'No team creation method has been enabled. Please contact an administrator for access.'}</div>);
}
}
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 2bde78726..df11fe045 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -183,11 +183,23 @@ export default class SignupUserComplete extends React.Component {
href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search}
>
<span className='icon' />
- <span>with GitLab</span>
+ <span>{'with GitLab'}</span>
</a>
);
}
+ if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
+ signupMessage.push(
+ <a
+ className='btn btn-custom-login google'
+ href={'/' + this.props.teamName + '/signup/google' + window.location.search}
+ >
+ <span className='icon' />
+ <span>{'with Google'}</span>
+ </a>
+ );
+ }
+
var emailSignup;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
diff --git a/web/react/components/suggestion/search_channel_provider.jsx b/web/react/components/suggestion/search_channel_provider.jsx
index 7547a9341..66a534907 100644
--- a/web/react/components/suggestion/search_channel_provider.jsx
+++ b/web/react/components/suggestion/search_channel_provider.jsx
@@ -19,7 +19,7 @@ class SearchChannelSuggestion extends React.Component {
onClick={onClick}
className={className}
>
- {item.name}
+ <i className='fa fa fa-plus-square'></i>{item.name}
</div>
);
}
diff --git a/web/react/components/suggestion/search_user_provider.jsx b/web/react/components/suggestion/search_user_provider.jsx
index cf2953937..0d553bfc4 100644
--- a/web/react/components/suggestion/search_user_provider.jsx
+++ b/web/react/components/suggestion/search_user_provider.jsx
@@ -22,7 +22,7 @@ class SearchUserSuggestion extends React.Component {
className='profile-img rounded'
src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
/>
- {item.username}
+ <i className='fa fa fa-plus-square'></i>{item.username}
</div>
);
}
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index 0254c8b4e..19b9750b3 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -26,6 +26,24 @@ export default class ChooseAuthPage extends React.Component {
);
}
+ if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
+ buttons.push(
+ <a
+ className='btn btn-custom-login google btn-full'
+ href='#'
+ onClick={
+ (e) => {
+ e.preventDefault();
+ this.props.updatePage('google');
+ }
+ }
+ >
+ <span className='icon' />
+ <span>{'Create new team with Google Apps Account'}</span>
+ </a>
+ );
+ }
+
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
buttons.push(
<a
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index e3f16efb0..f4b323956 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -88,6 +88,18 @@ export default class SSOSignUpPage extends React.Component {
<span>{'Create team with GitLab Account'}</span>
</a>
);
+ } else if (this.props.service === Constants.GOOGLE_SERVICE) {
+ button = (
+ <a
+ className='btn btn-custom-login google btn-full'
+ href='#'
+ onClick={this.handleSubmit}
+ disabled={disabled}
+ >
+ <span className='icon'/>
+ <span>{'Create team with Google Apps Account'}</span>
+ </a>
+ );
}
return (
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index b7d90922a..8ec3863f3 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -104,7 +104,7 @@ export default class CustomThemeChooser extends React.Component {
>
<label className='custom-label'>{element.uiName}</label>
<div
- className='input-group theme-group dropdown'
+ className='input-group theme-group group--code dropdown'
id={element.id}
>
<select
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 820f8fd8e..7edf6283b 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -1,9 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
+import FileStore from '../stores/file_store.jsx';
import ViewImagePopoverBar from './view_image_popover_bar.jsx';
const Modal = ReactBootstrap.Modal;
const KeyCodes = Constants.KeyCodes;
@@ -12,80 +14,90 @@ export default class ViewImageModal extends React.Component {
constructor(props) {
super(props);
- this.canSetState = false;
-
+ this.showImage = this.showImage.bind(this);
this.loadImage = this.loadImage.bind(this);
+
this.handleNext = this.handleNext.bind(this);
this.handlePrev = this.handlePrev.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
- this.getPublicLink = this.getPublicLink.bind(this);
- this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
+
this.onModalShown = this.onModalShown.bind(this);
this.onModalHidden = this.onModalHidden.bind(this);
+
+ this.onFileStoreChange = this.onFileStoreChange.bind(this);
+
+ this.getPublicLink = this.getPublicLink.bind(this);
+ this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
- var loaded = [];
- var progress = [];
+ const loaded = [];
+ const progress = [];
for (var i = 0; i < this.props.filenames.length; i++) {
loaded.push(false);
progress.push(0);
}
+
this.state = {
imgId: this.props.startId,
+ fileInfo: new Map(),
imgHeight: '100%',
- loaded: loaded,
- progress: progress,
- images: {},
- fileSizes: {},
- fileMimes: {},
- showFooter: false,
- isPlaying: {},
- isLoading: {}
+ loaded,
+ progress,
+ showFooter: false
};
}
+
handleNext(e) {
if (e) {
e.stopPropagation();
}
- var id = this.state.imgId + 1;
+ let id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) {
id = 0;
}
- this.setState({imgId: id});
- this.loadImage(id);
+ this.showImage(id);
}
+
handlePrev(e) {
if (e) {
e.stopPropagation();
}
- var id = this.state.imgId - 1;
+ let id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length - 1;
}
- this.setState({imgId: id});
- this.loadImage(id);
+ this.showImage(id);
}
+
handleKeyPress(e) {
- if (!e || !this.props.show) {
- return;
- } else if (e.keyCode === KeyCodes.RIGHT) {
+ if (e.keyCode === KeyCodes.RIGHT) {
this.handleNext();
} else if (e.keyCode === KeyCodes.LEFT) {
this.handlePrev();
}
}
+
onModalShown(nextProps) {
- this.setState({imgId: nextProps.startId});
- this.loadImage(nextProps.startId);
+ $(window).on('keyup', this.handleKeyPress);
+
+ this.showImage(nextProps.startId);
+
+ FileStore.addChangeListener(this.onFileStoreChange);
}
+
onModalHidden() {
+ $(window).off('keyup', this.handleKeyPress);
+
if (this.refs.video) {
var video = ReactDOM.findDOMNode(this.refs.video);
video.pause();
video.currentTime = 0;
}
+
+ FileStore.removeChangeListener(this.onFileStoreChange);
}
+
componentWillReceiveProps(nextProps) {
if (nextProps.show === true && this.props.show === false) {
this.onModalShown(nextProps);
@@ -93,31 +105,65 @@ export default class ViewImageModal extends React.Component {
this.onModalHidden();
}
}
- loadImage(id) {
- var imgHeight = $(window).height() - 100;
+
+ onFileStoreChange(filename) {
+ const id = this.props.filenames.indexOf(filename);
+
+ if (id !== -1 && !this.state.loaded[id]) {
+ const fileInfo = this.state.fileInfo;
+ fileInfo.set(filename, FileStore.getInfo(filename));
+ this.setState({fileInfo});
+
+ this.loadImage(id, filename);
+ }
+ }
+
+ showImage(id) {
+ this.setState({imgId: id});
+
+ const imgHeight = $(window).height() - 100;
this.setState({imgHeight});
- var filename = this.props.filenames[id];
+ const filename = this.props.filenames[id];
- var fileInfo = Utils.splitFileLocation(filename);
- var fileType = Utils.getFileType(fileInfo.ext);
+ if (!FileStore.hasInfo(filename)) {
+ // the image will actually be loaded once we know what we need to load
+ AsyncClient.getFileInfo(filename);
+ return;
+ }
+
+ if (!this.state.loaded[id]) {
+ this.loadImage(id, filename);
+ }
+ }
+
+ loadImage(id, filename) {
+ const fileInfo = FileStore.getInfo(filename);
+ const fileType = Utils.getFileType(fileInfo.extension);
if (fileType === 'image') {
- var img = new Image();
- img.load(this.getPreviewImagePath(filename),
- () => {
- const progress = this.state.progress;
- progress[id] = img.completedPercentage;
- this.setState({progress});
- });
+ let previewUrl;
+ if (fileInfo.has_image_preview) {
+ previewUrl = fileInfo.getPreviewImagePath(filename);
+ } else {
+ // some images (eg animated gifs) just show the file itself and not a preview
+ previewUrl = Utils.getFileUrl(filename);
+ }
+
+ const img = new Image();
+ img.load(
+ previewUrl,
+ () => {
+ const progress = this.state.progress;
+ progress[id] = img.completedPercentage;
+ this.setState({progress});
+ }
+ );
img.onload = () => {
const loaded = this.state.loaded;
loaded[id] = true;
this.setState({loaded});
};
- var images = this.state.images;
- images[id] = img;
- this.setState({images});
} else {
// there's nothing to load for non-image files
var loaded = this.state.loaded;
@@ -125,169 +171,82 @@ export default class ViewImageModal extends React.Component {
this.setState({loaded});
}
}
- playGif(e, filename, fileUrl) {
- var isLoading = this.state.isLoading;
- var isPlaying = this.state.isPlaying;
-
- isLoading[filename] = fileUrl;
- this.setState({isLoading});
-
- var img = new Image();
- img.load(fileUrl);
- img.onload = () => {
- delete isLoading[filename];
- isPlaying[filename] = fileUrl;
- this.setState({isPlaying, isLoading});
- };
- img.onError = () => {
- delete isLoading[filename];
- this.setState({isLoading});
- };
-
- e.stopPropagation();
- e.preventDefault();
- }
- stopGif(e, filename) {
- var isPlaying = this.state.isPlaying;
- delete isPlaying[filename];
- this.setState({isPlaying});
-
- e.stopPropagation();
- e.preventDefault();
- }
- componentDidMount() {
- $(window).on('keyup', this.handleKeyPress);
- // keep track of whether or not this component is mounted so we can safely set the state asynchronously
- this.canSetState = true;
- }
- componentWillUnmount() {
- this.canSetState = false;
- $(window).off('keyup', this.handleKeyPress);
- }
getPublicLink() {
var data = {};
data.channel_id = this.props.channelId;
data.user_id = this.props.userId;
data.filename = this.props.filenames[this.state.imgId];
- Client.getPublicLink(data,
- function sucess(serverData) {
+ Client.getPublicLink(
+ data,
+ (serverData) => {
if (Utils.isMobile()) {
window.location.href = serverData.public_link;
} else {
window.open(serverData.public_link);
}
},
- function error() {}
+ () => {}
);
}
+
getPreviewImagePath(filename) {
// Returns the path to a preview image that can be used to represent a file.
var fileInfo = Utils.splitFileLocation(filename);
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
- if (filename in this.state.isPlaying) {
- return this.state.isPlaying[filename];
- }
-
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
}
fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
- return fileInfo.path + '_preview.jpg' + '?' + Utils.getSessionIndex();
+ return fileInfo.path + '_preview.jpg?' + Utils.getSessionIndex();
}
// only images have proper previews, so just use a placeholder icon for non-images
return Utils.getPreviewImagePathForFileType(fileType);
}
+
onMouseEnterImage() {
this.setState({showFooter: true});
}
+
onMouseLeaveImage() {
this.setState({showFooter: false});
}
+
render() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>;
}
- var filename = this.props.filenames[this.state.imgId];
- var fileUrl = Utils.getFileUrl(filename);
-
- var name = decodeURIComponent(Utils.getFileName(filename));
+ const filename = this.props.filenames[this.state.imgId];
+ const fileUrl = Utils.getFileUrl(filename);
var content;
- var bgClass = '';
if (this.state.loaded[this.state.imgId]) {
- var fileInfo = Utils.splitFileLocation(filename);
- var fileType = Utils.getFileType(fileInfo.ext);
+ // if a file has been loaded, we also have its info
+ const fileInfo = this.state.fileInfo.get(filename);
- if (fileType === 'image') {
- if (!(filename in this.state.fileMimes)) {
- Client.getFileInfo(
- filename,
- (data) => {
- if (this.canSetState) {
- var fileMimes = this.state.fileMimes;
- fileMimes[filename] = data.mime;
- this.setState(fileMimes);
- }
- },
- () => {}
- );
- }
+ const extension = Utils.splitFileLocation(filename).ext;
+ const fileType = Utils.getFileType(extension);
- var playbackControls = '';
- if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading)) {
- if (filename in this.state.isPlaying) {
- playbackControls = (
- <div
- className='file-playback-controls stop'
- onClick={(e) => this.stopGif(e, filename)}
- >
- {"■"}
- </div>
- );
- } else {
- playbackControls = (
- <div
- className='file-playback-controls play'
- onClick={(e) => this.playGif(e, filename, fileUrl)}
- >
- {"►"}
- </div>
- );
- }
- }
-
- var loadingIndicator = '';
- if (this.state.isLoading[filename] === fileUrl) {
- loadingIndicator = (
- <img
- className='spinner file__loading'
- src='/static/images/load.gif'
- />
- );
- playbackControls = '';
+ if (fileType === 'image') {
+ let previewUrl;
+ if (fileInfo.has_preview_image) {
+ previewUrl = this.getPreviewImagePath(filename);
+ } else {
+ previewUrl = fileUrl;
}
- // image files just show a preview of the file
content = (
- <a
- href={fileUrl}
- target='_blank'
- >
- {loadingIndicator}
- {playbackControls}
- <img
- style={{maxHeight: this.state.imgHeight}}
- ref='image'
- src={this.getPreviewImagePath(filename)}
- />
- </a>
+ <ImagePreview
+ fileUrl={fileUrl}
+ previewUrl={previewUrl}
+ maxHeight={this.state.imgHeight}
+ />
);
} else if (fileType === 'video' || fileType === 'audio') {
let width = Constants.WEB_VIDEO_WIDTH;
@@ -311,11 +270,13 @@ export default class ViewImageModal extends React.Component {
);
} else {
// non-image files include a section providing details about the file
- var infoString = 'File type ' + fileInfo.ext.toUpperCase();
- if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) {
- infoString += ', Size ' + Utils.fileSizeToString(this.state.fileSizes[filename]);
+ let infoString = 'File type ' + fileInfo.extension.toUpperCase();
+ if (fileInfo.size > 0) {
+ infoString += ', Size ' + Utils.fileSizeToString(fileInfo.size);
}
+ const name = decodeURIComponent(Utils.getFileName(filename));
+
content = (
<div className='file-details__container'>
<a
@@ -335,53 +296,16 @@ export default class ViewImageModal extends React.Component {
</div>
</div>
);
- bgClass = 'white-bg';
-
- // asynchronously request the actual size of this file
- if (!(filename in this.state.fileSizes)) {
- Client.getFileInfo(
- filename,
- function success(data) {
- if (this.canSetState) {
- var fileSizes = this.state.fileSizes;
- fileSizes[filename] = parseInt(data.size, 10);
- this.setState(fileSizes);
- }
- }.bind(this),
- function fail() {}
- );
- }
}
} else {
// display a progress indicator when the preview for an image is still loading
- var percentage = Math.floor(this.state.progress[this.state.imgId]);
- if (percentage) {
- content = (
- <div>
- <img
- className='loader-image'
- src='/static/images/load.gif'
- />
- <span className='loader-percent'>
- {'Previewing ' + percentage + '%'}
- </span>
- </div>
- );
- } else {
- content = (
- <div>
- <img
- className='loader-image'
- src='/static/images/load.gif'
- />
- </div>
- );
- }
- bgClass = 'black-bg';
+ const progress = Math.floor(this.state.progress[this.state.imgId]);
+
+ content = <LoadingImagePreview progress={progress} />;
}
- var leftArrow = '';
- var rightArrow = '';
+ let leftArrow = null;
+ let rightArrow = null;
if (this.props.filenames.length > 1) {
leftArrow = (
<a
@@ -427,7 +351,6 @@ export default class ViewImageModal extends React.Component {
onClick={this.props.onModalDismissed}
>
<div
- className={bgClass}
onMouseEnter={this.onMouseEnterImage}
onMouseLeave={this.onMouseLeaveImage}
onClick={(e) => e.stopPropagation()}
@@ -471,3 +394,38 @@ ViewImageModal.propTypes = {
userId: React.PropTypes.string,
startId: React.PropTypes.number
};
+
+function LoadingImagePreview({progress}) {
+ let progressView = null;
+ if (progress) {
+ progressView = (
+ <span className='loader-percent'>
+ {'Loading ' + progress + '%'}
+ </span>
+ );
+ }
+
+ return (
+ <div className='view-image__loading'>
+ <img
+ className='loader-image'
+ src='/static/images/load.gif'
+ />
+ {progressView}
+ </div>
+ );
+}
+
+function ImagePreview({maxHeight, fileUrl, previewUrl}) {
+ return (
+ <a
+ href={fileUrl}
+ target='_blank'
+ >
+ <img
+ style={{maxHeight}}
+ src={previewUrl}
+ />
+ </a>
+ );
+}
diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx
new file mode 100644
index 000000000..ca8c6a96b
--- /dev/null
+++ b/web/react/stores/file_store.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
+import EventEmitter from 'events';
+
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'changed';
+
+class FileStore extends EventEmitter {
+ constructor() {
+ super();
+
+ this.addChangeListener = this.addChangeListener.bind(this);
+ this.removeChangeListener = this.removeChangeListener.bind(this);
+ this.emitChange = this.emitChange.bind(this);
+
+ this.handleEventPayload = this.handleEventPayload.bind(this);
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+
+ this.fileInfo = new Map();
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+ emitChange(filename) {
+ this.emit(CHANGE_EVENT, filename);
+ }
+
+ hasInfo(filename) {
+ return this.fileInfo.has(filename);
+ }
+
+ getInfo(filename) {
+ return this.fileInfo.get(filename);
+ }
+
+ setInfo(filename, info) {
+ this.fileInfo.set(filename, info);
+ }
+
+ handleEventPayload(payload) {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIEVED_FILE_INFO:
+ this.setInfo(action.filename, action.info);
+ this.emitChange(action.filename);
+ break;
+ }
+ }
+}
+
+export default new FileStore();
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 88b5aa739..f218270da 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -769,3 +769,31 @@ export function getSuggestedCommands(command, suggestionId, component) {
}
);
}
+
+export function getFileInfo(filename) {
+ const callName = 'getFileInfo' + filename;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getFileInfo(
+ filename,
+ (data) => {
+ callTracker[callName] = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_FILE_INFO,
+ filename,
+ info: data
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getFileInfo');
+ }
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index beabf5227..1c417153b 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -243,7 +243,7 @@ export function loginByEmail(name, email, password, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({name: name, email: email, password: password}),
+ data: JSON.stringify({name, email, password}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_login_success', data.team_id, 'email', data.email);
BrowserStore.signalLogin();
@@ -258,6 +258,26 @@ export function loginByEmail(name, email, password, success, error) {
});
}
+export function loginByLdap(teamName, id, password, success, error) {
+ $.ajax({
+ url: '/api/v1/users/login_ldap',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({teamName, id, password}),
+ success: function onSuccess(data, textStatus, xhr) {
+ track('api', 'api_users_loginLdap_success', data.team_id, 'id', id);
+ success(data, textStatus, xhr);
+ },
+ error: function onError(xhr, status, err) {
+ track('api', 'api_users_loginLdap_fail', teamName, 'id', id);
+
+ var e = handleError('loginByLdap', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function revokeSession(altId, success, error) {
$.ajax({
url: '/api/v1/users/revoke_session',
@@ -1056,7 +1076,9 @@ export function getFileInfo(filename, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success,
+ success: (data) => {
+ success(data);
+ },
error: function onError(xhr, status, err) {
var e = handleError('getFileInfo', xhr, status, err);
error(e);
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 5f027a409..ea4921417 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -37,6 +37,7 @@ export default {
RECIEVED_STATUSES: null,
RECIEVED_PREFERENCE: null,
RECIEVED_PREFERENCES: null,
+ RECIEVED_FILE_INFO: null,
RECIEVED_MSG: null,
@@ -114,6 +115,7 @@ export default {
DEFAULT_CHANNEL: 'town-square',
OFFTOPIC_CHANNEL: 'off-topic',
GITLAB_SERVICE: 'gitlab',
+ GOOGLE_SERVICE: 'google',
EMAIL_SERVICE: 'email',
POST_CHUNK_SIZE: 60,
MAX_POST_CHUNKS: 3,
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index fa5177232..23a847969 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -2,21 +2,21 @@
// See License.txt for license information.
const emoticonPatterns = {
- smile: /(^|\s)(:-?\))(?=$|\s)/g, // :)
+ slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :)
wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;)
open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o
scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o
smirk: /(^|\s)(:-?])(?=$|\s)/g, // :]
- grinning: /(^|\s)(:-?d)(?=$|\s)/gi, // :D
+ smile: /(^|\s)(:-?d)(?=$|\s)/gi, // :D
stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d
stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p
rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@
- frowning: /(^|\s)(:-?\()(?=$|\s)/g, // :(
- sob: /(^|\s)(:['’]-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
- pensive: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
+ slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :(
+ cry: /(^|\s)(:['’]-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
+ confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s
- flushed: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
- relaxed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$
+ neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
+ flushed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$
mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x
heart: /(^|\s)(<3|&lt;3)(?=$|\s)/g, // <3
broken_heart: /(^|\s)(<\/3|&lt;&#x2F;3)(?=$|\s)/g, // </3
@@ -34,10 +34,10 @@ function initializeEmoticonMap() {
'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
- 'black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,' +
- 'blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,' +
- 'bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,' +
- 'bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
+ 'black_large_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,' +
+ 'blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,' +
+ 'boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,' +
+ 'busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
@@ -92,16 +92,16 @@ function initializeEmoticonMap() {
'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
- 'ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,' +
- 'small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,' +
- 'snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,' +
- 'sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,' +
- 'statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,' +
- 'stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,' +
- 'sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,' +
- 'tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,' +
- 'three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,' +
- 'tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
+ 'ski,skull,sleeping,sleepy,slightly_smiling_face,slightly_frowning_face,slot_machine,small_blue_diamond,' +
+ 'small_orange_diamond,small_red_triangle,small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,' +
+ 'smirk,smirk_cat,smoking,snail,snake,snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,' +
+ 'spaghetti,sparkle,sparkler,sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,' +
+ 'star2,stars,station,statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,' +
+ 'stuck_out_tongue_closed_eyes,stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,' +
+ 'sunrise_over_mountains,surfer,sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,' +
+ 'symbols,syringe,tada,tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,' +
+ 'thought_balloon,three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,' +
+ 'tophat,tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index e408febd9..24d27b10a 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -617,6 +617,7 @@ export function applyTheme(theme) {
if (theme.centerChannelColor) {
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
+ changeCss('#archive-link-home', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.mentions--top, .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3);
changeCss('.mentions--top, .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2);
@@ -647,10 +648,11 @@ export function applyTheme(theme) {
changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
- 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('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.search-item-container, .post-right__container .post.post--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 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, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, pre, .post-right__container .post.post--root', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post__body, .post.post--comment.other--root.current--user .post-comment, .post.same--root.post--comment .post__body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 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, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
@@ -684,11 +686,11 @@ export function applyTheme(theme) {
}
if (theme.mentionHighlightBg) {
- changeCss('.mention-highlight, .search-highlight, #archive-link-home', 'background:' + theme.mentionHighlightBg, 1);
+ changeCss('.mention-highlight, .search-highlight', 'background:' + theme.mentionHighlightBg, 1);
}
if (theme.mentionHighlightBg) {
- changeCss('.post.post--highlight, #archive-link-home', 'background:' + changeOpacity(theme.mentionHighlightBg, 0.5), 1);
+ changeCss('.post.post--highlight', 'background:' + changeOpacity(theme.mentionHighlightBg, 0.5), 1);
}
if (theme.mentionHighlightLink) {