diff options
Diffstat (limited to 'web')
22 files changed, 483 insertions, 46 deletions
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index 3143bec22..f70027498 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -15,6 +15,19 @@ export default class AboutBuildModal extends React.Component { render() { const config = global.window.mm_config; + const license = global.window.mm_license; + + let title = 'Team Edition'; + let licensee; + if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') { + title = 'Enterprise Edition'; + licensee = ( + <div className='row form-group'> + <div className='col-sm-3 info__label'>{'Licensed by:'}</div> + <div className='col-sm-9'>{license.Company}</div> + </div> + ); + } return ( <Modal @@ -22,9 +35,15 @@ export default class AboutBuildModal extends React.Component { onHide={this.doHide} > <Modal.Header closeButton={true}> - <Modal.Title>{`Mattermost ${config.Version}`}</Modal.Title> + <Modal.Title>{'About Mattermost'}</Modal.Title> </Modal.Header> <Modal.Body> + <h4>{`Mattermost ${title}`}</h4> + {licensee} + <div className='row form-group'> + <div className='col-sm-3 info__label'>{'Version:'}</div> + <div className='col-sm-9'>{config.Version}</div> + </div> <div className='row form-group'> <div className='col-sm-3 info__label'>{'Build Number:'}</div> <div className='col-sm-9'>{config.BuildNumber}</div> diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 32b2e9bb7..0f85c238d 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -22,6 +22,7 @@ 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'; +import LicenseSettingsTab from './license_settings.jsx'; export default class AdminController extends React.Component { constructor(props) { @@ -154,6 +155,8 @@ export default class AdminController extends React.Component { tab = <LegalAndSupportSettingsTab config={this.state.config} />; } else if (this.state.selected === 'ldap_settings') { tab = <LdapSettingsTab config={this.state.config} />; + } else if (this.state.selected === 'license') { + tab = <LicenseSettingsTab />; } 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 1279f4d22..5a5eaa055 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -155,6 +155,36 @@ export default class AdminSidebar extends React.Component { } } + let ldapSettings; + let licenseSettings; + if (global.window.mm_config.BuildEnterpriseReady === 'true') { + if (global.window.mm_license.IsLicensed === 'true') { + ldapSettings = ( + <li> + <a + href='#' + className={this.isSelected('ldap_settings')} + onClick={this.handleClick.bind(this, 'ldap_settings', null)} + > + {'LDAP Settings'} + </a> + </li> + ); + } + + licenseSettings = ( + <li> + <a + href='#' + className={this.isSelected('license')} + onClick={this.handleClick.bind(this, 'license', null)} + > + {'Edition and License'} + </a> + </li> + ); + } + return ( <div className='sidebar--left sidebar--collapsable'> <div> @@ -252,6 +282,7 @@ export default class AdminSidebar extends React.Component { {'GitLab Settings'} </a> </li> + {ldapSettings} <li> <a href='#' @@ -300,6 +331,7 @@ export default class AdminSidebar extends React.Component { </li> </ul> <ul className='nav nav__sub-menu padded'> + {licenseSettings} <li> <a href='#' diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index 193fd4147..c568c5a77 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -254,7 +254,7 @@ export default class EmailSettings extends React.Component { /> {'false'} </label> - <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}</p> + <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.\nSetting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'}</p> </div> </div> diff --git a/web/react/components/admin_console/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx index 6e3da2f72..1447f3bd7 100644 --- a/web/react/components/admin_console/ldap_settings.jsx +++ b/web/react/components/admin_console/ldap_settings.jsx @@ -90,14 +90,41 @@ export default class LdapSettings extends React.Component { saveClass = 'btn btn-primary'; } - return ( - <div className='wrapper--fixed'> + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true'; + + let bannerContent; + if (licenseEnabled) { + bannerContent = ( <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> + ); + } else { + bannerContent = ( + <div className='banner warning'> + <div className='banner__content'> + <h4 className='banner__heading'>{'Note:'}</h4> + <p> + {'LDAP is an enterprise feature. Your current license does not support LDAP. Click '} + <a + href='http://mattermost.com' + target='_blank' + > + {'here'} + </a> + {' for information and pricing on enterprise licenses.'} + </p> + </div> + </div> + ); + } + + return ( + <div className='wrapper--fixed'> + {bannerContent} <h3>{'LDAP Settings'}</h3> <form className='form-horizontal' @@ -119,6 +146,7 @@ export default class LdapSettings extends React.Component { ref='Enable' defaultChecked={this.props.config.LdapSettings.Enable} onChange={this.handleEnable} + disabled={!licenseEnabled} /> {'true'} </label> diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx new file mode 100644 index 000000000..ba953f3bd --- /dev/null +++ b/web/react/components/admin_console/license_settings.jsx @@ -0,0 +1,237 @@ +// 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 LicenseSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleRemove = this.handleRemove.bind(this); + + this.state = { + fileSelected: false, + serverError: null + }; + } + + handleChange() { + const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); + if (element.prop('files').length > 0) { + this.setState({fileSelected: true}); + } + } + + handleSubmit(e) { + e.preventDefault(); + + const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); + if (element.prop('files').length === 0) { + return; + } + const file = element.prop('files')[0]; + + $('#upload-button').button('loading'); + + const formData = new FormData(); + formData.append('license', file, file.name); + + Client.uploadLicenseFile(formData, + () => { + Utils.clearFileInput(element[0]); + $('#upload-button').button('reset'); + this.setState({serverError: null}); + window.location.reload(true); + }, + (error) => { + Utils.clearFileInput(element[0]); + $('#upload-button').button('reset'); + this.setState({serverError: error.message}); + } + ); + } + + handleRemove(e) { + e.preventDefault(); + + $('#remove-button').button('loading'); + + Client.removeLicenseFile( + () => { + $('#remove-button').button('reset'); + this.setState({serverError: null}); + window.location.reload(true); + }, + (error) => { + $('#remove-button').button('reset'); + this.setState({serverError: error.message}); + } + ); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var btnClass = 'btn'; + if (this.state.fileSelected) { + btnClass = 'btn btn-primary'; + } + + let edition; + let licenseType; + let licenseKey; + + if (global.window.mm_license.IsLicensed === 'true') { + edition = 'Mattermost Enterprise Edition. Designed for enterprise-scale communication.'; + licenseType = ( + <div> + <p> + {'This compiled release of Mattermost platform is provided under a '} + <a + href='http://mattermost.com' + target='_blank' + > + {'commercial license'} + </a> + {' from Mattermost, Inc. based on your subscription level and is subject to the '} + <a + href={global.window.mm_config.TermsOfServiceLink} + target='_blank' + > + {'Terms of Service.'} + </a> + </p> + <p>{'Your subscription details are as follows:'}</p> + {'Name: ' + global.window.mm_license.Name} + <br/> + {'Company or organization name: ' + global.window.mm_license.Company} + <br/> + {'Number of users: ' + global.window.mm_license.Users} + <br/> + {`License issued: ${Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10))} ${Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true)}`} + <br/> + {'Start date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10))} + <br/> + {'Expiry date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10))} + <br/> + {'LDAP: ' + global.window.mm_license.LDAP} + <br/> + </div> + ); + + licenseKey = ( + <div className='col-sm-8'> + <button + className='btn btn-danger' + onClick={this.handleRemove} + id='remove-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Removing License...'} + > + {'Remove Enterprise License and Downgrade Server'} + </button> + <br/> + <br/> + <p className='help-text'> + {'If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, '} + <a + href='http://mattermost.com' + target='_blank' + > + {'disable all Enterprise Edition features on this server'} + </a> + {'. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.'} + </p> + </div> + ); + } else { + edition = 'Mattermost Team Edition. Designed for teams from 5 to 50 users.'; + + licenseType = ( + <span> + <p>{'This compiled release of Mattermost platform is offered under an MIT license.'}</p> + <p>{'See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.'}</p> + </span> + ); + + licenseKey = ( + <div className='col-sm-8'> + <input + className='pull-left' + ref='fileInput' + type='file' + accept='.mattermost-license' + onChange={this.handleChange} + /> + <button + className={btnClass + ' pull-left'} + disabled={!this.state.fileSelected} + onClick={this.handleSubmit} + id='upload-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Uploading License...'} + > + {'Upload'} + </button> + <br/> + <br/> + <br/> + {serverError} + <p className='help-text'> + {'Upload a license key for Mattermost Enterprise Edition to upgrade this server. '} + <a + href='http://mattermost.com' + target='_blank' + > + {'Visit us online'} + </a> + {' to learn more about the benefits of Enterprise Edition or to purchase a key.'} + </p> + </div> + ); + } + + return ( + <div className='wrapper--fixed'> + <h3>{'Edition and License'}</h3> + <form + className='form-horizontal' + role='form' + > + <div className='form-group'> + <label + className='control-label col-sm-4' + > + {'Edition: '} + </label> + <div className='col-sm-8'> + {edition} + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + > + {'License: '} + </label> + <div className='col-sm-8'> + {licenseType} + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + > + {'License Key: '} + </label> + {licenseKey} + </div> + </form> + </div> + ); + } +} diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index e28699d3c..fe7230946 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -3,8 +3,12 @@ import * as Client from '../../utils/client.jsx'; import * as Utils from '../../utils/utils.jsx'; +import Constants from '../../utils/constants.jsx'; import LineChart from './line_chart.jsx'; +var Tooltip = ReactBootstrap.Tooltip; +var OverlayTrigger = ReactBootstrap.OverlayTrigger; + export default class TeamAnalytics extends React.Component { constructor(props) { super(props); @@ -314,9 +318,25 @@ export default class TeamAnalytics extends React.Component { <tbody> { this.state.recent_active_users.map((user) => { + const tooltip = ( + <Tooltip id={'recent-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + return ( - <tr key={user.id}> - <td>{user.email}</td> + <tr key={'recent-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> <td>{Utils.displayDateTime(user.last_activity_at)}</td> </tr> ); @@ -347,9 +367,25 @@ export default class TeamAnalytics extends React.Component { <tbody> { this.state.newly_created_users.map((user) => { + const tooltip = ( + <Tooltip id={'new-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + return ( - <tr key={user.id}> - <td>{user.email}</td> + <tr key={'new-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> <td>{Utils.displayDateTime(user.create_at)}</td> </tr> ); diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 59ceb038e..f64834775 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -379,7 +379,7 @@ export default class ChannelHeader extends React.Component { <th> <div className='dropdown channel-header__links'> <OverlayTrigger - delayShow={400} + delayShow={Constants.OVERLAY_TIME_DELAY} placement='bottom' overlay={recentMentionsTooltip} > diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index 6337afabc..fef253c52 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -1,7 +1,7 @@ // 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'; import ChannelStore from '../stores/channel_store.jsx'; import * as Utils from '../utils/utils.jsx'; @@ -26,7 +26,7 @@ export default class FileUpload extends React.Component { for (var j = 0; j < data.client_ids.length; j++) { delete requests[data.client_ids[j]]; } - this.setState({requests: requests}); + this.setState({requests}); } fileUploadFail(clientId, err) { @@ -52,7 +52,7 @@ export default class FileUpload extends React.Component { } // generate a unique id that can be used by other components to refer back to this upload - let clientId = Utils.generateId(); + const clientId = Utils.generateId(); // prepare data to be uploaded var formData = new FormData(); @@ -60,14 +60,14 @@ export default class FileUpload extends React.Component { formData.append('files', files[i], files[i].name); formData.append('client_ids', clientId); - var request = client.uploadFile(formData, + var request = Client.uploadFile(formData, this.fileUploadSuccess.bind(this, channelId), this.fileUploadFail.bind(this, clientId) ); var requests = this.state.requests; requests[clientId] = request; - this.setState({requests: requests}); + this.setState({requests}); this.props.onUploadStart([clientId], channelId); @@ -90,16 +90,7 @@ export default class FileUpload extends React.Component { this.uploadFiles(element.prop('files')); - // clear file input for all modern browsers - try { - element[0].value = ''; - if (element.value) { - element[0].type = 'text'; - element[0].type = 'file'; - } - } catch (e) { - // Do nothing - } + Utils.clearFileInput(element[0]); } handleDrop(e) { @@ -227,14 +218,14 @@ export default class FileUpload extends React.Component { formData.append('files', file, name); formData.append('client_ids', clientId); - var request = client.uploadFile(formData, + var request = Client.uploadFile(formData, self.fileUploadSuccess.bind(self, channelId), self.fileUploadFail.bind(self, clientId) ); var requests = self.state.requests; requests[clientId] = request; - self.setState({requests: requests}); + self.setState({requests}); self.props.onUploadStart([clientId], channelId); } @@ -263,7 +254,7 @@ export default class FileUpload extends React.Component { request.abort(); delete requests[clientId]; - this.setState({requests: requests}); + this.setState({requests}); } } 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/sidebar.jsx b/web/react/components/sidebar.jsx index 18c360cb8..eaeb7bb91 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -372,7 +372,7 @@ export default class Sidebar extends React.Component { if (channel.status === 'online') { statusIcon = Constants.ONLINE_ICON_SVG; } else if (channel.status === 'away') { - statusIcon = Constants.ONLINE_ICON_SVG; + statusIcon = Constants.AWAY_ICON_SVG; } else { statusIcon = Constants.OFFLINE_ICON_SVG; } diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx index cffff6ee7..32947bd60 100644 --- a/web/react/components/time_since.jsx +++ b/web/react/components/time_since.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import Constants from '../utils/constants.jsx'; import * as Utils from '../utils/utils.jsx'; var Tooltip = ReactBootstrap.Tooltip; @@ -30,7 +31,7 @@ export default class TimeSince extends React.Component { return ( <OverlayTrigger - delayShow={400} + delayShow={Constants.OVERLAY_TIME_DELAY} placement='top' overlay={tooltip} > diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx index 3df9dfedf..45b05f19b 100644 --- a/web/react/components/user_settings/import_theme_modal.jsx +++ b/web/react/components/user_settings/import_theme_modal.jsx @@ -55,6 +55,7 @@ export default class ImportThemeModal extends React.Component { theme.sidebarHeaderBg = colors[1]; theme.sidebarHeaderTextColor = colors[5]; theme.onlineIndicator = colors[6]; + theme.awayIndicator = '#E0B333'; theme.mentionBj = colors[7]; theme.mentionColor = '#ffffff'; theme.centerChannelBg = '#ffffff'; diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index d9c5f58a9..d1266dd3f 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -206,7 +206,7 @@ export default class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + user.email} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email)} > {'Switch to using email and password'} </a> @@ -221,7 +221,7 @@ export default class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GITLAB_SERVICE} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GITLAB_SERVICE} > {'Switch to using GitLab SSO'} </a> @@ -236,7 +236,7 @@ export default class SecurityTab extends React.Component { <div> <a className='btn btn-primary' - href={'/' + teamName + '/claim?email=' + user.email + '&new_type=' + Constants.GOOGLE_SERVICE} + href={'/' + teamName + '/claim?email=' + encodeURIComponent(user.email) + '&new_type=' + Constants.GOOGLE_SERVICE} > {'Switch to using Google SSO'} </a> diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 96d1ef720..d60fea872 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1392,3 +1392,38 @@ export function regenOutgoingHookToken(data, success, error) { } }); } + +export function uploadLicenseFile(formData, success, error) { + $.ajax({ + url: '/api/v1/license/add', + type: 'POST', + data: formData, + cache: false, + contentType: false, + processData: false, + success, + error: function onError(xhr, status, err) { + var e = handleError('uploadLicenseFile', xhr, status, err); + error(e); + } + }); + + track('api', 'api_license_upload'); +} + +export function removeLicenseFile(success, error) { + $.ajax({ + url: '/api/v1/license/remove', + type: 'POST', + cache: false, + contentType: false, + processData: false, + success, + error: function onError(xhr, status, err) { + var e = handleError('removeLicenseFile', xhr, status, err); + error(e); + } + }); + + track('api', 'api_license_upload'); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 0298ce533..d0f34293f 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -163,8 +163,9 @@ export default { OPEN_TEAM: 'O', MAX_POST_LEN: 4000, EMOJI_SIZE: 16, - ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", - OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>", + ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>", + AWAY_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <ellipse class='away--icon' cx='-294.6' cy='394' rx='2.5' ry='2.5'/> <path class='away--icon' d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'/> </g> <path class='away--icon' d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'/> </svg>", + OFFLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <g> <ellipse class='offline--icon' cx='-294.5' cy='394' rx='2.5' ry='2.5'/> <path class='offline--icon' d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'/> </g> </g> <g> <path class='offline--icon' d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'/> </g> </svg>", MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>", COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>", UPDATE_TYPING_MS: 5000, @@ -180,6 +181,7 @@ export default { sidebarHeaderBg: '#2f81b7', sidebarHeaderTextColor: '#FFFFFF', onlineIndicator: '#7DBE00', + awayIndicator: '#DCBD4E', mentionBj: '#136197', mentionColor: '#bfcde8', centerChannelBg: '#f2f4f8', @@ -203,6 +205,7 @@ export default { sidebarHeaderBg: '#2389d7', sidebarHeaderTextColor: '#ffffff', onlineIndicator: '#7DBE00', + awayIndicator: '#DCBD4E', mentionBj: '#2389d7', mentionColor: '#ffffff', centerChannelBg: '#ffffff', @@ -226,6 +229,7 @@ export default { sidebarHeaderBg: '#1B2C3E', sidebarHeaderTextColor: '#FFFFFF', onlineIndicator: '#55C5B2', + awayIndicator: '#A9A14C', mentionBj: '#B74A4A', mentionColor: '#FFFFFF', centerChannelBg: '#2F3E4E', @@ -249,6 +253,7 @@ export default { sidebarHeaderBg: '#1f1f1f', sidebarHeaderTextColor: '#FFFFFF', onlineIndicator: '#0177e7', + awayIndicator: '#A9A14C', mentionBj: '#0177e7', mentionColor: '#FFFFFF', centerChannelBg: '#1F1F1F', @@ -300,6 +305,10 @@ export default { uiName: 'Online Indicator' }, { + id: 'awayIndicator', + uiName: 'Away Indicator' + }, + { id: 'mentionBj', uiName: 'Mention Jewel BG' }, @@ -443,5 +452,6 @@ export default { label: 'embed_preview', description: 'Show preview snippet of links below message' } - } + }, + OVERLAY_TIME_DELAY: 400 }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 2ddd0e5e3..db469952b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -201,11 +201,21 @@ export function displayDate(ticks) { return monthNames[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); } -export function displayTime(ticks) { +export function displayTime(ticks, utc) { const d = new Date(ticks); - let hours = d.getHours(); - let minutes = d.getMinutes(); + let hours; + let minutes; let ampm = ''; + let timezone = ''; + + if (utc) { + hours = d.getUTCHours(); + minutes = d.getUTCMinutes(); + timezone = ' UTC'; + } else { + hours = d.getHours(); + minutes = d.getMinutes(); + } if (minutes <= 9) { minutes = '0' + minutes; @@ -224,7 +234,7 @@ export function displayTime(ticks) { } } - return hours + ':' + minutes + ampm; + return hours + ':' + minutes + ampm + timezone; } export function displayDateTime(ticks) { @@ -572,7 +582,7 @@ export function applyTheme(theme) { changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1); changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6), 1); changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1); - changeCss('.sidebar--left .status path', 'fill:' + theme.sidebarText, 1); + changeCss('.sidebar--left .status .offline--icon, .sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText, 1); changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2); } @@ -617,6 +627,10 @@ export function applyTheme(theme) { changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1); } + if (theme.awayIndicator) { + changeCss('.sidebar--left .status .away--icon', 'fill:' + theme.awayIndicator, 1); + } + if (theme.mentionBj) { changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1); changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1); @@ -1295,5 +1309,22 @@ 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')); } + +export function clearFileInput(elm) { + // clear file input for all modern browsers + try { + elm.value = ''; + if (elm.value) { + elm.type = 'text'; + elm.type = 'file'; + } + } catch (e) { + // Do nothing + } +} diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index abba9de02..b28c7d984 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -174,6 +174,9 @@ .banner__content { width: 80%; } + &.warning { + background: #e60000; + } } .popover { border-radius: 3px; @@ -223,4 +226,4 @@ } } } -}
\ No newline at end of file +} diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 7b7c2d73a..c5cd10b20 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -183,6 +183,7 @@ body.ios { position: absolute; top: 50%; left: 50%; + pointer-events: none; } .overlay__files { diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index e99e21257..6f969ed47 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -42,9 +42,9 @@ margin-right: 6px; width: 12px; display: inline-block; - i, path { + i, path, ellipse { @include opacity(0.5); - &.online--icon { + &.online--icon, &.away--icon { @include opacity(1); } } diff --git a/web/templates/head.html b/web/templates/head.html index 08d8726ea..689c69d3c 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -46,6 +46,7 @@ <script> window.mm_config = {{ .ClientCfg }}; + window.mm_license = {{ .ClientLicense }}; window.mm_team = {{ .Team }}; window.mm_user = {{ .User }}; window.mm_channel = {{ .Channel }}; diff --git a/web/web.go b/web/web.go index 634a9d851..016e0c147 100644 --- a/web/web.go +++ b/web/web.go @@ -32,7 +32,7 @@ func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { props := make(map[string]string) props["Title"] = title - return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg} + return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg, ClientLicense: utils.ClientLicense} } func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { |