summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/post.go22
-rw-r--r--api/templates/password_change_body.html2
-rw-r--r--api/templates/password_change_subject.html2
-rw-r--r--doc/developer/tests/test-links.md1
-rw-r--r--doc/help/Markdown.md2
-rw-r--r--doc/install/Configuration-Settings.md7
-rw-r--r--model/config.go10
-rw-r--r--utils/config.go3
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx5
-rw-r--r--web/react/components/channel_info_modal.jsx12
-rw-r--r--web/react/components/file_upload.jsx6
-rw-r--r--web/react/components/password_reset_form.jsx13
-rw-r--r--web/react/components/posts_view.jsx5
-rw-r--r--web/react/components/rhs_thread.jsx12
-rw-r--r--web/react/components/signup_user_complete.jsx31
-rw-r--r--web/react/components/team_signup_password_page.jsx19
-rw-r--r--web/react/components/team_signup_username_page.jsx15
-rw-r--r--web/react/components/textbox.jsx67
-rw-r--r--web/react/components/unread_channel_indicator.jsx2
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx6
-rw-r--r--web/react/stores/socket_store.jsx2
-rw-r--r--web/react/utils/constants.jsx6
-rw-r--r--web/react/utils/utils.jsx19
-rw-r--r--web/sass-files/sass/partials/_post.scss3
-rw-r--r--web/sass-files/sass/partials/_sidebar--left.scss3
-rw-r--r--web/static/config/manifest.json49
-rw-r--r--web/static/images/favicon/android-chrome-144x144.pngbin0 -> 42510 bytes
-rw-r--r--web/static/images/favicon/android-chrome-192x192.pngbin0 -> 63268 bytes
-rw-r--r--web/static/images/favicon/android-chrome-256x256.pngbin0 -> 77443 bytes
-rw-r--r--web/static/images/favicon/android-chrome-36x36.pngbin0 -> 46037 bytes
-rw-r--r--web/static/images/favicon/android-chrome-384x384.pngbin0 -> 122679 bytes
-rw-r--r--web/static/images/favicon/android-chrome-48x48.pngbin0 -> 47013 bytes
-rw-r--r--web/static/images/favicon/android-chrome-512x512.pngbin0 -> 156688 bytes
-rw-r--r--web/static/images/favicon/android-chrome-72x72.pngbin0 -> 34794 bytes
-rw-r--r--web/static/images/favicon/android-chrome-96x96.pngbin0 -> 51380 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-114x114.pngbin0 -> 42510 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-120x120.pngbin0 -> 39527 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-144x144.pngbin0 -> 42510 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-152x152.pngbin0 -> 58512 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-57x57.pngbin0 -> 33180 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-60x60.pngbin0 -> 33245 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-72x72.pngbin0 -> 34794 bytes
-rw-r--r--web/static/images/favicon/apple-touch-icon-76x76.pngbin0 -> 35229 bytes
-rw-r--r--web/static/images/favicon/favicon-16x16.png (renamed from web/static/images/favicon.ico)bin15708 -> 18480 bytes
-rw-r--r--web/static/images/favicon/favicon-32x32.pngbin0 -> 18877 bytes
-rw-r--r--web/static/images/favicon/favicon-96x96.pngbin0 -> 20501 bytes
-rw-r--r--web/static/images/favicon/iTunesArtwork@2x.pngbin0 -> 529609 bytes
-rw-r--r--web/static/images/icon50x50.pngbin15502 -> 16591 bytes
-rw-r--r--web/static/images/logo-email.pngbin10380 -> 21529 bytes
-rw-r--r--web/static/images/logo.pngbin23393 -> 25177 bytes
-rw-r--r--web/static/images/logoWhite.pngbin5876 -> 20570 bytes
-rw-r--r--web/static/images/logo_compact.pngbin6258 -> 18160 bytes
-rw-r--r--web/templates/head.html16
-rw-r--r--web/web_test.go2
55 files changed, 234 insertions, 114 deletions
diff --git a/api/post.go b/api/post.go
index 7cd45d310..42c3c304d 100644
--- a/api/post.go
+++ b/api/post.go
@@ -185,6 +185,28 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc
attachment["text"] = aText
list[i] = attachment
}
+ if _, ok := attachment["pretext"]; ok {
+ aText := attachment["pretext"].(string)
+ aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
+ attachment["pretext"] = aText
+ list[i] = attachment
+ }
+ if fVal, ok := attachment["fields"]; ok {
+ if fields, ok := fVal.([]interface{}); ok {
+ // parse attachment field links into Markdown format
+ for j, fInt := range fields {
+ field := fInt.(map[string]interface{})
+ if _, ok := field["text"]; ok {
+ fText := field["text"].(string)
+ fText = linkWithTextRegex.ReplaceAllString(fText, "[${2}](${1})")
+ field["text"] = fText
+ fields[j] = field
+ }
+ }
+ attachment["fields"] = fields
+ list[i] = attachment
+ }
+ }
}
post.AddProp(key, list)
}
diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html
index 82f4d5429..6199a3423 100644
--- a/api/templates/password_change_body.html
+++ b/api/templates/password_change_body.html
@@ -18,7 +18,7 @@
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2>
- <p>You updated your password for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
+ <p>Your password has been updated for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
</td>
</tr>
<tr>
diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html
index e7a794090..0cbf052c1 100644
--- a/api/templates/password_change_subject.html
+++ b/api/templates/password_change_subject.html
@@ -1 +1 @@
-{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}}
+{{define "password_change_subject"}}Your password has been updated for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}}
diff --git a/doc/developer/tests/test-links.md b/doc/developer/tests/test-links.md
index 91e3e9403..b4a04e439 100644
--- a/doc/developer/tests/test-links.md
+++ b/doc/developer/tests/test-links.md
@@ -40,7 +40,6 @@ http://
./make-compiled-client.sh
test.:test
https://<your-mattermost-url>/signup/gitlab
-https://your-mattermost-url>/signup/gitlab
#### Only the links within these sentences should auto-link:
diff --git a/doc/help/Markdown.md b/doc/help/Markdown.md
index d185a4160..9d3cb2a90 100644
--- a/doc/help/Markdown.md
+++ b/doc/help/Markdown.md
@@ -88,7 +88,7 @@ and
## Emojis
-Emoji provided free by [Emoji One](http://emojione.com/). Check out a full list of emojis [here](http://http://emoji.codes/).
+Emoji provided free by [Emoji One](http://emojione.com/). Check out a full list of emojis [here](http://emoji.codes/).
```
:smile: :+1: :sheep:
diff --git a/doc/install/Configuration-Settings.md b/doc/install/Configuration-Settings.md
index 31d4551f6..7ee440b7c 100644
--- a/doc/install/Configuration-Settings.md
+++ b/doc/install/Configuration-Settings.md
@@ -41,6 +41,13 @@ Set the number of days before SSO sessions expire.
```"SessionCacheInMinutes" : 10```
Set the number of minutes to cache a session in memory.
+```"WebsocketSecurePort": 443```
+The port to use for secure websocket connections being initiated from the client. By default wss:// uses port 443. Some server configurations (e.g. Cloudfoundry) support wss on a different port.
+
+```"WebsocketPort": 80```
+The port to use for websocket connections being initiated from the client. By default ws:// uses port 80.
+
+
#### Webhooks
```"EnableIncomingWebhooks": true```
diff --git a/model/config.go b/model/config.go
index ed56ed0c7..640eb49e5 100644
--- a/model/config.go
+++ b/model/config.go
@@ -40,6 +40,8 @@ type ServiceSettings struct {
SessionLengthMobileInDays *int
SessionLengthSSOInDays *int
SessionCacheInMinutes *int
+ WebsocketSecurePort *int
+ WebsocketPort *int
}
type SSOSettings struct {
@@ -330,6 +332,14 @@ func (o *Config) SetDefaults() {
o.ServiceSettings.SessionCacheInMinutes = new(int)
*o.ServiceSettings.SessionCacheInMinutes = 10
}
+ if o.ServiceSettings.WebsocketPort == nil {
+ o.ServiceSettings.WebsocketPort = new(int)
+ *o.ServiceSettings.WebsocketPort = 80
+ }
+ if o.ServiceSettings.WebsocketSecurePort == nil {
+ o.ServiceSettings.WebsocketSecurePort = new(int)
+ *o.ServiceSettings.WebsocketSecurePort = 443
+ }
}
func (o *Config) IsValid() *AppError {
diff --git a/utils/config.go b/utils/config.go
index 12d03b5de..c2ae1f7a0 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -223,5 +223,8 @@ func getClientConfig(c *model.Config) map[string]string {
props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
+ props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
+ props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
+
return props
}
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
index 5ff7c3413..bf7d5f7e5 100644
--- a/web/react/components/admin_console/reset_password_modal.jsx
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import * as Client from '../../utils/client.jsx';
+import Constants from '../../utils/constants.jsx';
var Modal = ReactBootstrap.Modal;
export default class ResetPasswordModal extends React.Component {
@@ -20,8 +21,8 @@ export default class ResetPasswordModal extends React.Component {
e.preventDefault();
var password = ReactDOM.findDOMNode(this.refs.password).value;
- if (!password || password.length < 5) {
- this.setState({serverError: 'Please enter at least 5 characters.'});
+ if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
+ this.setState({serverError: 'Please enter at least ' + Constants.MIN_PASSWORD_LENGTH + ' characters.'});
return;
}
diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx
index 18e125de3..72c7c3daa 100644
--- a/web/react/components/channel_info_modal.jsx
+++ b/web/react/components/channel_info_modal.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import * as Utils from '../utils/utils.jsx';
const Modal = ReactBootstrap.Modal;
export default class ChannelInfoModal extends React.Component {
@@ -10,10 +11,13 @@ export default class ChannelInfoModal extends React.Component {
channel = {
display_name: 'No Channel Found',
name: 'No Channel Found',
+ purpose: 'No Channel Found',
id: 'No Channel Found'
};
}
+ const channelURL = Utils.getShortenedTeamURL() + channel.name;
+
return (
<Modal
show={this.props.show}
@@ -28,13 +32,17 @@ export default class ChannelInfoModal extends React.Component {
<div className='col-sm-9'>{channel.display_name}</div>
</div>
<div className='row form-group'>
- <div className='col-sm-3 info__label'>{'Channel Handle:'}</div>
- <div className='col-sm-9'>{channel.name}</div>
+ <div className='col-sm-3 info__label'>{'Channel URL:'}</div>
+ <div className='col-sm-9'>{channelURL}</div>
</div>
<div className='row'>
<div className='col-sm-3 info__label'>{'Channel ID:'}</div>
<div className='col-sm-9'>{channel.id}</div>
</div>
+ <div className='row'>
+ <div className='col-sm-3 info__label'>{'Channel Purpose:'}</div>
+ <div className='col-sm-9'>{channel.purpose}</div>
+ </div>
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index fef253c52..7e6cc2942 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -151,7 +151,11 @@ export default class FileUpload extends React.Component {
});
}
- document.addEventListener('paste', function handlePaste(e) {
+ document.addEventListener('paste', (e) => {
+ if (!e.clipboardData) {
+ return;
+ }
+
var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0];
if (textarea !== e.target && !$.contains(textarea, e.target)) {
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 812911569..8063db05a 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -1,7 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as client from '../utils/client.jsx';
+import * as Client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
export default class PasswordResetForm extends React.Component {
constructor(props) {
@@ -16,8 +17,8 @@ export default class PasswordResetForm extends React.Component {
var state = {};
var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
- if (!password || password.length < 5) {
- state.error = 'Please enter at least 5 characters.';
+ if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
+ state.error = 'Please enter at least ' + Constants.MIN_PASSWORD_LENGTH + ' characters.';
this.setState(state);
return;
}
@@ -31,7 +32,7 @@ export default class PasswordResetForm extends React.Component {
data.data = this.props.data;
data.name = this.props.teamName;
- client.resetPassword(data,
+ Client.resetPassword(data,
function resetSuccess() {
this.setState({error: null, updateText: 'Your password has been updated successfully.'});
}.bind(this),
@@ -59,7 +60,7 @@ export default class PasswordResetForm extends React.Component {
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
- <h3>Password Reset</h3>
+ <h3>{'Password Reset'}</h3>
<form onSubmit={this.handlePasswordReset}>
<p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p>
<div className={formClass}>
@@ -77,7 +78,7 @@ export default class PasswordResetForm extends React.Component {
type='submit'
className='btn btn-primary'
>
- Change my password
+ {'Change my password'}
</button>
{updateText}
</form>
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index 7d8c7e265..856403af5 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -57,7 +57,10 @@ export default class PostsView extends React.Component {
this.setState({displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false')});
}
isAtBottom() {
- return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
+ // consider the view to be at the bottom if it's within this many pixels of the bottom
+ const atBottomMargin = 10;
+
+ return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
}
handleScroll() {
// HACK FOR RHS -- REMOVE WHEN RHS DIES
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 2edcd8b37..945b09e37 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -17,6 +17,8 @@ export default class RhsThread extends React.Component {
constructor(props) {
super(props);
+ this.mounted = false;
+
this.onChange = this.onChange.bind(this);
this.onChangeAll = this.onChangeAll.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
@@ -50,8 +52,11 @@ export default class RhsThread extends React.Component {
PostStore.addSelectedPostChangeListener(this.onChange);
PostStore.addChangeListener(this.onChangeAll);
PreferenceStore.addChangeListener(this.forceUpdateInfo);
+
this.resize();
window.addEventListener('resize', this.handleResize);
+
+ this.mounted = true;
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
@@ -63,7 +68,10 @@ export default class RhsThread extends React.Component {
PostStore.removeSelectedPostChangeListener(this.onChange);
PostStore.removeChangeListener(this.onChangeAll);
PreferenceStore.removeChangeListener(this.forceUpdateInfo);
+
window.removeEventListener('resize', this.handleResize);
+
+ this.mounted = false;
}
forceUpdateInfo() {
if (this.state.postList) {
@@ -82,7 +90,7 @@ export default class RhsThread extends React.Component {
}
onChange() {
var newState = this.getStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
+ if (this.mounted && !Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -120,7 +128,7 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
+ if (this.mounted && !Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index df11fe045..ace0d28ae 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -5,6 +5,7 @@ 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 Constants from '../utils/constants.jsx';
export default class SignupUserComplete extends React.Component {
constructor(props) {
@@ -51,7 +52,7 @@ export default class SignupUserComplete extends React.Component {
return;
} else if (usernameError) {
this.setState({
- nameError: 'Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.',
+ nameError: 'Username must begin with a letter, and contain between ' + Constants.MIN_USERNAME_LENGTH + ' to ' + Constants.MAX_USERNAME_LENGTH + ' lowercase characters made up of numbers, letters, and the symbols \'.\', \'-\' and \'_\'.',
emailError: '',
passwordError: '',
serverError: ''
@@ -60,8 +61,8 @@ export default class SignupUserComplete extends React.Component {
}
const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
- if (!providedPassword || providedPassword.length < 5) {
- this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''});
+ if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) {
+ this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least ' + Constants.MIN_PASSWORD_LENGTH + ' characters', serverError: ''});
return;
}
@@ -111,7 +112,7 @@ export default class SignupUserComplete extends React.Component {
client.track('signup', 'signup_user_01_welcome');
if (this.state.wizard === 'finished') {
- return <div>You've already completed the signup process for this invitation or this invitation has expired.</div>;
+ return <div>{"You've already completed the signup process for this invitation or this invitation has expired."}</div>;
}
// set up error labels
@@ -123,9 +124,11 @@ export default class SignupUserComplete extends React.Component {
}
var nameError = null;
+ var nameHelpText = <span className='help-block'>{'Username must begin with a letter, and contain between ' + Constants.MIN_USERNAME_LENGTH + ' to ' + Constants.MAX_USERNAME_LENGTH + " lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'"}</span>;
var nameDivStyle = 'form-group';
if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>;
+ nameHelpText = '';
nameDivStyle += ' has-error';
}
@@ -148,7 +151,7 @@ export default class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
if (this.state.user.email) {
- yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.mm_config.SiteName}.</span>;
+ yourEmailIs = <span>{'Your email address is '}<strong>{this.state.user.email}</strong>{". You'll use this address to sign in to " + global.window.mm_config.SiteName + '.'}</span>;
}
var emailContainerStyle = 'margin--extra';
@@ -158,7 +161,7 @@ export default class SignupUserComplete extends React.Component {
var email = (
<div className={emailContainerStyle}>
- <h5><strong>What's your email address?</strong></h5>
+ <h5><strong>{"What's your email address?"}</strong></h5>
<div className={emailDivStyle}>
<input
type='email'
@@ -208,7 +211,7 @@ export default class SignupUserComplete extends React.Component {
{email}
{yourEmailIs}
<div className='margin--extra'>
- <h5><strong>Choose your username</strong></h5>
+ <h5><strong>{'Choose your username'}</strong></h5>
<div className={nameDivStyle}>
<input
type='text'
@@ -219,11 +222,11 @@ export default class SignupUserComplete extends React.Component {
spellCheck='false'
/>
{nameError}
- <span className='help-block'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</span>
+ {nameHelpText}
</div>
</div>
<div className='margin--extra'>
- <h5><strong>Choose your password</strong></h5>
+ <h5><strong>{'Choose your password'}</strong></h5>
<div className={passwordDivStyle}>
<input
type='password'
@@ -243,7 +246,7 @@ export default class SignupUserComplete extends React.Component {
onClick={this.handleSubmit}
className='btn-primary btn'
>
- Create Account
+ {'Create Account'}
</button>
</p>
</div>
@@ -255,7 +258,7 @@ export default class SignupUserComplete extends React.Component {
<div>
{signupMessage}
<div className='or__container'>
- <span>or</span>
+ <span>{'or'}</span>
</div>
</div>
);
@@ -268,10 +271,10 @@ export default class SignupUserComplete extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h5 className='margin--less'>Welcome to:</h5>
+ <h5 className='margin--less'>{'Welcome to:'}</h5>
<h2 className='signup-team__name'>{this.props.teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2>
- <h4 className='color--light'>Let's create your account</h4>
+ <h2 className='signup-team__subdomain'>{'on ' + global.window.mm_config.SiteName}</h2>
+ <h4 className='color--light'>{"Let's create your account"}</h4>
{signupMessage}
{emailSignup}
{serverError}
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index 378c7fe2c..7e11d38c3 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -4,6 +4,7 @@
import * as Client from '../utils/client.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import UserStore from '../stores/user_store.jsx';
+import Constants from '../utils/constants.jsx';
export default class TeamSignupPasswordPage extends React.Component {
constructor(props) {
@@ -23,8 +24,8 @@ export default class TeamSignupPasswordPage extends React.Component {
e.preventDefault();
var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
- if (!password || password.length < 5) {
- this.setState({passwordError: 'Please enter at least 5 characters'});
+ if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
+ this.setState({passwordError: 'Please enter at least ' + Constants.MIN_PASSWORD_LENGTH + ' characters'});
return;
}
@@ -92,15 +93,15 @@ export default class TeamSignupPasswordPage extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h2 className='margin--less'>Your password</h2>
- <h5 className='color--light'>Select a password that you'll use to login with your email address:</h5>
+ <h2 className='margin--less'>{'Your password'}</h2>
+ <h5 className='color--light'>{"Select a password that you'll use to login with your email address:"}</h5>
<div className='inner__content margin--extra'>
- <h5><strong>Email</strong></h5>
+ <h5><strong>{'Email'}</strong></h5>
<div className='block--gray form-group'>{this.props.state.team.email}</div>
<div className={passwordDivStyle}>
<div className='row'>
<div className='col-sm-11'>
- <h5><strong>Choose your password</strong></h5>
+ <h5><strong>{'Choose your password'}</strong></h5>
<input
autoFocus={true}
type='password'
@@ -110,7 +111,7 @@ export default class TeamSignupPasswordPage extends React.Component {
maxLength='128'
spellCheck='false'
/>
- <span className='color--light help-block'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</span>
+ <span className='color--light help-block'>{'Passwords must contain ' + Constants.MIN_PASSWORD_LENGTH + ' to ' + Constants.MAX_PASSWORD_LENGTH + ' characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.'}</span>
</div>
</div>
{passwordError}
@@ -125,7 +126,7 @@ export default class TeamSignupPasswordPage extends React.Component {
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating team...'}
onClick={this.submitNext}
>
- Finish
+ {'Finish'}
</button>
</div>
<p>By proceeding to create your account and use {global.window.mm_config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.mm_config.SiteName}.</p>
@@ -134,7 +135,7 @@ export default class TeamSignupPasswordPage extends React.Component {
href='#'
onClick={this.submitBack}
>
- Back to previous step
+ {'Back to previous step'}
</a>
</div>
</form>
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index de239f169..6ccab6656 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -3,6 +3,7 @@
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
export default class TeamSignupUsernamePage extends React.Component {
constructor(props) {
@@ -33,7 +34,7 @@ export default class TeamSignupUsernamePage extends React.Component {
this.setState({nameError: 'This username is reserved, please choose a new one.'});
return;
} else if (usernameError) {
- this.setState({nameError: 'Username must begin with a letter, and contain 3 to 15 characters in total, which may be numbers, lowercase letters, or any of the symbols \'.\', \'-\', or \'_\''});
+ this.setState({nameError: 'Username must begin with a letter, and contain between ' + Constants.MIN_USERNAME_LENGTH + ' to ' + Constants.MAX_USERNAME_LENGTH + ' characters in total, which may be numbers, lowercase letters, or any of the symbols \'.\', \'-\', or \'_\''});
return;
}
@@ -45,9 +46,11 @@ export default class TeamSignupUsernamePage extends React.Component {
Client.track('signup', 'signup_team_06_username');
var nameError = null;
+ var nameHelpText = <span className='color--light help-block'>{'Usernames must begin with a letter and contain between ' + Constants.MIN_USERNAME_LENGTH + ' to ' + Constants.MAX_USERNAME_LENGTH + " characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'"}</span>;
var nameDivClass = 'form-group';
if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>;
+ nameHelpText = '';
nameDivClass += ' has-error';
}
@@ -58,13 +61,13 @@ export default class TeamSignupUsernamePage extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h2 className='margin--less'>Your username</h2>
+ <h2 className='margin--less'>{'Your username'}</h2>
<h5 className='color--light'>{'Select a memorable username that makes it easy for teammates to identify you:'}</h5>
<div className='inner__content margin--extra'>
<div className={nameDivClass}>
<div className='row'>
<div className='col-sm-11'>
- <h5><strong>Choose your username</strong></h5>
+ <h5><strong>{'Choose your username'}</strong></h5>
<input
autoFocus={true}
type='text'
@@ -75,7 +78,7 @@ export default class TeamSignupUsernamePage extends React.Component {
maxLength='128'
spellCheck='false'
/>
- <span className='color--light help-block'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</span>
+ {nameHelpText}
</div>
</div>
{nameError}
@@ -86,7 +89,7 @@ export default class TeamSignupUsernamePage extends React.Component {
className='btn btn-primary margin--extra'
onClick={this.submitNext}
>
- Next
+ {'Next'}
<i className='glyphicon glyphicon-chevron-right'></i>
</button>
<div className='margin--extra'>
@@ -94,7 +97,7 @@ export default class TeamSignupUsernamePage extends React.Component {
href='#'
onClick={this.submitBack}
>
- Back to previous step
+ {'Back to previous step'}
</a>
</div>
</form>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index b29f304ab..62c0d5218 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -22,8 +22,6 @@ export default class Textbox extends React.Component {
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.resize = this.resize.bind(this);
- this.handleFocus = this.handleFocus.bind(this);
- this.handleBlur = this.handleBlur.bind(this);
this.showPreview = this.showPreview.bind(this);
this.state = {
@@ -81,51 +79,43 @@ export default class Textbox extends React.Component {
}
resize() {
- const e = this.refs.message.getTextbox();
- const w = ReactDOM.findDOMNode(this.refs.wrapper);
+ const textbox = this.refs.message.getTextbox();
+ const $textbox = $(textbox);
+ const $wrapper = $(ReactDOM.findDOMNode(this.refs.wrapper));
- const prevHeight = $(e).height();
+ const padding = parseInt($textbox.css('padding-bottom'), 10) + parseInt($textbox.css('padding-top'), 10);
+ const borders = parseInt($textbox.css('border-bottom-width'), 10) + parseInt($textbox.css('border-top-width'), 10);
+ const maxHeight = parseInt($textbox.css('max-height'), 10) - borders;
- const lht = parseInt($(e).css('lineHeight'), 10);
- const lines = e.scrollHeight / lht;
- let mod = 15;
+ const prevHeight = $textbox.height();
- if (lines < 2.5 || this.props.messageText === '') {
- mod = 30;
- }
+ // set the height to auto and remove the scrollbar so we can get the actual size of the contents
+ $textbox.css('height', 'auto').css('overflow-y', 'hidden');
+
+ let height = textbox.scrollHeight - padding;
+
+ if (height + padding > maxHeight) {
+ height = maxHeight - padding;
- if (e.scrollHeight - mod < 167) {
- $(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod);
- $(w).css({height: 'auto'}).height(e.scrollHeight + 2);
- $(w).closest('.post-body__cell').removeClass('scroll');
- if (this.state.preview) {
- $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'auto'}).height(e.scrollHeight - mod);
- }
+ // turn scrollbar on and move over attachment icon to compensate for that
+ $textbox.css('overflow-y', 'scroll');
+ $wrapper.closest('.post-body__cell').addClass('scroll');
} else {
- $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod);
- $(w).css({height: 'auto'}).height(163);
- $(w).closest('.post-body__cell').addClass('scroll');
- if (this.state.preview) {
- $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(163);
- }
+ $wrapper.closest('.post-body__cell').removeClass('scroll');
}
- if (prevHeight !== $(e).height() && this.props.onHeightChange) {
- this.props.onHeightChange();
- }
- }
+ // set the textarea to be the proper height
+ $textbox.height(height);
+
+ // set the wrapper height to match the height of the textbox including padding and borders
+ $wrapper.height(height + padding + borders);
- handleFocus() {
- const elm = this.refs.message.getTextbox();
- if (elm.title === elm.value) {
- elm.value = '';
+ if (this.state.preview) {
+ $(ReactDOM.findDOMNode(this.refs.preview)).height(height + borders);
}
- }
- handleBlur() {
- const elm = this.refs.message.getTextbox();
- if (elm.value === '') {
- elm.value = elm.title;
+ if (height !== prevHeight && this.props.onHeightChange) {
+ this.props.onHeightChange();
}
}
@@ -178,9 +168,6 @@ export default class Textbox extends React.Component {
onUserInput={this.props.onUserInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
- onFocus={this.handleFocus}
- onBlur={this.handleBlur}
- onPaste={this.handlePaste}
style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
listComponent={SuggestionList}
providers={this.suggestionProviders}
diff --git a/web/react/components/unread_channel_indicator.jsx b/web/react/components/unread_channel_indicator.jsx
index 6ae06528b..c0c34584f 100644
--- a/web/react/components/unread_channel_indicator.jsx
+++ b/web/react/components/unread_channel_indicator.jsx
@@ -10,7 +10,7 @@ export default class UnreadChannelIndicator extends React.Component {
render() {
let displayValue = 'none';
if (this.props.show) {
- displayValue = 'initial';
+ displayValue = 'block';
}
return (
<div
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 014038dd4..df7ae4a25 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -47,7 +47,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.setState({clientError: 'This username is reserved, please choose a new one.'});
return;
} else if (usernameError) {
- this.setState({clientError: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."});
+ this.setState({clientError: 'Username must begin with a letter, and contain between ' + Constants.MIN_USERNAME_LENGTH + ' to ' + Constants.MAX_USERNAME_LENGTH + " lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."});
return;
}
@@ -493,7 +493,7 @@ export default class UserSettingsGeneralTab extends React.Component {
);
submit = this.submitEmail;
- } else {
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
inputs.push(
<div
key='oauthEmailInfo'
@@ -531,7 +531,7 @@ export default class UserSettingsGeneralTab extends React.Component {
} else {
describe = UserStore.getCurrentUser().email;
}
- } else {
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
describe = 'Log in done through GitLab';
}
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index d1266dd3f..5a21abd19 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -48,8 +48,8 @@ export default class SecurityTab extends React.Component {
return;
}
- if (newPassword.length < 5) {
- this.setState({passwordError: 'New passwords must be at least 5 characters', serverError: ''});
+ if (newPassword.length < Constants.MIN_PASSWORD_LENGTH) {
+ this.setState({passwordError: 'New passwords must be at least ' + Constants.MIN_PASSWORD_LENGTH + ' characters', serverError: ''});
return;
}
@@ -337,7 +337,7 @@ export default class SecurityTab extends React.Component {
className='security-links theme'
dialogType={AccessHistoryModal}
>
- <i className='fa fa-clock-o'></i>View Access History
+ <i className='fa fa-clock-o'></i>{'View Access History'}
</ToggleModalButton>
<b> </b>
<ToggleModalButton
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 24fa79ca6..f1fade305 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -49,7 +49,7 @@ class SocketStoreClass extends EventEmitter {
protocol = 'wss://';
}
- var connUrl = protocol + location.host + '/api/v1/websocket?' + Utils.getSessionIndex();
+ var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex();
if (this.failCount === 0) {
console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index d0f34293f..5d6aa9329 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -453,5 +453,9 @@ export default {
description: 'Show preview snippet of links below message'
}
},
- OVERLAY_TIME_DELAY: 400
+ OVERLAY_TIME_DELAY: 400,
+ MIN_USERNAME_LENGTH: 3,
+ MAX_USERNAME_LENGTH: 15,
+ MIN_PASSWORD_LENGTH: 5,
+ MAX_PASSWORD_LENGTH: 50
};
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 24042321f..71fd0852b 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -879,8 +879,8 @@ export function isValidUsername(name) {
var error = '';
if (!name) {
error = 'This field is required';
- } else if (name.length < 3 || name.length > 15) {
- error = 'Must be between 3 and 15 characters';
+ } else if (name.length < Constants.MIN_USERNAME_LENGTH || name.length > Constants.MAX_USERNAME_LENGTH) {
+ error = 'Must be between ' + Constants.MIN_USERNAME_LENGTH + ' and ' + Constants.MAX_USERNAME_LENGTH + ' characters';
} else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
} else if (!(/[a-z]/).test(name.charAt(0))) { //eslint-disable-line no-negated-condition
@@ -1101,6 +1101,17 @@ export function getFileName(path) {
return split[split.length - 1];
}
+// Gets the websocket port to use. Configurable on the server.
+export function getWebsocketPort(protocol) {
+ if ((/^wss:/).test(protocol)) { // wss://
+ return ':' + global.window.mm_config.WebsocketSecurePort;
+ }
+ if ((/^ws:/).test(protocol)) {
+ return ':' + global.window.mm_config.WebsocketPort;
+ }
+ return '';
+}
+
export function getSessionIndex() {
if (global.window.mm_session_token_index >= 0) {
return 'session_token_index=' + global.window.mm_session_token_index;
@@ -1309,6 +1320,10 @@ export function fillArray(value, length) {
// Checks if a data transfer contains files not text, folders, etc..
// Slightly modified from http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa
export function isFileTransfer(files) {
+ if (isBrowserIE()) {
+ return files.types != null && files.types.contains('Files');
+ }
+
return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.contains('application/x-moz-file'));
}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 7b7c2d73a..34ff7e5a9 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -11,6 +11,7 @@
resize: none;
line-height:20px;
min-height:36px;
+ overflow-x: hidden;
&:focus {
border-color: #ccc;
box-shadow: none;
@@ -183,6 +184,7 @@ body.ios {
position: absolute;
top: 50%;
left: 50%;
+ pointer-events: none;
}
.overlay__files {
@@ -335,7 +337,6 @@ body.ios {
padding-top: 8px;
padding-right: 28px;
max-height: 162px !important;
- overflow: auto;
line-height: 1.5;
}
.textarea-div {
diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss
index 6f969ed47..5e7f04724 100644
--- a/web/sass-files/sass/partials/_sidebar--left.scss
+++ b/web/sass-files/sass/partials/_sidebar--left.scss
@@ -42,6 +42,9 @@
margin-right: 6px;
width: 12px;
display: inline-block;
+ svg {
+ max-height: 14px;
+ }
i, path, ellipse {
@include opacity(0.5);
&.online--icon, &.away--icon {
diff --git a/web/static/config/manifest.json b/web/static/config/manifest.json
index 8f29460b3..dd95d917e 100644
--- a/web/static/config/manifest.json
+++ b/web/static/config/manifest.json
@@ -1,13 +1,40 @@
{
- "name": "Mattermost",
- "icons": [
- {
- "src": "../static/iamges/icon50x50.png",
- "sizes": "50x50",
- "type": "image/png"
- }
- ],
- "start_url": "/",
- "display": "standalone",
- "orientation": "portrait"
+ "name": "Mattermost",
+ "icons": [{
+ "src": "/static/images/favicon/android-chrome-36x36.png",
+ "type": "image/png",
+ "sizes": "36x36"
+ }, {
+ "src": "/static/images/favicon/android-chrome-48x48.png",
+ "type": "image/png",
+ "sizes": "48x48"
+ }, {
+ "src": "/static/images/favicon/android-chrome-72x72.png",
+ "type": "image/png",
+ "sizes": "72x72"
+ }, {
+ "src": "/static/images/favicon/android-chrome-96x96.png",
+ "type": "image/png",
+ "sizes": "96x96"
+ }, {
+ "src": "/static/images/favicon/android-chrome-144x144.png",
+ "type": "image/png",
+ "sizes": "144x144"
+ }, {
+ "src": "/static/images/favicon/android-chrome-192x192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ }, {
+ "src": "/static/images/favicon/android-chrome-256x256.png",
+ "type": "image/png",
+ "sizes": "256x256"
+ }, {
+ "src": "/static/images/favicon/android-chrome-384x384.png",
+ "type": "image/png",
+ "sizes": "384x384"
+ }, {
+ "src": "/static/images/favicon/android-chrome-512x512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }]
} \ No newline at end of file
diff --git a/web/static/images/favicon/android-chrome-144x144.png b/web/static/images/favicon/android-chrome-144x144.png
new file mode 100644
index 000000000..b4cb16a20
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-144x144.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-192x192.png b/web/static/images/favicon/android-chrome-192x192.png
new file mode 100644
index 000000000..43b64dee2
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-192x192.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-256x256.png b/web/static/images/favicon/android-chrome-256x256.png
new file mode 100644
index 000000000..17d568314
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-256x256.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-36x36.png b/web/static/images/favicon/android-chrome-36x36.png
new file mode 100644
index 000000000..a46f34b26
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-36x36.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-384x384.png b/web/static/images/favicon/android-chrome-384x384.png
new file mode 100644
index 000000000..6a3fde9d3
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-384x384.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-48x48.png b/web/static/images/favicon/android-chrome-48x48.png
new file mode 100644
index 000000000..6beb5bb2a
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-48x48.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-512x512.png b/web/static/images/favicon/android-chrome-512x512.png
new file mode 100644
index 000000000..b9229a3b7
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-512x512.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-72x72.png b/web/static/images/favicon/android-chrome-72x72.png
new file mode 100644
index 000000000..9f6afec0d
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-72x72.png
Binary files differ
diff --git a/web/static/images/favicon/android-chrome-96x96.png b/web/static/images/favicon/android-chrome-96x96.png
new file mode 100644
index 000000000..b6ae0c7a7
--- /dev/null
+++ b/web/static/images/favicon/android-chrome-96x96.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-114x114.png b/web/static/images/favicon/apple-touch-icon-114x114.png
new file mode 100644
index 000000000..b4cb16a20
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-114x114.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-120x120.png b/web/static/images/favicon/apple-touch-icon-120x120.png
new file mode 100644
index 000000000..29a96291d
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-120x120.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-144x144.png b/web/static/images/favicon/apple-touch-icon-144x144.png
new file mode 100644
index 000000000..b4cb16a20
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-144x144.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-152x152.png b/web/static/images/favicon/apple-touch-icon-152x152.png
new file mode 100644
index 000000000..be3f49d99
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-152x152.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-57x57.png b/web/static/images/favicon/apple-touch-icon-57x57.png
new file mode 100644
index 000000000..5e7e3d366
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-57x57.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-60x60.png b/web/static/images/favicon/apple-touch-icon-60x60.png
new file mode 100644
index 000000000..6f6c5e6bf
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-60x60.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-72x72.png b/web/static/images/favicon/apple-touch-icon-72x72.png
new file mode 100644
index 000000000..9f6afec0d
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-72x72.png
Binary files differ
diff --git a/web/static/images/favicon/apple-touch-icon-76x76.png b/web/static/images/favicon/apple-touch-icon-76x76.png
new file mode 100644
index 000000000..adb504eab
--- /dev/null
+++ b/web/static/images/favicon/apple-touch-icon-76x76.png
Binary files differ
diff --git a/web/static/images/favicon.ico b/web/static/images/favicon/favicon-16x16.png
index af5505331..777a60546 100644
--- a/web/static/images/favicon.ico
+++ b/web/static/images/favicon/favicon-16x16.png
Binary files differ
diff --git a/web/static/images/favicon/favicon-32x32.png b/web/static/images/favicon/favicon-32x32.png
new file mode 100644
index 000000000..36c4a12e0
--- /dev/null
+++ b/web/static/images/favicon/favicon-32x32.png
Binary files differ
diff --git a/web/static/images/favicon/favicon-96x96.png b/web/static/images/favicon/favicon-96x96.png
new file mode 100644
index 000000000..4f6c6d1b2
--- /dev/null
+++ b/web/static/images/favicon/favicon-96x96.png
Binary files differ
diff --git a/web/static/images/favicon/iTunesArtwork@2x.png b/web/static/images/favicon/iTunesArtwork@2x.png
new file mode 100644
index 000000000..10321f3a1
--- /dev/null
+++ b/web/static/images/favicon/iTunesArtwork@2x.png
Binary files differ
diff --git a/web/static/images/icon50x50.png b/web/static/images/icon50x50.png
index 7ac6ce1c9..4791e755f 100644
--- a/web/static/images/icon50x50.png
+++ b/web/static/images/icon50x50.png
Binary files differ
diff --git a/web/static/images/logo-email.png b/web/static/images/logo-email.png
index c16978ba8..d0a38e21d 100644
--- a/web/static/images/logo-email.png
+++ b/web/static/images/logo-email.png
Binary files differ
diff --git a/web/static/images/logo.png b/web/static/images/logo.png
index 423d4d270..1a35cdf01 100644
--- a/web/static/images/logo.png
+++ b/web/static/images/logo.png
Binary files differ
diff --git a/web/static/images/logoWhite.png b/web/static/images/logoWhite.png
index 11bbd4632..3166c974c 100644
--- a/web/static/images/logoWhite.png
+++ b/web/static/images/logoWhite.png
Binary files differ
diff --git a/web/static/images/logo_compact.png b/web/static/images/logo_compact.png
index b861b7c6d..a4bce6281 100644
--- a/web/static/images/logo_compact.png
+++ b/web/static/images/logo_compact.png
Binary files differ
diff --git a/web/templates/head.html b/web/templates/head.html
index 689c69d3c..b5eb3a9b3 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -15,6 +15,19 @@
<!-- iOS add to homescreen -->
<!-- Android add to homescreen -->
+ <link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
+ <link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
+ <link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
+ <link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
+ <link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
+ <link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
+ <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
+ <link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/config/manifest.json">
<!-- Android add to homescreen -->
@@ -27,9 +40,6 @@
<link rel="stylesheet" href="/static/css/katex.min.css">
<link rel="stylesheet" class="code_theme" href="">
- <link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
- <link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
-
<script src="/static/js/react-0.14.3.js"></script>
<script src="/static/js/react-dom-0.14.3.js"></script>
<script src="/static/js/jquery-2.1.4.js"></script>
diff --git a/web/web_test.go b/web/web_test.go
index 237fdceaf..efe681a55 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -46,7 +46,7 @@ func TestStatic(t *testing.T) {
// add a short delay to make sure the server is ready to receive requests
time.Sleep(1 * time.Second)
- resp, err := http.Get(URL + "/static/images/favicon.ico")
+ resp, err := http.Get(URL + "/static/images/favicon/favicon-16x16.png")
if err != nil {
t.Fatalf("got error while trying to get static files %v", err)