Merge branch 'master' into PLT-1429

Этот коммит содержится в:
=Corey Hulen
2016-01-14 09:08:13 -06:00
родитель 0b986ed314 a341dbad2b
Коммит 6d6cada097
104 изменённых файлов: 1941 добавлений и 848 удалений

Просмотреть файл

@@ -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>

Просмотреть файл

@@ -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]} />;

Просмотреть файл

@@ -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='#'

Просмотреть файл

@@ -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>
@@ -581,12 +581,12 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='PushNotificationServer'
ref='PushNotificationServer'
placeholder='E.g.: "https://push.mattermost.com"'
placeholder='E.g.: "https://push-test.mattermost.com"'
defaultValue={this.props.config.EmailSettings.PushNotificationServer}
onChange={this.handleChange}
disabled={!this.state.sendPushNotifications}
/>
<p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
<p className='help-text'>{'Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use https://push-test.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'}</p>
</div>
</div>

Просмотреть файл

@@ -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>

Просмотреть файл

@@ -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 youre 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>
);
}
}

Просмотреть файл

@@ -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>
);

Просмотреть файл

@@ -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}
>

Просмотреть файл

@@ -20,9 +20,14 @@ export default class ChannelInviteModal extends React.Component {
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
this.state = this.getStateFromStores();
// the state gets populated when the modal is shown
this.state = {};
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
return false;
}
if (!Utils.areObjectsEqual(this.props, nextProps)) {
return true;
}
@@ -34,13 +39,25 @@ export default class ChannelInviteModal extends React.Component {
return false;
}
getStateFromStores() {
function getId(user) {
return user.id;
}
var users = UserStore.getActiveOnlyProfiles();
var memberIds = ChannelStore.getCurrentExtraInfo().members.map(getId);
const users = UserStore.getActiveOnlyProfiles();
var loading = $.isEmptyObject(users);
if ($.isEmptyObject(users)) {
return {
loading: true
};
}
// make sure we have all members of this channel before rendering
const extraInfo = ChannelStore.getCurrentExtraInfo();
if (extraInfo.member_count !== extraInfo.members.length) {
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
return {
loading: true
};
}
const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
for (var id in users) {
@@ -55,7 +72,7 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
loading
loading: false
};
}
onShow() {

Просмотреть файл

@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoadingScreen from './loading_screen.jsx';
import MemberList from './member_list.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
@@ -21,9 +22,10 @@ export default class ChannelMembersModal extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleRemove = this.handleRemove.bind(this);
const state = this.getStateFromStores();
state.showInviteModal = false;
this.state = state;
// the rest of the state gets populated when the modal is shown
this.state = {
showInviteModal: false
};
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(this.props, nextProps)) {
@@ -37,8 +39,18 @@ export default class ChannelMembersModal extends React.Component {
return false;
}
getStateFromStores() {
const extraInfo = ChannelStore.getCurrentExtraInfo();
if (extraInfo.member_count !== extraInfo.members.length) {
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
return {
loading: true
};
}
const users = UserStore.getActiveOnlyProfiles();
const memberList = ChannelStore.getCurrentExtraInfo().members;
const memberList = extraInfo.members;
const nonmemberList = [];
for (const id in users) {
@@ -71,14 +83,14 @@ export default class ChannelMembersModal extends React.Component {
return {
nonmemberList,
memberList
memberList,
loading: false
};
}
onShow() {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -89,6 +101,8 @@ export default class ChannelMembersModal extends React.Component {
if (!this.props.show && nextProps.show) {
ChannelStore.addExtraInfoChangeListener(this.onChange);
ChannelStore.addChangeListener(this.onChange);
this.onChange();
} else if (this.props.show && !nextProps.show) {
ChannelStore.removeExtraInfoChangeListener(this.onChange);
ChannelStore.removeChangeListener(this.onChange);
@@ -154,6 +168,21 @@ export default class ChannelMembersModal extends React.Component {
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
let content;
if (this.state.loading) {
content = (<LoadingScreen />);
} else {
content = (
<div className='team-member-list'>
<MemberList
memberList={this.state.memberList}
isAdmin={isAdmin}
handleRemove={this.handleRemove}
/>
</div>
);
}
return (
<div>
<Modal
@@ -178,13 +207,7 @@ export default class ChannelMembersModal extends React.Component {
ref='modalBody'
style={{maxHeight}}
>
<div className='team-member-list'>
<MemberList
memberList={this.state.memberList}
isAdmin={isAdmin}
handleRemove={this.handleRemove}
/>
</div>
{content}
</Modal.Body>
<Modal.Footer>
<button

Просмотреть файл

@@ -32,7 +32,6 @@ export default class CreateComment extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
@@ -239,11 +238,6 @@ export default class CreateComment extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
}
}
handleTextDrop(text) {
const newText = this.state.messageText + text;
this.handleUserInput(newText);
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
}
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -344,7 +338,6 @@ export default class CreateComment extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
onTextDrop={this.handleTextDrop}
postType='comment'
channelId={this.props.channelId}
/>

Просмотреть файл

@@ -40,7 +40,6 @@ export default class CreatePost extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
@@ -276,11 +275,6 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
}
}
handleTextDrop(text) {
const newText = this.state.messageText + text;
this.handleUserInput(newText);
Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.textbox.refs.message), newText.length);
}
removePreview(id) {
const previews = Object.assign([], this.state.previews);
const uploadsInProgress = this.state.uploadsInProgress;
@@ -457,7 +451,6 @@ export default class CreatePost extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
onTextDrop={this.handleTextDrop}
postType='post'
channelId=''
/>

Просмотреть файл

@@ -266,7 +266,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
title={'Download ' + filenameString}
title={'Download \"' + filenameString + '\"'}
className='post-image__name'
>
{trimmedFilename}

Просмотреть файл

@@ -1,10 +1,10 @@
// 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';
import * as Utils from '../utils/utils.jsx';
export default class FileUpload extends React.Component {
constructor(props) {
@@ -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) {
@@ -109,8 +100,6 @@ export default class FileUpload extends React.Component {
if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
} else {
this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
}
}
@@ -120,11 +109,19 @@ export default class FileUpload extends React.Component {
if (this.props.postType === 'post') {
$('.row.main').dragster({
enter() {
$('.center-file-overlay').removeClass('hidden');
enter(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
if (Utils.isFileTransfer(files)) {
$('.center-file-overlay').removeClass('hidden');
}
},
leave() {
$('.center-file-overlay').addClass('hidden');
leave(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
if (Utils.isFileTransfer(files)) {
$('.center-file-overlay').addClass('hidden');
}
},
drop(dragsterEvent, e) {
$('.center-file-overlay').addClass('hidden');
@@ -133,11 +130,19 @@ export default class FileUpload extends React.Component {
});
} else if (this.props.postType === 'comment') {
$('.post-right__container').dragster({
enter() {
$('.right-file-overlay').removeClass('hidden');
enter(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
if (Utils.isFileTransfer(files)) {
$('.right-file-overlay').removeClass('hidden');
}
},
leave() {
$('.right-file-overlay').addClass('hidden');
leave(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
if (Utils.isFileTransfer(files)) {
$('.right-file-overlay').addClass('hidden');
}
},
drop(dragsterEvent, e) {
$('.right-file-overlay').addClass('hidden');
@@ -191,7 +196,7 @@ export default class FileUpload extends React.Component {
var channelId = self.props.channelId || ChannelStore.getCurrentId();
// generate a unique id that can be used by other components to refer back to this file upload
var clientId = utils.generateId();
var clientId = Utils.generateId();
var formData = new FormData();
formData.append('channel_id', channelId);
@@ -213,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);
}
@@ -229,6 +234,18 @@ export default class FileUpload extends React.Component {
});
}
componentWillUnmount() {
let target;
if (this.props.postType === 'post') {
target = $('.row.main');
} else {
target = $('.post-right__container');
}
// jquery-dragster doesn't provide a function to unregister itself so do it manually
target.off('dragenter dragleave dragover drop dragster:enter dragster:leave dragster:over dragster:drop');
}
cancelUpload(clientId) {
var requests = this.state.requests;
var request = requests[clientId];
@@ -237,11 +254,23 @@ export default class FileUpload extends React.Component {
request.abort();
delete requests[clientId];
this.setState({requests: requests});
this.setState({requests});
}
}
render() {
let multiple = true;
if (Utils.isMobileApp()) {
// iOS WebViews don't upload videos properly in multiple mode
multiple = false;
}
let accept = '';
if (Utils.isIosChrome()) {
// iOS Chrome can't upload videos at all
accept = 'image/*';
}
return (
<span
ref='input'
@@ -254,7 +283,8 @@ export default class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
multiple='true'
multiple={multiple}
accept={accept}
/>
</span>
);

Просмотреть файл

@@ -8,6 +8,7 @@ import * as Client from '../utils/client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import TeamStore from '../stores/team_store.jsx';
import ConfirmModal from './confirm_modal.jsx';
@@ -304,6 +305,11 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
var defaultChannelName = '';
if (ChannelStore.getByName(Constants.DEFAULT_CHANNEL)) {
defaultChannelName = ChannelStore.getByName(Constants.DEFAULT_CHANNEL).display_name;
}
if (this.state.emailEnabled && this.state.userCreationEnabled) {
content = (
<div>
@@ -312,10 +318,10 @@ export default class InviteMemberModal extends React.Component {
type='button'
className='btn btn-default'
onClick={this.addInviteFields}
>Add another</button>
>{'Add another'}</button>
<br/>
<br/>
<span>People invited automatically join Town Square channel.</span>
<span>{'People invited automatically join the '}<strong>{defaultChannelName}</strong>{' channel.'}</span>
</div>
);

Просмотреть файл

@@ -223,13 +223,13 @@ export default class PostInfo extends React.Component {
/>
</li>
<li className='col col__reply'>
{comments}
<div
className='dropdown'
ref='dotMenu'
>
{dropdown}
</div>
{comments}
<Overlay
show={this.state.show}
target={() => ReactDOM.findDOMNode(this.refs.dotMenu)}

Просмотреть файл

@@ -24,6 +24,7 @@ export default class PostsView extends React.Component {
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -339,6 +340,10 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
});
}
scrollToBottomAnimated() {
var postList = $(this.refs.postlist);
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
}
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -458,7 +463,7 @@ export default class PostsView extends React.Component {
<ScrollToBottomArrows
isScrolling={this.state.isScrolling}
atBottom={this.wasAtBottom}
onClick={this.scrollToBottom}
onClick={this.scrollToBottomAnimated}
/>
<div
ref='postlist'

Просмотреть файл

@@ -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;
}

Просмотреть файл

@@ -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}
>

Просмотреть файл

@@ -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';

Просмотреть файл

@@ -168,7 +168,7 @@ export function getMoreChannels(force) {
}
}
export function getChannelExtraInfo(id) {
export function getChannelExtraInfo(id, memberLimit) {
let channelId;
if (id) {
channelId = id;
@@ -185,6 +185,7 @@ export function getChannelExtraInfo(id) {
client.getChannelExtraInfo(
channelId,
memberLimit,
(data, textStatus, xhr) => {
callTracker['getChannelExtraInfo_' + channelId] = 0;

Просмотреть файл

@@ -824,10 +824,17 @@ export function getChannelCounts(success, error) {
});
}
export function getChannelExtraInfo(id, success, error) {
export function getChannelExtraInfo(id, memberLimit, success, error) {
let url = '/api/v1/channels/' + id + '/extra_info';
if (memberLimit) {
url += '/' + memberLimit;
}
$.ajax({
url: '/api/v1/channels/' + id + '/extra_info',
url,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success,
error: function onError(xhr, status, err) {
@@ -1399,3 +1406,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');
}

Просмотреть файл

@@ -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',
@@ -299,6 +304,10 @@ export default {
id: 'onlineIndicator',
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
};

Просмотреть файл

@@ -74,6 +74,21 @@ export function isSafari() {
return false;
}
export function isIosChrome() {
// https://developer.chrome.com/multidevice/user-agent
return navigator.userAgent.indexOf('CriOS') !== -1;
}
export function isMobileApp() {
const userAgent = navigator.userAgent;
// the mobile app has different user agents for the native api calls and the shim, so handle them both
const isApi = userAgent.indexOf('Mattermost') !== -1;
const isShim = userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('Chrome') === -1;
return isApi || isShim;
}
export function isInRole(roles, inRole) {
var parts = roles.split(' ');
for (var i = 0; i < parts.length; i++) {
@@ -186,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;
@@ -209,7 +234,7 @@ export function displayTime(ticks) {
}
}
return hours + ':' + minutes + ampm;
return hours + ':' + minutes + ampm + timezone;
}
export function displayDateTime(ticks) {
@@ -557,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);
}
@@ -602,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);
@@ -1276,3 +1305,22 @@ export function fillArray(value, length) {
return arr;
}
// 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) {
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
}
}