summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/.eslintrc3
-rw-r--r--web/react/components/access_history_modal.jsx5
-rw-r--r--web/react/components/admin_console/admin_controller.jsx9
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx15
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx1
-rw-r--r--web/react/components/admin_console/line_chart.jsx50
-rw-r--r--web/react/components/admin_console/team_analytics.jsx393
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/admin_console/team_users.jsx10
-rw-r--r--web/react/components/center_panel.jsx84
-rw-r--r--web/react/components/channel_header.jsx189
-rw-r--r--web/react/components/channel_view.jsx43
-rw-r--r--web/react/components/create_comment.jsx35
-rw-r--r--web/react/components/create_post.jsx207
-rw-r--r--web/react/components/edit_channel_modal.jsx38
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx124
-rw-r--r--web/react/components/edit_post_modal.jsx25
-rw-r--r--web/react/components/error_bar.jsx12
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/find_team.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/invite_member_modal.jsx3
-rw-r--r--web/react/components/login.jsx59
-rw-r--r--web/react/components/mention_list.jsx13
-rw-r--r--web/react/components/more_channels.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx62
-rw-r--r--web/react/components/msg_typing.jsx55
-rw-r--r--web/react/components/navbar.jsx66
-rw-r--r--web/react/components/navbar_dropdown.jsx5
-rw-r--r--web/react/components/new_channel_flow.jsx10
-rw-r--r--web/react/components/new_channel_modal.jsx14
-rw-r--r--web/react/components/popover_list_members.jsx143
-rw-r--r--web/react/components/post.jsx4
-rw-r--r--web/react/components/post_body.jsx6
-rw-r--r--web/react/components/post_info.jsx22
-rw-r--r--web/react/components/post_list.jsx720
-rw-r--r--web/react/components/post_list_container.jsx63
-rw-r--r--web/react/components/posts_view.jsx303
-rw-r--r--web/react/components/posts_view_container.jsx267
-rw-r--r--web/react/components/register_app_modal.jsx135
-rw-r--r--web/react/components/rhs_comment.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx1
-rw-r--r--web/react/components/rhs_thread.jsx10
-rw-r--r--web/react/components/search_autocomplete.jsx28
-rw-r--r--web/react/components/search_bar.jsx25
-rw-r--r--web/react/components/search_results.jsx21
-rw-r--r--web/react/components/search_results_item.jsx4
-rw-r--r--web/react/components/setting_item_max.jsx4
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/settings_sidebar.jsx4
-rw-r--r--web/react/components/sidebar.jsx156
-rw-r--r--web/react/components/sidebar_header.jsx77
-rw-r--r--web/react/components/sidebar_right.jsx68
-rw-r--r--web/react/components/sidebar_right_menu.jsx3
-rw-r--r--web/react/components/signup_team.jsx80
-rw-r--r--web/react/components/team_general_tab.jsx373
-rw-r--r--web/react/components/team_settings.jsx8
-rw-r--r--web/react/components/team_settings_modal.jsx2
-rw-r--r--web/react/components/team_signup_display_name_page.jsx3
-rw-r--r--web/react/components/team_signup_url_page.jsx4
-rw-r--r--web/react/components/team_signup_with_email.jsx2
-rw-r--r--web/react/components/textbox.jsx6
-rw-r--r--web/react/components/time_since.jsx50
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx161
-rw-r--r--web/react/components/tutorial/tutorial_tip.jsx131
-rw-r--r--web/react/components/user_settings/code_theme_chooser.jsx55
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx5
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx39
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx169
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx25
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx100
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx12
-rw-r--r--web/react/package.json1
-rw-r--r--web/react/pages/channel.jsx95
-rw-r--r--web/react/pages/login.jsx1
-rw-r--r--web/react/pages/signup_team.jsx14
-rw-r--r--web/react/stores/post_store.jsx158
-rw-r--r--web/react/stores/search_store.jsx153
-rw-r--r--web/react/stores/socket_store.jsx10
-rw-r--r--web/react/utils/async_client.jsx4
-rw-r--r--web/react/utils/channel_intro_mssages.jsx218
-rw-r--r--web/react/utils/client.jsx51
-rw-r--r--web/react/utils/constants.jsx51
-rw-r--r--web/react/utils/markdown.jsx154
-rw-r--r--web/react/utils/text_formatting.jsx102
-rw-r--r--web/react/utils/utils.jsx192
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss9
-rw-r--r--web/sass-files/sass/partials/_base.scss41
-rw-r--r--web/sass-files/sass/partials/_headers.scss5
-rw-r--r--web/sass-files/sass/partials/_modal.scss7
-rw-r--r--web/sass-files/sass/partials/_popover.scss53
-rw-r--r--web/sass-files/sass/partials/_post.scss28
-rw-r--r--web/sass-files/sass/partials/_responsive.scss46
-rw-r--r--web/sass-files/sass/partials/_search.scss47
-rw-r--r--web/sass-files/sass/partials/_settings.scss61
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss19
-rw-r--r--web/sass-files/sass/partials/_signup.scss28
-rw-r--r--web/sass-files/sass/partials/_statistics.scss84
-rw-r--r--web/sass-files/sass/partials/_tutorial.scss189
-rw-r--r--web/sass-files/sass/styles.scss2
l---------web/static/css/highlight1
-rw-r--r--web/static/help/about.html2
-rw-r--r--web/static/images/themes/code_themes/github.pngbin0 -> 9648 bytes
-rw-r--r--web/static/images/themes/code_themes/monokai.pngbin0 -> 9303 bytes
-rw-r--r--web/static/images/themes/code_themes/solarized_dark.pngbin0 -> 8172 bytes
-rw-r--r--web/static/images/themes/code_themes/solarized_light.pngbin0 -> 8860 bytes
-rw-r--r--web/static/images/tutorialTip.gifbin0 -> 18421 bytes
-rw-r--r--web/static/js/Chart.min.js11
-rw-r--r--web/static/js/babel-es6-polyfill.js2591
-rw-r--r--web/static/js/babel-es6-polyfill.min.js2
-rw-r--r--web/templates/admin_console.html1
-rw-r--r--web/templates/channel.html19
-rw-r--r--web/templates/head.html17
-rw-r--r--web/web.go55
118 files changed, 7567 insertions, 1843 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 6a35d3123..d78068882 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -20,7 +20,8 @@
"globals": {
"React": false,
"ReactDOM": false,
- "ReactBootstrap": false
+ "ReactBootstrap": false,
+ "Chart": false
},
"rules": {
"comma-dangle": [2, "never"],
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index c8af2553d..f0a31ce90 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component {
case '/channels/update':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
break;
- case '/channels/update_desc':
- currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group header';
break;
default:
let userIdField = [];
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f770d166c..8e0ab0555 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -6,6 +6,7 @@ var AdminStore = require('../../stores/admin_store.jsx');
var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
+var Utils = require('../../utils/utils.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
@@ -18,6 +19,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
var TeamUsersTab = require('./team_users.jsx');
+var TeamAnalyticsTab = require('./team_analytics.jsx');
export default class AdminController extends React.Component {
constructor(props) {
@@ -45,7 +47,8 @@ export default class AdminController extends React.Component {
};
if (!props.tab) {
- history.replaceState(null, null, `/admin_console/${this.state.selected}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
}
}
@@ -149,6 +152,10 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'team_analytics') {
+ if (this.state.teams) {
+ tab = <TeamAnalyticsTab 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 b0e01ff17..0d52ae347 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -3,6 +3,7 @@
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
var SelectTeamModal = require('./select_team_modal.jsx');
+var Utils = require('../../utils/utils.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -24,12 +25,13 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
if (this.props.selected === name) {
- if (name === 'team_users') {
+ if (name === 'team_users' || name === 'team_analytics') {
if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
return 'active';
}
@@ -121,6 +123,15 @@ export default class AdminSidebar extends React.Component {
{'- Users'}
</a>
</li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_analytics', team.id)}
+ onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
+ >
+ {'- Statistics'}
+ </a>
+ </li>
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index 8b0f00083..f8fb6d115 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -259,7 +259,6 @@ export default class GitLabSettings extends React.Component {
}
}
-
//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
// <div className='form-group'>
// <label
diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx
new file mode 100644
index 000000000..7e2f95c84
--- /dev/null
+++ b/web/react/components/admin_console/line_chart.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class LineChart extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.initChart = this.initChart.bind(this);
+ this.chart = null;
+ }
+
+ componentDidMount() {
+ this.initChart(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.chart) {
+ this.chart.destroy();
+ this.initChart(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ initChart(props) {
+ var el = ReactDOM.findDOMNode(this);
+ var ctx = el.getContext('2d');
+ this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
+ }
+
+ render() {
+ return (
+ <canvas
+ width={this.props.width}
+ height={this.props.height}
+ />
+ );
+ }
+}
+
+LineChart.propTypes = {
+ width: React.PropTypes.string,
+ height: React.PropTypes.string,
+ data: React.PropTypes.object,
+ options: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
new file mode 100644
index 000000000..0c9d1f61b
--- /dev/null
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -0,0 +1,393 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+var LineChart = require('./line_chart.jsx');
+
+export default class TeamAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = {
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null
+ };
+ }
+
+ componentDidMount() {
+ this.getData(this.props.team.id);
+ }
+
+ getData(teamId) {
+ Client.getAnalytics(
+ teamId,
+ 'standard',
+ (data) => {
+ for (var index in data) {
+ if (data[index].name === 'channel_open_count') {
+ this.setState({channel_open_count: data[index].value});
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ this.setState({channel_private_count: data[index].value});
+ }
+
+ if (data[index].name === 'post_count') {
+ this.setState({post_count: data[index].value});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'post_counts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Total Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({post_counts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'user_counts_with_posts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Active Users With Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({user_counts_with_posts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ this.setState({users});
+
+ var usersList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ usersList.push(users[id]);
+ }
+ }
+
+ usersList.sort((a, b) => {
+ if (a.last_activity_at < b.last_activity_at) {
+ return 1;
+ }
+
+ if (a.last_activity_at > b.last_activity_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var recentActive = [];
+ for (let i = 0; i < usersList.length; i++) {
+ recentActive.push(usersList[i]);
+ if (i > 19) {
+ break;
+ }
+ }
+
+ this.setState({recent_active_users: recentActive});
+
+ usersList.sort((a, b) => {
+ if (a.create_at < b.create_at) {
+ return 1;
+ }
+
+ if (a.create_at > b.create_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var newlyCreated = [];
+ for (let i = 0; i < usersList.length; i++) {
+ newlyCreated.push(usersList[i]);
+ if (i > 19) {
+ break;
+ }
+ }
+
+ this.setState({newly_created_users: newlyCreated});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.setState({
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null
+ });
+
+ this.getData(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
+ <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ </div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Public Groups'}<i className='fa fa-unlock-alt'/></div>
+ <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ </div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
+ <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ </div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
+ <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ </div>
+ </div>
+ );
+
+ var postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.state.post_counts_day != null) {
+ postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.post_counts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.state.user_counts_with_posts_day != null) {
+ usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.user_counts_with_posts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var recentActiveUser = (
+ <div className='recent-active-users'>
+ <div>{'Recent Active Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.recent_active_users != null) {
+ recentActiveUser = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Recent Active Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.recent_active_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var newUsers = (
+ <div className='recent-active-users'>
+ <div>{'Newly Created Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.newly_created_users != null) {
+ newUsers = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Newly Created Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.newly_created_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>{'Statistics for ' + this.props.team.name}</h3>
+ {serverError}
+ <div className='row'>
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ <div className='row'>
+ {postCountsByDay}
+ </div>
+ <div className='row'>
+ {usersWithPostsByDay}
+ </div>
+ <div className='row'>
+ {recentActiveUser}
+ {newUsers}
+ </div>
+ </div>
+ );
+ }
+}
+
+TeamAnalytics.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index 9ecd14a1e..6587184ea 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -32,6 +32,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
+ config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -243,6 +244,39 @@ export default class TeamSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamListing'
+ >
+ {'Enable Team Directory: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='true'
+ ref='EnableTeamListing'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
index ffb412159..b44aba56e 100644
--- a/web/react/components/admin_console/team_users.jsx
+++ b/web/react/components/admin_console/team_users.jsx
@@ -33,14 +33,6 @@ export default class UserList extends React.Component {
this.getTeamProfiles(this.props.team.id);
}
- // this.setState({
- // teamId: this.state.teamId,
- // users: this.state.users,
- // serverError: this.state.serverError,
- // showPasswordModal: this.state.showPasswordModal,
- // user: this.state.user
- // });
-
getTeamProfiles(teamId) {
Client.getProfilesForTeam(
teamId,
@@ -95,8 +87,6 @@ export default class UserList extends React.Component {
}
doPasswordResetDismiss() {
- this.state.showPasswordModal = false;
- this.state.user = null;
this.setState({
teamId: this.state.teamId,
users: this.state.users,
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
new file mode 100644
index 000000000..242c2c637
--- /dev/null
+++ b/web/react/components/center_panel.jsx
@@ -0,0 +1,84 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const TutorialIntroScreens = require('./tutorial/tutorial_intro_screens.jsx');
+const CreatePost = require('./create_post.jsx');
+const PostsViewContainer = require('./posts_view_container.jsx');
+const ChannelHeader = require('./channel_header.jsx');
+const Navbar = require('./navbar.jsx');
+const FileUploadOverlay = require('./file_upload_overlay.jsx');
+
+const PreferenceStore = require('../stores/preference_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const TutorialSteps = Constants.TutorialSteps;
+const Preferences = Constants.Preferences;
+
+export default class CenterPanel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
+ }
+ render() {
+ let postsContainer;
+ if (this.state.showTutorialScreens) {
+ postsContainer = <TutorialIntroScreens />;
+ } else {
+ postsContainer = <PostsViewContainer />;
+ }
+
+ return (
+ <div className='inner__wrap channel__wrap'>
+ <div className='row header'>
+ <div id='navbar'>
+ <Navbar/>
+ </div>
+ </div>
+ <div className='row main'>
+ <FileUploadOverlay
+ id='file_upload_overlay'
+ overlayType='center'
+ />
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <div id='channel-header'>
+ <ChannelHeader />
+ </div>
+ <div id='post-list'>
+ {postsContainer}
+ </div>
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+CenterPanel.defaultProps = {
+};
+
+CenterPanel.propTypes = {
+};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 1b709336f..20f106f30 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -3,7 +3,8 @@
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
-const PostStore = require('../stores/post_store.jsx');
+const SearchStore = require('../stores/search_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -11,6 +12,7 @@ const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.jsx');
@@ -27,7 +29,9 @@ export default class ChannelHeader extends React.Component {
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -35,20 +39,22 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
memberTeam: UserStore.getCurrentUser(),
users: ChannelStore.getCurrentExtraInfo().members,
- searchVisible: PostStore.getSearchResults() !== null
+ searchVisible: SearchStore.getSearchResults() !== null
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
- PostStore.addSearchChangeListener(this.onListenerChange);
+ SearchStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
+ PreferenceStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
- PostStore.removeSearchChangeListener(this.onListenerChange);
- UserStore.addChangeListener(this.onListenerChange);
+ SearchStore.removeSearchChangeListener(this.onListenerChange);
+ UserStore.removeChangeListener(this.onListenerChange);
+ PreferenceStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -110,11 +116,11 @@ export default class ChannelHeader extends React.Component {
bSize='large'
placement='bottom'
className='description'
- onMouseOver={() => this.refs.descriptionOverlay.show()}
- onMouseOut={() => this.refs.descriptionOverlay.hide()}
+ onMouseOver={() => this.refs.headerOverlay.show()}
+ onMouseOut={() => this.refs.headerOverlay.hide()}
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
/>
</Popover>
);
@@ -131,7 +137,7 @@ export default class ChannelHeader extends React.Component {
} else {
contact = this.state.users[0];
}
- channelTitle = contact.nickname || contact.username;
+ channelTitle = Utils.displayUsername(contact.id);
}
}
@@ -144,7 +150,7 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
dropdownContents.push(
<li
- key='edit_description_direct'
+ key='edit_header_direct'
role='presentation'
>
<a
@@ -152,11 +158,11 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
@@ -216,7 +222,7 @@ export default class ChannelHeader extends React.Component {
dropdownContents.push(
<li
- key='set_channel_description'
+ key='set_channel_header'
role='presentation'
>
<a
@@ -224,11 +230,25 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set {channelTerm} Description...
+ Set {channelTerm} Header...
+ </a>
+ </li>
+ );
+ dropdownContents.push(
+ <li
+ key='set_channel_purpose'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ >
+ Set {channelTerm} Purpose...
</a>
</li>
);
@@ -307,84 +327,91 @@ export default class ChannelHeader extends React.Component {
}
return (
- <table className='channel-header alt'>
- <tbody>
- <tr>
- <th>
- <div className='channel-header__info'>
- <div className='dropdown'>
+ <div>
+ <table className='channel-header alt'>
+ <tbody>
+ <tr>
+ <th>
+ <div className='channel-header__info'>
+ <div className='dropdown'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <strong className='heading'>{channelTitle} </strong>
+ <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {dropdownContents}
+ </ul>
+ </div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ placement='bottom'
+ overlay={popoverContent}
+ ref='headerOverlay'
+ >
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ />
+ </OverlayTrigger>
+ </div>
+ </th>
+ <th>
+ <PopoverListMembers
+ members={this.state.users}
+ channelId={channel.id}
+ />
+ </th>
+ <th className='search-bar__container'><NavbarSearchBox /></th>
+ <th>
+ <div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
+ id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
- <strong className='heading'>{channelTitle} </strong>
- <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
- className='dropdown-menu'
+ className='dropdown-menu dropdown-menu-right'
role='menu'
- aria-labelledby='channel_header_dropdown'
+ aria-labelledby='channel_header_right_dropdown'
>
- {dropdownContents}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.searchMentions}
+ >
+ Recent Mentions
+ </a>
+ </li>
</ul>
</div>
- <OverlayTrigger
- trigger={['hover', 'focus']}
- placement='bottom'
- overlay={popoverContent}
- ref='descriptionOverlay'
- >
- <div
- onClick={TextFormatting.handleClick}
- className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
- />
- </OverlayTrigger>
- </div>
- </th>
- <th>
- <PopoverListMembers
- members={this.state.users}
- channelId={channel.id}
- />
- </th>
- <th className='search-bar__container'><NavbarSearchBox /></th>
- <th>
- <div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
- >
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- Recent Mentions
- </a>
- </li>
- </ul>
- </div>
- </th>
- </tr>
- </tbody>
- </table>
+ </th>
+ </tr>
+ </tbody>
+ </table>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
new file mode 100644
index 000000000..3f53a94c2
--- /dev/null
+++ b/web/react/components/channel_view.jsx
@@ -0,0 +1,43 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const CenterPanel = require('../components/center_panel.jsx');
+const Sidebar = require('../components/sidebar.jsx');
+const SidebarRight = require('../components/sidebar_right.jsx');
+const SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+
+export default class ChannelView extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ return (
+ <div className='container-fluid'>
+ <div
+ className='sidebar--right'
+ id='sidebar-right'
+ >
+ <SidebarRight/>
+ </div>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ >
+ <SidebarRightMenu/>
+ </div>
+ <div
+ className='sidebar--left'
+ id='sidebar-left'
+ >
+ <Sidebar/>
+ </div>
+ <CenterPanel />
+ </div>
+ );
+ }
+}
+ChannelView.defaultProps = {
+};
+
+ChannelView.propTypes = {
+};
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 435c7d542..058594165 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const Textbox = require('./textbox.jsx');
const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -27,7 +28,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -36,6 +37,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearCommentDraftUploads();
@@ -45,15 +47,23 @@ export default class CreateComment extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- windowWidth: Utils.windowWidth()
+ windowWidth: Utils.windowWidth(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
}
componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
handleResize() {
this.setState({windowWidth: Utils.windowWidth()});
}
@@ -140,14 +150,16 @@ export default class CreateComment extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
}
commentMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
this.lastTime = t;
}
@@ -161,7 +173,12 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.commentMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -313,7 +330,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 8b5fc4162..1545cdfaa 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -1,20 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+const MsgTyping = require('./msg_typing.jsx');
+const Textbox = require('./textbox.jsx');
+const FileUpload = require('./file_upload.jsx');
+const FilePreview = require('./file_preview.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
+const Utils = require('../utils/utils.jsx');
+
const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const SocketStore = require('../stores/socket_store.jsx');
-const MsgTyping = require('./msg_typing.jsx');
-const Textbox = require('./textbox.jsx');
-const FileUpload = require('./file_upload.jsx');
-const FilePreview = require('./file_preview.jsx');
-const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
@@ -35,13 +41,16 @@ export default class CreatePost extends React.Component {
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
const draft = this.getCurrentDraft();
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
this.state = {
channelId: ChannelStore.getCurrentId(),
@@ -51,8 +60,12 @@ export default class CreatePost extends React.Component {
submitting: false,
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
+ windowHeight: Utils.windowHeight(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER
};
+
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
}
handleResize() {
this.setState({
@@ -122,6 +135,11 @@ export default class CreatePost extends React.Component {
post.message,
false,
(data) => {
+ if (data.response === 'not implemented') {
+ this.sendMessage(post);
+ return;
+ }
+
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
@@ -130,72 +148,82 @@ export default class CreatePost extends React.Component {
}
},
(err) => {
- const state = {};
- state.serverError = err.message;
- state.submitting = false;
- this.setState(state);
- }
- );
- } else {
- post.channel_id = this.state.channelId;
- post.filenames = this.state.previews;
-
- const time = Utils.getTimestamp();
- const userId = UserStore.getCurrentId();
- post.pending_post_id = `${userId}:${time}`;
- post.user_id = userId;
- post.create_at = time;
- post.root_id = this.state.rootId;
- post.parent_id = this.state.parentId;
-
- const channel = ChannelStore.get(this.state.channelId);
-
- PostStore.storePendingPost(post);
- PostStore.storeDraft(channel.id, null);
- this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
-
- Client.createPost(post, channel,
- (data) => {
- AsyncClient.getPosts();
-
- const member = ChannelStore.getMember(channel.id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = Date.now();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
- },
- (err) => {
- const state = {};
-
- if (err.message === 'Invalid RootId parameter') {
- if ($('#post_deleted').length > 0) {
- $('#post_deleted').modal('show');
- }
- PostStore.removePendingPost(post.pending_post_id);
+ if (err.sendMessage) {
+ this.sendMessage(post);
} else {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
+ const state = {};
+ state.serverError = err.message;
+ state.submitting = false;
+ this.setState(state);
}
-
- state.submitting = false;
- this.setState(state);
}
);
+ } else {
+ this.sendMessage(post);
}
}
+ sendMessage(post) {
+ post.channel_id = this.state.channelId;
+ post.filenames = this.state.previews;
+
+ const time = Utils.getTimestamp();
+ const userId = UserStore.getCurrentId();
+ post.pending_post_id = `${userId}:${time}`;
+ post.user_id = userId;
+ post.create_at = time;
+ post.root_id = this.state.rootId;
+ post.parent_id = this.state.parentId;
+
+ const channel = ChannelStore.get(this.state.channelId);
+
+ PostStore.storePendingPost(post);
+ PostStore.storeDraft(channel.id, null);
+ PostStore.jumpPostsViewToBottom();
+ this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
+
+ Client.createPost(post, channel,
+ (data) => {
+ AsyncClient.getPosts();
+
+ const member = ChannelStore.getMember(channel.id);
+ member.msg_count = channel.total_msg_count;
+ member.last_viewed_at = Date.now();
+ ChannelStore.setChannelMember(member);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST,
+ post: data
+ });
+ },
+ (err) => {
+ const state = {};
+
+ if (err.message === 'Invalid RootId parameter') {
+ if ($('#post_deleted').length > 0) {
+ $('#post_deleted').modal('show');
+ }
+ PostStore.removePendingPost(post.pending_post_id);
+ } else {
+ post.state = Constants.POST_FAILED;
+ PostStore.updatePendingPost(post);
+ }
+
+ state.submitting = false;
+ this.setState(state);
+ }
+ );
+ }
postMsgKeyPress(e) {
- if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
this.lastTime = t;
}
@@ -240,8 +268,14 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
+ let message = err;
+ if (message && typeof message !== 'string') {
+ // err is an AppError from the server
+ message = err.message;
+ }
+
if (clientId === -1) {
- this.setState({serverError: err});
+ this.setState({serverError: message});
} else {
const draft = PostStore.getDraft(this.state.channelId);
@@ -252,7 +286,7 @@ export default class CreatePost extends React.Component {
PostStore.storeDraft(this.state.channelId, draft);
- this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
+ this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
}
}
handleTextDrop(text) {
@@ -286,11 +320,13 @@ export default class CreatePost extends React.Component {
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
this.resizePostHolder();
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
onChange() {
@@ -301,6 +337,13 @@ export default class CreatePost extends React.Component {
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER,
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
getFileCount(channelId) {
if (channelId === this.state.channelId) {
return this.state.previews.length + this.state.uploadsInProgress.length;
@@ -309,7 +352,12 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.postMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -330,6 +378,25 @@ export default class CreatePost extends React.Component {
});
}
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Sending Messages'}</h4>
+ <p>{'Type here to write a message.'}</p>
+ <p>{'Click the attachment button to upload an image or a file.'}</p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='top'
+ screens={screens}
+ overlayClass='tip-overlay--chat'
+ />
+ );
+ }
render() {
let serverError = null;
if (this.state.serverError) {
@@ -361,6 +428,11 @@ export default class CreatePost extends React.Component {
postFooterClassName += ' has-error';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<form
id='create_post'
@@ -374,7 +446,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
@@ -399,6 +471,7 @@ export default class CreatePost extends React.Component {
>
<i className='fa fa-paper-plane' />
</a>
+ {tutorialTip}
</div>
<div className={postFooterClassName}>
{postError}
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index d63a1db30..5b3c74e82 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component {
this.onShow = this.onShow.bind(this);
this.state = {
- description: '',
+ header: '',
title: '',
channelId: '',
serverError: ''
@@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component {
return;
}
- data.channel_description = this.state.description.trim();
+ data.channel_header = this.state.header.trim();
- Client.updateChannelDesc(data,
- function handleUpdateSuccess() {
+ Client.updateChannelHeader(data,
+ () => {
this.setState({serverError: ''});
AsyncClient.getChannel(this.state.channelId);
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- }.bind(this),
- function handleUpdateError(err) {
- if (err.message === 'Invalid channel_description parameter') {
- this.setState({serverError: 'This description is too long, please enter a shorter one'});
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
} else {
this.setState({serverError: err.message});
}
- }.bind(this)
+ }
);
}
handleUserInput(e) {
- this.setState({description: e.target.value});
+ this.setState({header: e.target.value});
}
handleClose() {
- this.setState({description: '', serverError: ''});
+ this.setState({header: '', serverError: ''});
}
onShow(e) {
const button = e.relatedTarget;
- this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
+ this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
@@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description
+ {'Edit Header'}
</h4>
);
if (this.state.title) {
@@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description for <span className='name'>{this.state.title}</span>
+ {'Edit Header for '}<span className='name'>{this.state.title}</span>
</h4>
);
}
@@ -105,17 +105,17 @@ export default class EditChannelModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
{editTitle}
</div>
<div className='modal-body'>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
<textarea
className='form-control no-resize'
rows='6'
- ref='channelDesc'
maxLength='1024'
- value={this.state.description}
+ value={this.state.header}
onChange={this.handleUserInput}
/>
{serverError}
@@ -126,14 +126,14 @@ export default class EditChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleEdit}
>
- Save
+ {'Save'}
</button>
</div>
</div>
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
new file mode 100644
index 000000000..4d162cfe7
--- /dev/null
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelPurposeModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleHide = this.handleHide.bind(this);
+ this.handleSave = this.handleSave.bind(this);
+
+ this.state = {serverError: ''};
+ }
+
+ handleHide() {
+ this.setState({serverError: ''});
+
+ if (this.props.onModalDismissed) {
+ this.props.onModalDismissed();
+ }
+ }
+
+ handleSave() {
+ if (!this.props.channel) {
+ return;
+ }
+
+ const data = {
+ channel_id: this.props.channel.id,
+ channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim()
+ };
+
+ Client.updateChannelPurpose(data,
+ () => {
+ AsyncClient.getChannel(this.props.channel.id);
+
+ this.handleHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_purpose parameter') {
+ this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='form-group has-error'>
+ <br/>
+ <label className='control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ let title = <span>{'Edit Purpose'}</span>;
+ if (this.props.channel.display_name) {
+ title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
+ }
+
+ let channelTerm = 'Channel';
+ if (this.props.channel.channelType === 'P') {
+ channelTerm = 'Group';
+ }
+
+ return (
+ <Modal
+ className='modal-edit-channel-purpose'
+ show={this.props.show}
+ onHide={this.handleHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ {title}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <p>{`Describe how this ${channelTerm} should be used.`}</p>
+ <textarea
+ ref='purpose'
+ className='form-control no-resize'
+ rows='6'
+ maxLength='128'
+ defaultValue={this.props.channel.purpose}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.handleHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleSave}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelPurposeModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ channel: React.PropTypes.object,
+ onModalDismissed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index b259b3c18..ef32baa7d 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -6,6 +6,10 @@ var AsyncClient = require('../utils/async_client.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var PostStore = require('../stores/post_store.jsx');
+var PreferenceStore = require('../stores/preference_store.jsx');
+
+var Constants = require('../utils/constants.jsx');
+var KeyCodes = Constants.KeyCodes;
export default class EditPostModal extends React.Component {
constructor() {
@@ -16,6 +20,8 @@ export default class EditPostModal extends React.Component {
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
@@ -51,7 +57,7 @@ export default class EditPostModal extends React.Component {
this.setState({editText: editMessage});
}
handleEditKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
+ if (this.state.ctrlSend === 'false' && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.editbox).blur();
this.handleEdit(e);
@@ -72,6 +78,16 @@ export default class EditPostModal extends React.Component {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.handleEdit(e);
+ }
+ }
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
componentDidMount() {
var self = this;
@@ -84,7 +100,7 @@ export default class EditPostModal extends React.Component {
if (!button) {
return;
}
- self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
+ self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refocusid')});
});
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
@@ -101,9 +117,11 @@ export default class EditPostModal extends React.Component {
});
PostStore.addEditPostListener(this.handleEditPostEvent);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
}
componentWillUnmount() {
- PostStore.removeEditPostListener(this.handleEditPostEvent);
+ PostStore.removeEditPostListner(this.handleEditPostEvent);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
render() {
var error = (<div className='form-group'><br /></div>);
@@ -138,6 +156,7 @@ export default class EditPostModal extends React.Component {
<Textbox
onUserInput={this.handleEditInput}
onKeyPress={this.handleEditKeyPress}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
id='edit_textbox'
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index 6311d9460..f098384aa 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
- this.prevTimer = null;
this.state = ErrorStore.getLastError();
- if (this.isValidError(this.state)) {
- this.prevTimer = setTimeout(this.handleClose, 10000);
- }
}
isValidError(s) {
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
onErrorChange() {
var newState = ErrorStore.getLastError();
- if (this.prevTimer != null) {
- clearInterval(this.prevTimer);
- this.prevTimer = null;
- }
-
if (newState) {
this.setState(newState);
- if (!this.isConnectionError(newState)) {
- this.prevTimer = setTimeout(this.handleClose, 10000);
- }
} else {
this.setState({message: null});
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 4d4e8390c..e707e32f5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={filenameString}
+ title={'Download ' + filenameString}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index e324f3666..bd3c11973 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -50,7 +50,7 @@ export default class FindTeam extends React.Component {
if (this.state.sent) {
return (
<div>
- <h4>{'Find Your team'}</h4>
+ <h4>{'Find Your teams'}</h4>
<p>{'An email was sent with links to any teams to which you are a member.'}</p>
</div>
);
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 325e86f3d..8839bc3c7 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component {
<br /><br />
</p>
<textarea
- className='form-control no-resize'
+ className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.state.value}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 86a4b04cf..bea700725 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -4,6 +4,7 @@
var utils = require('../utils/utils.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var ConfirmModal = require('./confirm_modal.jsx');
export default class InviteMemberModal extends React.Component {
@@ -292,7 +293,7 @@ export default class InviteMemberModal extends React.Component {
} else {
var teamInviteLink = null;
if (currentUser && this.props.teamType === 'O') {
- var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id;
+ var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id;
var link =
(
<a
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 108735caf..2b9ce67ca 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -101,7 +101,7 @@ export default class Login extends React.Component {
href={'/' + teamName + '/login/gitlab'}
>
<span className='icon' />
- <span>with GitLab</span>
+ <span>{'with GitLab'}</span>
</a>
);
}
@@ -154,7 +154,7 @@ export default class Login extends React.Component {
type='submit'
className='btn btn-primary'
>
- Sign in
+ {'Sign in'}
</button>
</div>
</div>
@@ -166,7 +166,7 @@ export default class Login extends React.Component {
<div>
{loginMessage}
<div className='or__container'>
- <span>or</span>
+ <span>{'or'}</span>
</div>
</div>
);
@@ -176,16 +176,48 @@ export default class Login extends React.Component {
if (emailSignup) {
forgotPassword = (
<div className='form-group'>
- <a href={'/' + teamName + '/reset_password'}>I forgot my password</a>
+ <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a>
+ </div>
+ );
+ }
+
+ let userSignUp = null;
+ if (this.props.inviteId) {
+ userSignUp = (
+ <div>
+ <span>{`Don't have an account? `}
+ <a
+ href={'/signup_user_complete/?id=' + this.props.inviteId}
+ className='signup-team-login'
+ >
+ {'Create one now'}
+ </a>
+ </span>
+ </div>
+ );
+ }
+
+ let teamSignUp = null;
+ if (global.window.mm_config.EnableTeamCreation === 'true') {
+ teamSignUp = (
+ <div className='margin--extra'>
+ <span>{'Want to create your own team? '}
+ <a
+ href='/'
+ className='signup-team-login'
+ >
+ {'Sign up now'}
+ </a>
+ </span>
</div>
);
}
return (
<div className='signup-team__container'>
- <h5 className='margin--less'>Sign in to:</h5>
+ <h5 className='margin--less'>{'Sign in to:'}</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2>
+ <h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2>
<form onSubmit={this.handleSubmit}>
{verifiedBox}
<div className={'form-group' + errorClass}>
@@ -193,20 +225,12 @@ export default class Login extends React.Component {
</div>
{loginMessage}
{emailSignup}
+ {userSignUp}
<div className='form-group margin--extra form-group--small'>
<span><a href='/find_team'>{'Find other teams'}</a></span>
</div>
{forgotPassword}
- <div className='margin--extra'>
- <span>{'Want to create your own team? '}
- <a
- href='/'
- className='signup-team-login'
- >
- Sign up now
- </a>
- </span>
- </div>
+ {teamSignUp}
</form>
</div>
);
@@ -219,5 +243,6 @@ Login.defaultProps = {
};
Login.propTypes = {
teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string,
+ inviteId: React.PropTypes.string
};
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
index 8c1da942d..61a24c09c 100644
--- a/web/react/components/mention_list.jsx
+++ b/web/react/components/mention_list.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Mention = require('./mention.jsx');
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
}
}
componentDidMount() {
- PostStore.addMentionDataChangeListener(this.onListenerChange);
+ SearchStore.addMentionDataChangeListener(this.onListenerChange);
$('.post-right__scroll').scroll(this.onScroll);
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
$(document).click(this.onClick);
}
componentWillUnmount() {
- PostStore.removeMentionDataChangeListener(this.onListenerChange);
+ SearchStore.removeMentionDataChangeListener(this.onListenerChange);
$('body').off('keydown.mentionlist', '#' + this.props.id);
}
@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
if (this.state.selectedMention === index) {
isFocused = 'mentions-focus';
}
+
+ if (!users[i].secondary_text) {
+ users[i].secondary_text = Utils.getFullName(users[i]);
+ }
+
mentions[index] = (
<Mention
key={'mention_key_' + index}
ref={'mention' + index}
username={users[i].username}
- secondary_text={Utils.getFullName(users[i])}
+ secondary_text={users[i].secondary_text}
id={users[i].id}
listId={index}
isFocused={isFocused}
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index a0084ad30..c4f831c2e 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component {
<tr key={channel.id}>
<td>
<p className='more-name'>{channel.display_name}</p>
- <p className='more-description'>{channel.description}</p>
+ <p className='more-purpose'>{channel.purpose}</p>
</td>
<td className='td--action'>
{joinButton}
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 41746d1d7..b0232fc08 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -1,13 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const Constants = require('../utils/constants.jsx');
-const Client = require('../utils/client.jsx');
const Modal = ReactBootstrap.Modal;
-const PreferenceStore = require('../stores/preference_store.jsx');
-const TeamStore = require('../stores/team_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
@@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component {
}
handleShowDirectChannel(teammate, e) {
+ e.preventDefault();
+
if (this.state.loadingDMChannel !== -1) {
return;
}
- e.preventDefault();
-
- const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
- let channel = ChannelStore.getByName(channelName);
-
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
- AsyncClient.savePreferences([preference]);
-
- if (channel) {
- Utils.switchChannel(channel);
-
- this.handleHide();
- } else {
- this.setState({loadingDMChannel: teammate.id});
-
- channel = {
- name: channelName,
- last_post_at: 0,
- total_msg_count: 0,
- type: 'D',
- display_name: teammate.username,
- teammate_id: teammate.id,
- status: UserStore.getStatus(teammate.id)
- };
-
- Client.createDirectChannel(
- channel,
- teammate.id,
- (data) => {
- this.setState({loadingDMChannel: -1});
-
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
-
- this.handleHide();
- },
- () => {
- this.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
- }
- );
- }
+ this.setState({loadingDMChannel: teammate.id});
+ Utils.openDirectChannelToUser(
+ teammate,
+ (channel) => {
+ Utils.switchChannel(channel);
+ this.setState({loadingDMChannel: -1});
+ this.handleHide();
+ },
+ () => {
+ this.setState({loadingDMChannel: -1});
+ }
+ );
}
handleUserChange() {
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index 1bd23c55c..ccf8a2445 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component {
constructor(props) {
super(props);
- this.timer = null;
- this.lastTime = 0;
-
this.onChange = this.onChange.bind(this);
+ this.updateTypingText = this.updateTypingText.bind(this);
+ this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
+ this.typingUsers = {};
this.state = {
text: ''
};
@@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component {
componentWillReceiveProps(newProps) {
if (this.props.channelId !== newProps.channelId) {
- this.setState({text: ''});
+ this.updateTypingText();
}
}
@@ -36,28 +36,51 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
+ let username = 'Someone';
if (msg.action === SocketEvents.TYPING &&
this.props.channelId === msg.channel_id &&
this.props.parentId === msg.props.parent_id) {
- this.lastTime = new Date().getTime();
-
- var username = 'Someone';
if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
- this.setState({text: username + ' is typing...'});
-
- if (!this.timer) {
- this.timer = setInterval(function myTimer() {
- if ((new Date().getTime() - this.lastTime) > 8000) {
- this.setState({text: ''});
- }
- }.bind(this), 3000);
+ if (this.typingUsers[username]) {
+ clearTimeout(this.typingUsers[username]);
}
+
+ this.typingUsers[username] = setTimeout(function myTimer(user) {
+ delete this.typingUsers[user];
+ this.updateTypingText();
+ }.bind(this, username), Constants.UPDATE_TYPING_MS);
+
+ this.updateTypingText();
} else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
- this.setState({text: ''});
+ if (UserStore.hasProfile(msg.user_id)) {
+ username = UserStore.getProfile(msg.user_id).username;
+ }
+ clearTimeout(this.typingUsers[username]);
+ delete this.typingUsers[username];
+ this.updateTypingText();
+ }
+ }
+
+ updateTypingText() {
+ const users = Object.keys(this.typingUsers);
+ let text = '';
+ switch (users.length) {
+ case 0:
+ text = '';
+ break;
+ case 1:
+ text = users[0] + ' is typing...';
+ break;
+ default:
+ const last = users.pop();
+ text = users.join(', ') + ' and ' + last + ' are typing...';
+ break;
}
+
+ this.setState({text});
}
render() {
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index f9cd525fd..f7778f25f 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -26,7 +27,9 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -106,22 +109,35 @@ export default class Navbar extends React.Component {
</li>
);
- var setChannelDescriptionOption = (
+ var setChannelHeaderOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
+ var setChannelPurposeOption = null;
+ if (!isDirect) {
+ setChannelPurposeOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ />
+ </li>
+ );
+ }
+
var addMembersOption;
var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) {
@@ -249,7 +265,8 @@ export default class Navbar extends React.Component {
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
- {setChannelDescriptionOption}
+ {setChannelHeaderOption}
+ {setChannelPurposeOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
@@ -335,10 +352,10 @@ export default class Navbar extends React.Component {
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
options={{singleline: true, mentionHighlight: false}}
/>
</Popover>
@@ -360,20 +377,20 @@ export default class Navbar extends React.Component {
}
}
- if (channel.description.length === 0) {
+ if (channel.header.length === 0) {
popoverContent = (
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<div>
- {'No channel description yet.'}
+ {'No channel header yet.'}
<br/>
<a
href='#'
data-toggle='modal'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
data-target='#edit_channel'
@@ -392,17 +409,24 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return (
- <nav
- className='navbar navbar-default navbar-fixed-top'
- role='navigation'
- >
- <div className='container-fluid theme'>
- <div className='navbar-header'>
- {collapseButtons}
- {channelMenuDropdown}
+ <div>
+ <nav
+ className='navbar navbar-default navbar-fixed-top'
+ role='navigation'
+ >
+ <div className='container-fluid theme'>
+ <div className='navbar-header'>
+ {collapseButtons}
+ {channelMenuDropdown}
+ </div>
</div>
- </div>
- </nav>
+ </nav>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 2b68645e5..f43bdffdf 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component {
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
@@ -103,7 +104,7 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- if (this.props.teamType === 'O') {
+ if (this.props.teamType === Constants.OPEN_TEAM) {
teamLink = (
<li>
<a
@@ -111,7 +112,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#get_link'
data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
>
{'Get Team Invite Link'}
</a>
diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx
index 186cfc2b0..d6280d118 100644
--- a/web/react/components/new_channel_flow.jsx
+++ b/web/react/components/new_channel_flow.jsx
@@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
};
}
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
});
}
@@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component {
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
- channel.description = this.state.channelDescription;
+ channel.purpose = this.state.channelPurpose;
channel.type = this.state.channelType;
Client.createChannel(channel,
@@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
- channelDescription: data.description
+ channelPurpose: data.purpose
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
@@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
- description: this.state.channelDescription
+ purpose: this.state.channelPurpose
};
let showChannelModal = false;
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index 4e6280c99..c0cea496f 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() {
const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
- description: ReactDOM.findDOMNode(this.refs.channel_desc).value
+ purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
};
this.props.onDataChanged(newData);
}
@@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component {
</div>
<div className='form-group less'>
<div className='col-sm-3'>
- <label className='form__label control-label'>{'Description'}</label>
+ <label className='form__label control-label'>{'Purpose'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-9'>
<textarea
className='form-control no-resize'
- ref='channel_desc'
+ ref='channel_purpose'
rows='4'
- placeholder='Description'
- maxLength='1024'
- value={this.props.channelData.description}
+ placeholder='Purpose'
+ maxLength='128'
+ value={this.props.channelData.purpose}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
- {'Description helps others decide whether to join this channel.'}
+ {`Describe how this ${channelTerm} should be used.`}
</p>
{serverError}
</div>
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 155e88600..f3c0fa0b4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -3,9 +3,23 @@
var UserStore = require('../stores/user_store.jsx');
var Popover = ReactBootstrap.Popover;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+var Overlay = ReactBootstrap.Overlay;
+const Utils = require('../utils/utils.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
export default class PopoverListMembers extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
+ this.closePopover = this.closePopover.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({showPopover: false});
+ }
+
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
@@ -27,12 +41,40 @@ export default class PopoverListMembers extends React.Component {
}
};
}
+
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.memebersPopover)).find('.popover-content').perfectScrollbar();
+ }
+
+ handleShowDirectChannel(teammate, e) {
+ e.preventDefault();
+
+ Utils.openDirectChannelToUser(
+ teammate,
+ (channel, channelAlreadyExisted) => {
+ Utils.switchChannel(channel);
+ if (channelAlreadyExisted) {
+ this.closePopover();
+ }
+ },
+ () => {
+ this.closePopover();
+ }
+ );
+ }
+
+ closePopover() {
+ this.setState({showPopover: false});
+ }
+
render() {
let popoverHtml = [];
let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
+ const currentUserId = UserStore.getCurrentId();
+ const ch = ChannelStore.getCurrent();
if (members && teamMembers) {
members.sort((a, b) => {
@@ -40,13 +82,69 @@ export default class PopoverListMembers extends React.Component {
});
members.forEach((m, i) => {
+ const details = [];
+
+ const fullName = Utils.getFullName(m);
+ if (fullName) {
+ details.push(
+ <span
+ key={`${m.id}__full-name`}
+ className='full-name'
+ >
+ {fullName}
+ </span>
+ );
+ }
+
+ if (m.nickname) {
+ const separator = fullName ? ' - ' : '';
+ details.push(
+ <span
+ key={`${m.nickname}__nickname`}
+ >
+ {separator + m.nickname}
+ </span>
+ );
+ }
+
+ let button = '';
+ if (currentUserId !== m.id && ch.type !== 'D') {
+ button = (
+ <a
+ href='#'
+ className='btn-message'
+ onClick={(e) => this.handleShowDirectChannel(m, e)}
+ >
+ {'Message'}
+ </a>
+ );
+ }
+
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
- className='text--nowrap'
+ className='text-nowrap'
key={'popover-member-' + i}
>
- {m.username}
+
+ <img
+ className='profile-img rounded pull-left'
+ width='26px'
+ height='26px'
+ src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
+ />
+ <div className='pull-left'>
+ <div
+ className='more-name'
+ >
+ {m.username}
+ </div>
+ </div>
+ <div
+ className='pull-right'
+ >
+ {button}
+ </div>
</div>
);
count++;
@@ -61,29 +159,36 @@ export default class PopoverListMembers extends React.Component {
}
return (
- <OverlayTrigger
- trigger='click'
- placement='bottom'
- rootClose={true}
- overlay={
+ <div>
+ <div
+ id='member_popover'
+ ref='member_popover_target'
+ onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
+ >
+ <div>
+ {countText}
+ <span
+ className='fa fa-user'
+ aria-hidden='true'
+ />
+ </div>
+ </div>
+ <Overlay
+ rootClose={true}
+ onHide={this.closePopover}
+ show={this.state.showPopover}
+ target={() => this.state.popoverTarget}
+ placement='bottom'
+ >
<Popover
+ ref='memebersPopover'
title='Members'
id='member-list-popover'
>
{popoverHtml}
</Popover>
- }
- >
- <div id='member_popover'>
- <div>
- {countText}
- <span
- className='fa fa-user'
- aria-hidden='true'
- />
- </div>
+ </Overlay>
</div>
- </OverlayTrigger>
);
}
}
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index dedac8951..c3c5b3e0b 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -204,7 +204,6 @@ export default class Post extends React.Component {
posts={posts}
handleCommentClick={this.handleCommentClick}
retryPost={this.retryPost}
- resize={this.props.resize}
/>
<PostInfo
ref='info'
@@ -228,6 +227,5 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool,
- resize: React.PropTypes.func
+ isLastComment: React.PropTypes.bool
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 45eae8c6a..e1f495d54 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -50,7 +50,6 @@ export default class PostBody extends React.Component {
componentDidUpdate() {
this.parseEmojis();
- this.props.resize();
}
componentWillReceiveProps(nextProps) {
@@ -297,7 +296,7 @@ export default class PostBody extends React.Component {
}
let embed;
- if (filenames.length === 0 && this.state.links) {
+ if (filenames.length === 0 && this.state.links && this.state.links.length > 0) {
embed = this.createEmbed(this.state.links[0]);
}
@@ -338,6 +337,5 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired,
- resize: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 36260d77c..a01d842e5 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -3,10 +3,9 @@
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
+var TimeSince = require('./time_since.jsx');
var Constants = require('../utils/constants.jsx');
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -44,7 +43,7 @@ export default class PostInfo extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
- data-refoucsid='#post_textbox'
+ data-refocusid='#post_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
@@ -126,7 +125,7 @@ export default class PostInfo extends React.Component {
lastCommentClass = ' comment-icon__container__show';
}
- if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
+ if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
comments = (
<a
href='#'
@@ -144,21 +143,12 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
- let tooltip = <Tooltip id={post.id + 'tooltip'}>{`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}</Tooltip>;
-
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <OverlayTrigger
- delayShow={500}
- container={this}
- placement='top'
- overlay={tooltip}
- >
- <time className='post-profile-time'>
- {utils.displayDateTime(post.create_at)}
- </time>
- </OverlayTrigger>
+ <TimeSince
+ eventTime={post.create_at}
+ />
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
deleted file mode 100644
index 3ceef478c..000000000
--- a/web/react/components/post_list.jsx
+++ /dev/null
@@ -1,720 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const Post = require('./post.jsx');
-const UserProfile = require('./user_profile.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
-
-const PostStore = require('../stores/post_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
-const SocketEvents = Constants.SocketEvents;
-
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-
-export default class PostList extends React.Component {
- constructor(props) {
- super(props);
-
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
- this.loadInProgress = false;
-
- this.onChange = this.onChange.bind(this);
- this.onTimeChange = this.onTimeChange.bind(this);
- this.onSocketChange = this.onSocketChange.bind(this);
- this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
- this.loadMorePosts = this.loadMorePosts.bind(this);
- this.loadFirstPosts = this.loadFirstPosts.bind(this);
- this.activate = this.activate.bind(this);
- this.deactivate = this.deactivate.bind(this);
- this.handleResize = this.handleResize.bind(this);
- this.resizePostList = this.resizePostList.bind(this);
- this.updateScroll = this.updateScroll.bind(this);
-
- const state = this.getStateFromStores(props.channelId);
- state.numToDisplay = Constants.POST_CHUNK_SIZE;
- state.isFirstLoadComplete = false;
- state.windowHeight = Utils.windowHeight();
-
- this.state = state;
- }
- getStateFromStores(id) {
- var postList = PostStore.getPosts(id);
-
- if (postList != null) {
- var deletedPosts = PostStore.getUnseenDeletedPosts(id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (var pid in deletedPosts) {
- if (deletedPosts.hasOwnProperty(pid)) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
- }
- }
-
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
- var pendingPostList = PostStore.getPendingPosts(id);
-
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (var ppid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(ppid)) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
- }
- }
-
- return {
- postList
- };
- }
- componentDidMount() {
- window.onload = () => this.scrollToBottom();
- if (this.props.isActive) {
- this.activate();
- this.loadFirstPosts(this.props.channelId);
- }
- }
- componentWillUnmount() {
- this.deactivate();
- }
- activate() {
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
-
- PostStore.clearUnseenDeletedPosts(this.props.channelId);
- PostStore.addChangeListener(this.onChange);
- UserStore.addStatusesChangeListener(this.onTimeChange);
- PreferenceStore.addChangeListener(this.onTimeChange);
- SocketStore.addChangeListener(this.onSocketChange);
-
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
-
- window.addEventListener('resize', this.handleResize);
-
- postHolder.on('scroll', () => {
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
-
- if (position >= bottom) {
- this.scrolled = false;
- } else {
- this.scrolled = true;
- }
-
- if (this.isUserScroll) {
- this.userHasSeenNew = true;
- }
- this.isUserScroll = true;
-
- $('.top-visible-post').removeClass('top-visible-post');
-
- $(ReactDOM.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
- if ($(this).position().top + $(this).height() / 2 > 0) {
- $(this).addClass('top-visible-post');
- return false;
- }
- });
- });
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (!this.state.isFirstLoadComplete) {
- this.loadFirstPosts(this.props.channelId);
- }
-
- this.resizePostList();
- this.onChange();
- this.scrollToBottom();
- }
- deactivate() {
- PostStore.removeChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onTimeChange);
- SocketStore.removeChangeListener(this.onSocketChange);
- PreferenceStore.removeChangeListener(this.onTimeChange);
- $('body').off('click.userpopover');
-
- window.removeEventListener('resize', this.handleResize);
-
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder.off('scroll');
- }
- componentDidUpdate(prevProps, prevState) {
- if (!this.props.isActive) {
- return;
- }
-
- if (prevState.windowHeight !== this.state.windowHeight) {
- this.resizePostList();
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (this.state.postList == null || prevState.postList == null) {
- this.scrollToBottom();
- return;
- }
-
- var order = this.state.postList.order || [];
- var posts = this.state.postList.posts || {};
- var oldOrder = prevState.postList.order || [];
- var oldPosts = prevState.postList.posts || {};
- var userId = UserStore.getCurrentId();
- var firstPost = posts[order[0]] || {};
- var isNewPost = oldOrder.indexOf(order[0]) === -1;
-
- if (this.props.isActive && !prevProps.isActive) {
- this.scrollToBottom();
- } else if (oldOrder.length === 0) {
- this.scrollToBottom();
-
- // the user is scrolled to the bottom
- } else if (!this.scrolled) {
- this.scrollToBottom();
-
- // there's a new post and
- // it's by the user (and not from their webhook) and not a comment
- } else if (isNewPost &&
- userId === firstPost.user_id &&
- !firstPost.props.from_webhook &&
- !Utils.isComment(firstPost)) {
- this.scrollToBottom(true);
-
- // the user clicked 'load more messages'
- } else if (this.gotMorePosts && oldOrder.length > 0) {
- let index;
- if (prevState.numToDisplay >= oldOrder.length) {
- index = oldOrder.length - 1;
- } else {
- index = prevState.numToDisplay;
- }
- const lastPost = oldPosts[oldOrder[index]];
- $('#post_' + lastPost.id)[0].scrollIntoView();
- this.gotMorePosts = false;
- } else {
- this.scrollTo(this.prevScrollTop);
- }
- }
- componentWillUpdate() {
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- this.prevScrollTop = postHolder.scrollTop();
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.isActive === true && this.props.isActive === false) {
- this.activate();
- } else if (nextProps.isActive === false && this.props.isActive === true) {
- this.deactivate();
- }
- }
- updateScroll() {
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
- handleResize() {
- this.setState({
- windowHeight: Utils.windowHeight()
- });
- }
- resizePostList() {
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#create_post').length > 0) {
- const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
- postHolder.css('height', height + 'px');
- }
- }
- scrollTo(val) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder[0].scrollTop = val;
- }
- scrollToBottom(force) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) {
- $('#new_message_' + this.props.channelId)[0].scrollIntoView();
- } else {
- postHolder.addClass('hide-scroll');
- postHolder[0].scrollTop = postHolder[0].scrollHeight;
- postHolder.removeClass('hide-scroll');
- }
- }
- loadFirstPosts(id) {
- if (this.loadInProgress) {
- return;
- }
-
- if (this.props.channelId == null) {
- return;
- }
-
- this.loadInProgress = true;
- Client.getPosts(
- id,
- PostStore.getLatestUpdate(id),
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- },
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- }
- );
- }
- onChange() {
- var newState = this.getStateFromStores(this.props.channelId);
-
- if (!Utils.areStatesEqual(newState.postList, this.state.postList)) {
- this.setState(newState);
- }
- }
- onSocketChange(msg) {
- if (msg.action === SocketEvents.POST_DELETED) {
- var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
- var activeRootPostId = '';
- if (activeRoot && activeRoot.id.length > 0) {
- activeRootPostId = activeRoot.id;
- }
-
- if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
- $('#post_deleted').modal('show');
- }
- }
- }
- onTimeChange() {
- if (!this.state.postList) {
- return;
- }
-
- for (var id in this.state.postList.posts) {
- if (!this.refs[id]) {
- continue;
- }
- this.refs[id].forceUpdateInfo();
- }
- }
- createDMIntroMessage(channel) {
- var teammate = Utils.getDirectTeammate(channel.id);
-
- if (teammate) {
- var teammateName = teammate.username;
- if (teammate.nickname.length > 0) {
- teammateName = teammate.nickname;
- }
-
- return (
- <div className='channel-intro'>
- <div className='post-profile-img__container channel-intro-img'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
- height='50'
- width='50'
- />
- </div>
- <div className='channel-intro-profile'>
- <strong><UserProfile userId={teammate.id} /></strong>
- </div>
- <p className='channel-intro-text'>
- {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
- {'Direct messages and files shared here are not shown to people outside this area.'}
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- </div>
- );
- }
-
- return (
- <div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
- </div>
- );
- }
- createChannelIntroMessage(channel) {
- if (channel.type === 'D') {
- return this.createDMIntroMessage(channel);
- } else if (ChannelStore.isDefault(channel)) {
- return this.createDefaultIntroMessage(channel);
- } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return this.createOffTopicIntroMessage(channel);
- } else if (channel.type === 'O' || channel.type === 'P') {
- return this.createStandardIntroMessage(channel);
- }
- }
- createDefaultIntroMessage(channel) {
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- {'Welcome to ' + channel.display_name + '!'}
- <br/><br/>
- {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
- <br/><br/>
- {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'}
- <br/>
- </p>
- </div>
- );
- }
- createOffTopicIntroMessage(channel) {
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#channel_invite'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this channel'}
- </a>
- </div>
- );
- }
- getChannelCreator(channel) {
- if (channel.creator_id.length > 0) {
- var creator = UserStore.getProfile(channel.creator_id);
- if (creator) {
- return creator.username;
- }
- }
-
- var members = ChannelStore.getExtraInfo(channel.id).members;
- for (var i = 0; i < members.length; i++) {
- if (Utils.isAdmin(members[i].roles)) {
- return members[i].username;
- }
- }
- }
- createStandardIntroMessage(channel) {
- var uiName = channel.display_name;
- var creatorName = '';
-
- var uiType;
- var memberMessage;
- if (channel.type === 'P') {
- uiType = 'private group';
- memberMessage = ' Only invited members can see this private group.';
- } else {
- uiType = 'channel';
- memberMessage = ' Any member can join and read this channel.';
- }
-
- var createMessage;
- if (creatorName === '') {
- createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + Utils.displayDate(channel.create_at) + '.';
- } else {
- createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{Utils.displayDate(channel.create_at)}</strong></span>);
- }
-
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
- <p className='channel-intro__content'>
- {createMessage}
- {memberMessage}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#channel_invite'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
- </a>
- </div>
- );
- }
- createPosts(posts, order) {
- var postCtls = [];
- var previousPostDay = new Date(0);
- var userId = UserStore.getCurrentId();
-
- var renderedLastViewed = false;
- var lastViewed = Number.MAX_VALUE;
-
- if (ChannelStore.getMember(this.props.channelId) != null) {
- lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at;
- }
-
- var numToDisplay = this.state.numToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (var i = numToDisplay; i >= 0; i--) {
- var post = posts[order[i]];
- var parentPost = posts[post.parent_id];
-
- var sameUser = false;
- var sameRoot = false;
- var hideProfilePic = false;
- var prevPost = posts[order[i + 1]];
-
- if (prevPost) {
- sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
-
- sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
-
- // hide the profile pic if:
- // the previous post was made by the same user as the current post,
- // the previous post is not a comment,
- // the current post is not a comment,
- // the current post is not from a webhook
- // and the previous post is not from a webhook
- if ((prevPost.user_id === post.user_id) &&
- !Utils.isComment(prevPost) &&
- !Utils.isComment(post) &&
- (!post.props || !post.props.from_webhook) &&
- (!prevPost.props || !prevPost.props.from_webhook)) {
- hideProfilePic = true;
- }
- }
-
- // check if it's the last comment in a consecutive string of comments on the same post
- // it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
-
- var postCtl = (
- <Post
- key={post.id + 'postKey'}
- ref={post.id}
- sameUser={sameUser}
- sameRoot={sameRoot}
- post={post}
- parentPost={parentPost}
- posts={posts}
- hideProfilePic={hideProfilePic}
- isLastComment={isLastComment}
- resize={this.updateScroll}
- />
- );
-
- const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
- if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
- postCtls.push(
- <div
- key={currentPostDay.toDateString()}
- className='date-separator'
- >
- <hr className='separator__hr' />
- <div className='separator__text'>{currentPostDay.toDateString()}</div>
- </div>
- );
- }
-
- if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
- renderedLastViewed = true;
-
- // Temporary fix to solve ie11 rendering issue
- let newSeparatorId = '';
- if (!Utils.isBrowserIE()) {
- newSeparatorId = 'new_message_' + this.props.channelId;
- }
- postCtls.push(
- <div
- id={newSeparatorId}
- key='unviewed'
- className='new-separator'
- >
- <hr
- className='separator__hr'
- />
- <div className='separator__text'>{'New Messages'}</div>
- </div>
- );
- }
- postCtls.push(postCtl);
- previousPostDay = currentPostDay;
- }
-
- return postCtls;
- }
- loadMorePosts() {
- if (this.state.postList == null) {
- return;
- }
-
- var posts = this.state.postList.posts;
- var order = this.state.postList.order;
- var channelId = this.props.channelId;
-
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
-
- Client.getPostsPage(
- channelId,
- order.length,
- Constants.POST_CHUNK_SIZE,
- function success(data) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- this.gotMorePosts = true;
- this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE});
-
- if (!data) {
- return;
- }
-
- if (data.order.length === 0) {
- return;
- }
-
- var postList = {};
- postList.posts = $.extend(posts, data.posts);
- postList.order = order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- post_list: postList
- });
-
- Client.getProfiles();
- }.bind(this),
- function fail(err) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- AsyncClient.dispatchError(err, 'getPosts');
- }.bind(this)
- );
- }
- render() {
- var order = [];
- var posts;
- var channel = ChannelStore.get(this.props.channelId);
-
- if (this.state.postList != null) {
- posts = this.state.postList.posts;
- order = this.state.postList.order;
- }
-
- var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>;
- if (channel != null) {
- if (order.length >= this.state.numToDisplay) {
- moreMessages = (
- <a
- ref='loadmore'
- className='more-messages-text theme'
- href='#'
- onClick={this.loadMorePosts}
- >
- {'Load more messages'}
- </a>
- );
- } else {
- moreMessages = this.createChannelIntroMessage(channel);
- }
- }
-
- var postCtls = [];
- if (posts && this.state.isFirstLoadComplete) {
- postCtls = this.createPosts(posts, order);
- } else {
- postCtls.push(
- <LoadingScreen
- position='absolute'
- key='loading'
- />);
- }
-
- var activeClass = '';
- if (!this.props.isActive) {
- activeClass = 'inactive';
- }
-
- return (
- <div
- ref='postlist'
- className={'post-list-holder-by-time ' + activeClass}
- >
- <div className='post-list__table'>
- <div
- ref='postlistcontent'
- className='post-list__content'
- >
- {moreMessages}
- {postCtls}
- </div>
- </div>
- </div>
- );
- }
-}
-
-PostList.defaultProps = {
- isActive: false,
- channelId: null
-};
-PostList.propTypes = {
- isActive: React.PropTypes.bool,
- channelId: React.PropTypes.string
-};
diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx
deleted file mode 100644
index 09cee6218..000000000
--- a/web/react/components/post_list_container.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const PostList = require('./post_list.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-
-export default class PostListContainer extends React.Component {
- constructor() {
- super();
-
- this.onChange = this.onChange.bind(this);
- this.onLeave = this.onLeave.bind(this);
-
- let currentChannelId = ChannelStore.getCurrentId();
- if (currentChannelId) {
- this.state = {currentChannelId: currentChannelId, postLists: [currentChannelId]};
- } else {
- this.state = {currentChannelId: null, postLists: []};
- }
- }
- componentDidMount() {
- ChannelStore.addChangeListener(this.onChange);
- ChannelStore.addLeaveListener(this.onLeave);
- }
- onChange() {
- let channelId = ChannelStore.getCurrentId();
- if (channelId === this.state.currentChannelId) {
- return;
- }
-
- let postLists = this.state.postLists;
- if (postLists.indexOf(channelId) === -1) {
- postLists.push(channelId);
- }
- this.setState({currentChannelId: channelId, postLists: postLists});
- }
- onLeave(id) {
- let postLists = this.state.postLists;
- var index = postLists.indexOf(id);
- if (index !== -1) {
- postLists.splice(index, 1);
- }
- }
- render() {
- let postLists = this.state.postLists;
- let channelId = this.state.currentChannelId;
-
- let postListCtls = [];
- for (let i = 0; i <= this.state.postLists.length - 1; i++) {
- postListCtls.push(
- <PostList
- key={'postlistkey' + i}
- channelId={postLists[i]}
- isActive={postLists[i] === channelId}
- />
- );
- }
-
- return (
- <div>{postListCtls}</div>
- );
- }
-}
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
new file mode 100644
index 000000000..2b81d1d79
--- /dev/null
+++ b/web/react/components/posts_view.jsx
@@ -0,0 +1,303 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
+const Post = require('./post.jsx');
+const Constants = require('../utils/constants.jsx');
+
+export default class PostsView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleScroll = this.handleScroll.bind(this);
+ this.isAtBottom = this.isAtBottom.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.createPosts = this.createPosts.bind(this);
+ this.updateScrolling = this.updateScrolling.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+
+ this.jumpToPostNode = null;
+ this.wasAtBottom = true;
+ this.scrollHeight = 0;
+ }
+ static get SCROLL_TYPE_FREE() {
+ return 1;
+ }
+ static get SCROLL_TYPE_BOTTOM() {
+ return 2;
+ }
+ static get SIDEBAR_OPEN() {
+ return 3;
+ }
+ isAtBottom() {
+ return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
+ }
+ handleScroll() {
+ // HACK FOR RHS -- REMOVE WHEN RHS DIES
+ const childNodes = this.refs.postlistcontent.childNodes;
+ for (let i = 0; i < childNodes.length; i++) {
+ // If the node is 1/3 down the page
+ if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / 3))) {
+ this.jumpToPostNode = childNodes[i];
+ break;
+ }
+ }
+ this.wasAtBottom = this.isAtBottom();
+
+ // --- --------
+
+ this.props.postViewScrolled(this.isAtBottom());
+ this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ }
+ loadMorePostsTop() {
+ this.props.loadMorePostsTopClicked();
+ }
+ createPosts(posts, order) {
+ const postCtls = [];
+ let previousPostDay = new Date(0);
+ const userId = UserStore.getCurrentId();
+
+ let renderedLastViewed = false;
+
+ let numToDisplay = this.props.numPostsToDisplay;
+ if (order.length - 1 < numToDisplay) {
+ numToDisplay = order.length - 1;
+ }
+
+ for (let i = numToDisplay; i >= 0; i--) {
+ const post = posts[order[i]];
+ const parentPost = posts[post.parent_id];
+ const prevPost = posts[order[i + 1]];
+
+ // If the post is a comment whose parent has been deleted, don't add it to the list.
+ if (parentPost && parentPost.state === Constants.POST_DELETED) {
+ continue;
+ }
+
+ let sameUser = false;
+ let sameRoot = false;
+ let hideProfilePic = false;
+
+ if (prevPost) {
+ sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+
+ sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+
+ // hide the profile pic if:
+ // the previous post was made by the same user as the current post,
+ // the previous post is not a comment,
+ // the current post is not a comment,
+ // the current post is not from a webhook
+ // and the previous post is not from a webhook
+ if ((prevPost.user_id === post.user_id) &&
+ !Utils.isComment(prevPost) &&
+ !Utils.isComment(post) &&
+ (!post.props || !post.props.from_webhook) &&
+ (!prevPost.props || !prevPost.props.from_webhook)) {
+ hideProfilePic = true;
+ }
+ }
+
+ // check if it's the last comment in a consecutive string of comments on the same post
+ // it is the last comment if it is last post in the channel or the next post has a different root post
+ var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+
+ var postCtl = (
+ <Post
+ key={post.id + 'postKey'}
+ ref={post.id}
+ sameUser={sameUser}
+ sameRoot={sameRoot}
+ post={post}
+ parentPost={parentPost}
+ posts={posts}
+ hideProfilePic={hideProfilePic}
+ isLastComment={isLastComment}
+ />
+ );
+
+ const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
+ if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
+ postCtls.push(
+ <div
+ key={currentPostDay.toDateString()}
+ className='date-separator'
+ >
+ <hr className='separator__hr' />
+ <div className='separator__text'>{currentPostDay.toDateString()}</div>
+ </div>
+ );
+ }
+
+ if (post.user_id !== userId &&
+ this.props.messageSeparatorTime !== 0 &&
+ post.create_at > this.props.messageSeparatorTime &&
+ !renderedLastViewed) {
+ renderedLastViewed = true;
+
+ // Temporary fix to solve ie11 rendering issue
+ let newSeparatorId = '';
+ if (!Utils.isBrowserIE()) {
+ newSeparatorId = 'new_message_' + post.id;
+ }
+ postCtls.push(
+ <div
+ id={newSeparatorId}
+ key='unviewed'
+ className='new-separator'
+ >
+ <hr
+ className='separator__hr'
+ />
+ <div className='separator__text'>{'New Messages'}</div>
+ </div>
+ );
+ }
+ postCtls.push(postCtl);
+ previousPostDay = currentPostDay;
+ }
+
+ return postCtls;
+ }
+ updateScrolling() {
+ if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ });
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
+ window.requestAnimationFrame(() => {
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ postNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === postNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
+ }
+ });
+ } else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
+ // If we are at the bottom then stay there
+ if (this.wasAtBottom) {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ } else {
+ window.requestAnimationFrame(() => {
+ this.jumpToPostNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
+ }
+ });
+ }
+ } else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ });
+ }
+ }
+ handleResize() {
+ this.updateScrolling();
+ }
+ componentDidMount() {
+ this.updateScrolling();
+ window.addEventListener('resize', this.handleResize);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ componentDidUpdate() {
+ this.updateScrolling();
+ }
+ shouldComponentUpdate(nextProps) {
+ if (this.props.isActive !== nextProps.isActive) {
+ return true;
+ }
+ if (this.props.postList !== nextProps.postList) {
+ return true;
+ }
+ if (this.props.scrollPost !== nextProps.scrollPost) {
+ return true;
+ }
+ if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
+ return true;
+ }
+ if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
+ return true;
+ }
+ if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
+ return true;
+ }
+ if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) {
+ return true;
+ }
+
+ return false;
+ }
+ render() {
+ let posts = [];
+ let order = [];
+ let moreMessages;
+ let postElements;
+ let activeClass = 'inactive';
+ if (this.props.postList != null) {
+ posts = this.props.postList.posts;
+ order = this.props.postList.order;
+
+ // Create intro message or top loadmore link
+ if (order.length >= this.props.numPostsToDisplay) {
+ moreMessages = (
+ <a
+ ref='loadmore'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePostsTop}
+ >
+ {'Load more messages'}
+ </a>
+ );
+ } else {
+ moreMessages = this.props.introText;
+ }
+
+ // Create post elements
+ postElements = this.createPosts(posts, order);
+
+ // Show ourselves if we are marked active
+ if (this.props.isActive) {
+ activeClass = '';
+ }
+ }
+
+ return (
+ <div
+ ref='postlist'
+ className={'post-list-holder-by-time ' + activeClass}
+ onScroll={this.handleScroll}
+ >
+ <div className='post-list__table'>
+ <div
+ ref='postlistcontent'
+ className='post-list__content'
+ >
+ {moreMessages}
+ {postElements}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+PostsView.defaultProps = {
+};
+
+PostsView.propTypes = {
+ isActive: React.PropTypes.bool,
+ postList: React.PropTypes.object,
+ scrollPost: React.PropTypes.string,
+ scrollType: React.PropTypes.number,
+ postViewScrolled: React.PropTypes.func.isRequired,
+ loadMorePostsTopClicked: React.PropTypes.func.isRequired,
+ numPostsToDisplay: React.PropTypes.number,
+ introText: React.PropTypes.element,
+ messageSeparatorTime: React.PropTypes.number
+};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
new file mode 100644
index 000000000..7671ca01d
--- /dev/null
+++ b/web/react/components/posts_view_container.jsx
@@ -0,0 +1,267 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const PostsView = require('./posts_view.jsx');
+const LoadingScreen = require('./loading_screen.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
+const PostStore = require('../stores/post_store.jsx');
+
+const Utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
+
+export default class PostsViewContainer extends React.Component {
+ constructor() {
+ super();
+
+ this.onChannelChange = this.onChannelChange.bind(this);
+ this.onChannelLeave = this.onChannelLeave.bind(this);
+ this.onPostsChange = this.onPostsChange.bind(this);
+ this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.postsLoaded = this.postsLoaded.bind(this);
+ this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
+ this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
+
+ const currentChannelId = ChannelStore.getCurrentId();
+ const state = {
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ scrollPost: null,
+ numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ };
+ if (currentChannelId) {
+ Object.assign(state, {
+ currentChannelIndex: 0,
+ channels: [currentChannelId],
+ postLists: [this.getChannelPosts(currentChannelId)]
+ });
+ } else {
+ Object.assign(state, {
+ currentChannelIndex: null,
+ channels: [],
+ postLists: []
+ });
+ }
+
+ this.state = state;
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.onChannelChange);
+ ChannelStore.addLeaveListener(this.onChannelLeave);
+ PostStore.addChangeListener(this.onPostsChange);
+ PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.onChannelChange);
+ ChannelStore.removeLeaveListener(this.onChannelLeave);
+ PostStore.removeChangeListener(this.onPostsChange);
+ PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ handlePostsViewJumpRequest(type, post) {
+ switch (type) {
+ case Constants.PostsViewJumpTypes.BOTTOM:
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ break;
+ case Constants.PostsViewJumpTypes.POST:
+ this.setState({
+ scrollType: PostsView.SCROLL_TYPE_POST,
+ scrollPost: post
+ });
+ break;
+ case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
+ this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ break;
+ }
+ }
+ onChannelChange() {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const channelId = ChannelStore.getCurrentId();
+
+ // Has the channel really changed?
+ if (channelId === channels[this.state.currentChannelIndex]) {
+ return;
+ }
+
+ PostStore.clearUnseenDeletedPosts(channelId);
+
+ let lastViewed = Number.MAX_VALUE;
+ const member = ChannelStore.getMember(channelId);
+ if (member != null) {
+ lastViewed = member.last_viewed_at;
+ }
+
+ let newIndex = channels.indexOf(channelId);
+ if (newIndex === -1) {
+ newIndex = channels.length;
+ channels.push(channelId);
+ postLists[newIndex] = this.getChannelPosts(channelId);
+ }
+ this.setState({
+ currentChannelIndex: newIndex,
+ currentLastViewed: lastViewed,
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ channels,
+ postLists});
+ }
+ onChannelLeave(id) {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const index = channels.indexOf(id);
+ if (index !== -1) {
+ postLists.splice(index, 1);
+ channels.splice(index, 1);
+ }
+ this.setState({channels, postLists});
+ }
+ onPostsChange() {
+ const channels = this.state.channels;
+ const postLists = Object.assign({}, this.state.postLists);
+ const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
+
+ postLists[this.state.currentChannelIndex] = newPostsView;
+ this.setState({postLists});
+ }
+ getChannelPosts(id) {
+ const postList = PostStore.getPosts(id);
+
+ if (postList != null) {
+ const deletedPosts = PostStore.getUnseenDeletedPosts(id);
+
+ if (deletedPosts && Object.keys(deletedPosts).length > 0) {
+ for (const pid in deletedPosts) {
+ if (deletedPosts.hasOwnProperty(pid)) {
+ postList.posts[pid] = deletedPosts[pid];
+ postList.order.unshift(pid);
+ }
+ }
+
+ postList.order.sort((a, b) => {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ const pendingPostList = PostStore.getPendingPosts(id);
+
+ if (pendingPostList) {
+ postList.order = pendingPostList.order.concat(postList.order);
+ for (const ppid in pendingPostList.posts) {
+ if (pendingPostList.posts.hasOwnProperty(ppid)) {
+ postList.posts[ppid] = pendingPostList.posts[ppid];
+ }
+ }
+ }
+ }
+
+ return postList;
+ }
+ loadMorePostsTop() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const currentPostList = postLists[this.state.currentChannelIndex];
+
+ this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
+
+ Client.getPostsPage(
+ currentChannelId,
+ currentPostList.order.length,
+ Constants.POST_CHUNK_SIZE,
+ this.postsLoaded,
+ this.postsLoadedFailure
+ );
+ }
+ postsLoaded(data) {
+ if (!data) {
+ return;
+ }
+
+ if (data.order.length === 0) {
+ return;
+ }
+
+ const postLists = this.state.postLists;
+ const currentPostList = postLists[this.state.currentChannelIndex];
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+
+ var newPostList = {};
+ newPostList.posts = Object.assign(currentPostList.posts, data.posts);
+ newPostList.order = currentPostList.order.concat(data.order);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: currentChannelId,
+ post_list: newPostList
+ });
+
+ Client.getProfiles();
+ }
+ postsLoadedFailure(err) {
+ AsyncClient.dispatchError(err, 'getPosts');
+ }
+ handlePostsViewScroll(atBottom) {
+ if (atBottom) {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ } else {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
+ }
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ if (Utils.areStatesEqual(this.state, nextState)) {
+ return false;
+ }
+
+ return true;
+ }
+ render() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const channel = ChannelStore.get(currentChannelId);
+
+ const postListCtls = [];
+ for (let i = 0; i < channels.length; i++) {
+ const isActive = (channels[i] === currentChannelId);
+ postListCtls.push(
+ <PostsView
+ key={'postsviewkey' + i}
+ isActive={isActive}
+ postList={postLists[i]}
+ scrollType={this.state.scrollType}
+ scrollPost={this.state.scrollPost}
+ postViewScrolled={this.handlePostsViewScroll}
+ loadMorePostsTopClicked={this.loadMorePostsTop}
+ numPostsToDisplay={this.state.numPostsToDisplay}
+ introText={channel ? createChannelIntroMessage(channel) : null}
+ messageSeparatorTime={this.state.currentLastViewed}
+ />
+ );
+ if ((!postLists[i] || !channel) && isActive) {
+ postListCtls.push(
+ <LoadingScreen
+ position='absolute'
+ key='loading'
+ />
+ );
+ }
+ }
+
+ return (
+ <div>{postListCtls}</div>
+ );
+ }
+}
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 3d4d9bf45..c40409dcc 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component {
var body = '';
if (this.state.clientId === '') {
body = (
- <div className='form-group user-settings'>
- <h3>{'Register a New Application'}</h3>
- <br/>
- <label className='col-sm-4 control-label'>{'Application Name'}</label>
- <div className='col-sm-7'>
- <input
- ref='name'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {nameError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
- <div className='col-sm-7'>
- <input
- ref='homepage'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {homepageError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Description'}</label>
- <div className='col-sm-7'>
- <input
- ref='desc'
- className='form-control'
- type='text'
- placeholder='Optional'
- />
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Callback URL'}</label>
- <div className='col-sm-7'>
- <textarea
- ref='callback'
- className='form-control'
- type='text'
- placeholder='Required'
- rows='5'
- />
- {callbackError}
+ <div className='settings-modal'>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Register a New Application'}</h4>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Application Name'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='name'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {nameError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='homepage'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {homepageError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Description'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='desc'
+ className='form-control'
+ type='text'
+ placeholder='Optional'
+ />
+ </div>
+ </div>
+ <div className='row padding-top padding-bottom x2'>
+ <label className='col-sm-4 control-label'>{'Callback URL'}</label>
+ <div className='col-sm-7'>
+ <textarea
+ ref='callback'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ rows='5'
+ />
+ {callbackError}
+ </div>
+ </div>
+ {serverError}
+ <hr />
+ <a
+ className='btn btn-sm theme pull-right'
+ href='#'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ {'Cancel'}
+ </a>
+ <a
+ className='btn btn-sm btn-primary pull-right'
+ onClick={this.register}
+ >
+ {'Register'}
+ </a>
</div>
- <br/>
- <br/>
- <br/>
- <br/>
- <br/>
- {serverError}
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
);
} else {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index cfff04fa2..8c6324c72 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -93,6 +93,7 @@ export default class RhsComment extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title='Comment'
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index deef389e2..21e52b438 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -79,6 +79,7 @@ export default class RhsRootPost extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index bcdec2870..fe57bed28 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -34,12 +34,12 @@ export default class RhsThread extends React.Component {
}
var channelId = postList.posts[postList.order[0]].channel_id;
- var pendingPostList = PostStore.getPendingPosts(channelId);
+ var pendingPostsList = PostStore.getPendingPosts(channelId);
- if (pendingPostList) {
- for (var pid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(pid)) {
- postList.posts[pid] = pendingPostList.posts[pid];
+ if (pendingPostsList) {
+ for (var pid in pendingPostsList.posts) {
+ if (pendingPostsList.posts.hasOwnProperty(pid)) {
+ postList.posts[pid] = pendingPostsList.posts[pid];
}
}
}
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03c7b894c..03e14ec49 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -10,6 +10,7 @@ const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
+const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ }
+
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
@@ -136,7 +142,10 @@ export default class SearchAutocomplete extends React.Component {
let channels = ChannelStore.getAll();
if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter));
+ channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
+ } else {
+ // don't show direct channels
+ channels = channels.filter((channel) => channel.type !== 'D');
}
channels.sort((a, b) => a.name.localeCompare(b.name));
@@ -193,7 +202,7 @@ export default class SearchAutocomplete extends React.Component {
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__channel';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -211,7 +220,7 @@ export default class SearchAutocomplete extends React.Component {
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__user';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -224,7 +233,7 @@ export default class SearchAutocomplete extends React.Component {
className={className}
>
<img
- className='profile-img'
+ className='profile-img rounded'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
@@ -234,12 +243,15 @@ export default class SearchAutocomplete extends React.Component {
}
return (
- <div
- ref='container'
- className='search-autocomplete'
+ <Popover
+ ref='searchPopover'
+ onShow={this.componentDidMount}
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
>
{suggestions}
- </div>
+ </Popover>
);
}
}
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index e1d36ad7d..90865475b 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -3,7 +3,7 @@
var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
this.state = state;
}
getSearchTermStateFromStores() {
- var term = PostStore.getSearchTerm() || '';
+ var term = SearchStore.getSearchTerm() || '';
return {
searchTerm: term
};
}
componentDidMount() {
- PostStore.addSearchTermChangeListener(this.onListenerChange);
+ SearchStore.addSearchTermChangeListener(this.onListenerChange);
this.mounted = true;
}
componentWillUnmount() {
- PostStore.removeSearchTermChangeListener(this.onListenerChange);
+ SearchStore.removeSearchTermChangeListener(this.onListenerChange);
this.mounted = false;
}
onListenerChange(doSearch, isMentionSearch) {
@@ -84,20 +84,16 @@ export default class SearchBar extends React.Component {
}
handleUserInput(e) {
var term = e.target.value;
- PostStore.storeSearchTerm(term);
- PostStore.emitSearchTermChange(false);
+ SearchStore.storeSearchTerm(term);
+ SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
}
- handleMouseInput(e) {
- e.preventDefault();
- }
handleUserBlur() {
this.setState({focused: false});
}
- handleUserFocus(e) {
- e.target.select();
+ handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
@@ -105,6 +101,7 @@ export default class SearchBar extends React.Component {
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
+
client.search(
terms,
(data) => {
@@ -143,8 +140,8 @@ export default class SearchBar extends React.Component {
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
- PostStore.storeSearchTerm(text);
- PostStore.emitSearchTermChange(false);
+ SearchStore.storeSearchTerm(text);
+ SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}
@@ -178,6 +175,7 @@ export default class SearchBar extends React.Component {
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{overflow: 'visible'}}
+ autoComplete='off'
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -190,7 +188,6 @@ export default class SearchBar extends React.Component {
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
- onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index 30e15d0ad..b56a7b006 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SearchBox = require('./search_bar.jsx');
var Utils = require('../utils/utils.jsx');
@@ -9,7 +9,7 @@ var SearchResultsHeader = require('./search_results_header.jsx');
var SearchResultsItem = require('./search_results_item.jsx');
function getStateFromStores() {
- return {results: PostStore.getSearchResults()};
+ return {results: SearchStore.getSearchResults()};
}
export default class SearchResults extends React.Component {
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
componentDidMount() {
this.mounted = true;
- PostStore.addSearchChangeListener(this.onChange);
+ SearchStore.addSearchChangeListener(this.onChange);
this.resize();
window.addEventListener('resize', this.handleResize);
}
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
}
componentWillUnmount() {
- PostStore.removeSearchChangeListener(this.onChange);
+ SearchStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
window.removeEventListener('resize', this.handleResize);
}
@@ -78,12 +78,21 @@ export default class SearchResults extends React.Component {
searchForm = <SearchBox />;
}
var noResults = (!results || !results.order || !results.order.length);
- var searchTerm = PostStore.getSearchTerm();
+ var searchTerm = SearchStore.getSearchTerm();
var ctls = null;
if (noResults) {
- ctls = <div className='sidebar--right__subheader'>No results</div>;
+ ctls =
+ (
+ <div className='sidebar--right__subheader'>
+ <h4>{'NO RESULTS'}</h4>
+ <ul>
+ <li>If you're searching a partial phrase (ex. searching "rea", looking for "reach" or "reaction"), append a * to your search term</li>
+ <li>Due to the volume of results, two letter searches and common words like "this", "a" and "is" won't appear in search results</li>
+ </ul>
+ </div>
+ );
} else {
ctls = results.order.map(function mymap(id) {
var post = results.posts[id];
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index d212e47a3..a8bd4db2c 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var UserProfile = require('./user_profile.jsx');
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST_SELECTED,
post_list: data,
- from_search: PostStore.getSearchTerm()
+ from_search: SearchStore.getSearchTerm()
});
AppDispatcher.handleServerAction({
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 774f98a43..d6c4b0d4b 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component {
var widthClass;
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
- } else {
+ } else if (this.props.width === 'medium') {
widthClass = 'col-sm-10 col-sm-offset-2';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
}
return (
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index b6bcb13a6..e69412cca 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src=''
/>
);
@@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src={this.props.src}
/>
);
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index 4af46c35a..68d9cea48 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -1,10 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+var utils = require('../utils/utils.jsx');
export default class SettingsSidebar extends React.Component {
componentDidUpdate() {
$('.settings-modal').find('.modal-body').scrollTop(0);
$('.settings-modal').find('.modal-body').perfectScrollbar('update');
+ if (utils.isSafari()) {
+ $('.settings-modal .settings-links .nav').addClass('absolute');
+ }
}
constructor(props) {
super(props);
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index ed2c84057..c47919885 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
const NewChannelFlow = require('./new_channel_flow.jsx');
const MoreDirectChannels = require('./more_direct_channels.jsx');
const SearchBox = require('./search_bar.jsx');
const SidebarHeader = require('./sidebar_header.jsx');
-const TeamStore = require('../stores/team_store.jsx');
const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
+
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -40,6 +47,9 @@ export default class Sidebar extends React.Component {
this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
+ this.updateTitle = this.updateTitle.bind(this);
+ this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
+ this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -48,8 +58,45 @@ export default class Sidebar extends React.Component {
state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
-
this.state = state;
+
+ this.unreadCountPerChannel = {};
+ this.setUnreadCountPerChannel();
+ }
+ setUnreadCountPerChannel() {
+ const channels = ChannelStore.getAll();
+ const members = ChannelStore.getAllMembers();
+ const channelUnreadCounts = {};
+
+ channels.forEach((ch) => {
+ const chMember = members[ch.id];
+ let chMentionCount = chMember.mention_count;
+ let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
+
+ if (ch.type === 'D') {
+ chMentionCount = chUnreadCount;
+ chUnreadCount = 0;
+ }
+
+ channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
+ });
+
+ this.unreadCountPerChannel = channelUnreadCounts;
+ }
+ getUnreadCount(channelId) {
+ let mentions = 0;
+ let msgs = 0;
+
+ if (channelId) {
+ return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
+ }
+
+ Object.keys(this.unreadCountPerChannel).forEach((chId) => {
+ msgs += this.unreadCountPerChannel[chId].msgs;
+ mentions += this.unreadCountPerChannel[chId].mentions;
+ });
+
+ return {msgs, mentions};
}
getStateFromStores() {
const members = ChannelStore.getAllMembers();
@@ -96,7 +143,7 @@ export default class Sidebar extends React.Component {
channel.type = 'D';
}
- channel.display_name = teammate.username;
+ channel.display_name = Utils.displayUsername(teammate.id);
channel.teammate_id = teammate.id;
channel.status = UserStore.getStatus(teammate.id);
@@ -115,12 +162,15 @@ export default class Sidebar extends React.Component {
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
return {
activeId: currentId,
channels: ChannelStore.getAll(),
members,
visibleDirectChannels,
- hiddenDirectChannelCount
+ hiddenDirectChannelCount,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
};
}
@@ -138,10 +188,6 @@ export default class Sidebar extends React.Component {
window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areStatesEqual(nextProps, this.props)) {
- return true;
- }
-
if (!Utils.areStatesEqual(nextState, this.state)) {
return true;
}
@@ -192,7 +238,10 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
- document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
+ const unread = this.getUnreadCount();
+ const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
+ const unreadTitle = unread.msgs > 0 ? '* ' : '';
+ document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
}
}
onScroll() {
@@ -269,10 +318,56 @@ export default class Sidebar extends React.Component {
this.setState({showDirectChannelsModal: false});
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Channels'}</h4>
+ <p><strong>{'Channels'}</strong>{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}<strong>{'Direct Messages'}</strong>{' for a single person or '}<strong>{'Private Groups'}</strong>{' for multiple people.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'"Town Square" and "Off-Topic" channels'}</h4>
+ <p>{'Here are two public channels to start:'}</p>
+ <p>
+ <strong>{'Town Square'}</strong>{' is a place for team-wide communication. Everyone in your team is a member of this channel.'}
+ </p>
+ <p>
+ <strong>{'Off-Topic'}</strong>{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'Creating and Joining Channels'}</h4>
+ <p>
+ {'Click '}<strong>{'"More..."'}</strong>{' to create a new channel or join an existing one.'}
+ </p>
+ <p>
+ {'You can also create a new channel or private group by clicking the '}<strong>{'"+" symbol'}</strong>{' next to the channel or private group header.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--sidebar'
+ />
+ );
+ }
+
createChannelElement(channel, index, arr, handleClose) {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
+ var unreadCount = this.getUnreadCount(channel.id);
var msgCount;
var linkClass = '';
@@ -284,7 +379,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
- msgCount = channel.total_msg_count - channelMember.msg_count;
+ msgCount = unreadCount.msgs + unreadCount.mentions;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
@@ -301,16 +396,8 @@ export default class Sidebar extends React.Component {
var badge = null;
if (channelMember) {
- if (channel.type === 'D') {
- // direct message channels show badges for any number of unread posts
- msgCount = channel.total_msg_count - channelMember.msg_count;
- if (msgCount > 0) {
- badge = <span className='badge pull-right small'>{msgCount}</span>;
- this.badgesActive = true;
- }
- } else if (channelMember.mention_count > 0) {
- // public and private channels only show badges for mentions
- badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
+ if (unreadCount.mentions) {
+ badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
@@ -412,6 +499,11 @@ export default class Sidebar extends React.Component {
rowClass += ' has-close';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<li
key={channel.name}
@@ -428,12 +520,15 @@ export default class Sidebar extends React.Component {
{badge}
{closeButton}
</a>
+ {tutorialTip}
</li>
);
}
render() {
this.badgesActive = false;
+ this.setUnreadCountPerChannel();
+
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
@@ -505,9 +600,9 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
- teamDisplayName={this.props.teamDisplayName}
- teamName={this.props.teamName}
- teamType={this.props.teamType}
+ teamDisplayName={TeamStore.getCurrent().display_name}
+ teamName={TeamStore.getCurrent().name}
+ teamType={TeamStore.getCurrent().type}
/>
<SearchBox />
@@ -593,11 +688,6 @@ export default class Sidebar extends React.Component {
}
Sidebar.defaultProps = {
- teamType: '',
- teamDisplayName: ''
};
Sidebar.propTypes = {
- teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index de28a8374..3f777d93c 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -1,17 +1,42 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var NavbarDropdown = require('./navbar_dropdown.jsx');
-var UserStore = require('../stores/user_store.jsx');
+const NavbarDropdown = require('./navbar_dropdown.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const UserStore = require('../stores/user_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
+
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+
+ this.state = this.getStateFromStores();
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ getStateFromStores() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
- this.state = {};
+ return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER};
+ }
+ onPreferenceChange() {
+ this.setState(this.getStateFromStores());
}
toggleDropdown(e) {
e.preventDefault();
@@ -21,6 +46,38 @@ export default class SidebarHeader extends React.Component {
}
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Main Menu'}</h4>
+ <p>
+ {'The '}<strong>{'Main Menu'}</strong>{' is where you can '}
+ <strong>{'Invite New Members'}</strong>
+ {', access your '}
+ <strong>{'Account Settings'}</strong>
+ {' and set your '}<strong>{'Theme Color'}</strong>{'.'}
+ </p>
+ <p>
+ {'Team administrators can also access their '}<strong>{'Team Settings'}</strong>{' from this menu.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <div
+ onClick={this.toggleDropdown}
+ >
+ <TutorialTip
+ ref='tip'
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--header'
+ />
+ </div>
+ );
+ }
render() {
var me = UserStore.getCurrentUser();
var profilePicture = null;
@@ -38,8 +95,14 @@ export default class SidebarHeader extends React.Component {
);
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<div className='team__header theme'>
+ {tutorialTip}
<a
href='#'
onClick={this.toggleDropdown}
@@ -47,7 +110,15 @@ export default class SidebarHeader extends React.Component {
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ delayShow={1000}
+ placement='bottom'
+ overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
+ ref='descriptionOverlay'
+ >
<div className='team__name'>{this.props.teamDisplayName}</div>
+ </OverlayTrigger>
</div>
</a>
<NavbarDropdown
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 4e6985a86..e2ef60959 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -3,11 +3,12 @@
var SearchResults = require('./search_results.jsx');
var RhsThread = require('./rhs_thread.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var PostStore = require('../stores/post_store.jsx');
var Utils = require('../utils/utils.jsx');
function getStateFromStores() {
- return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()};
+ return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
}
export default class SidebarRight extends React.Component {
@@ -19,23 +20,48 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.doStrangeThings = this.doStrangeThings.bind(this);
+
this.state = getStateFromStores();
}
componentDidMount() {
- PostStore.addSearchChangeListener(this.onSearchChange);
+ SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
+ this.doStrangeThings();
}
componentWillUnmount() {
- PostStore.removeSearchChangeListener(this.onSearchChange);
+ SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
- componentDidUpdate() {
- if (this.plScrolledToBottom) {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- postHolder.scrollTop(postHolder[0].scrollHeight);
- } else {
- $('.top-visible-post')[0].scrollIntoView();
+ componentWillUpdate() {
+ PostStore.jumpPostsViewSidebarOpen();
+ }
+ doStrangeThings() {
+ // We should have a better way to do this stuff
+ // Hence the function name.
+ $('.inner__wrap').removeClass('.move--right');
+ $('.inner__wrap').addClass('move--left');
+ $('.sidebar--left').removeClass('move--right');
+ $('.sidebar--right').addClass('move--left');
+
+ //$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
+
+ if (!(this.state.search_visible || this.state.post_right_visible)) {
+ $('.inner__wrap').removeClass('move--left').removeClass('move--right');
+ $('.sidebar--right').removeClass('move--left');
+ return (
+ <div></div>
+ );
}
+
+ /*setTimeout(() => {
+ $('.sidebar__overlay').fadeOut('200', () => {
+ $('.sidebar__overlay').remove();
+ });
+ }, 500);*/
+ }
+ componentDidUpdate() {
+ this.doStrangeThings();
}
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
@@ -51,30 +77,6 @@ export default class SidebarRight extends React.Component {
}
}
render() {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
- this.plScrolledToBottom = position >= bottom;
-
- if (!(this.state.search_visible || this.state.post_right_visible)) {
- $('.inner__wrap').removeClass('move--left').removeClass('move--right');
- $('.sidebar--right').removeClass('move--left');
- return (
- <div></div>
- );
- }
-
- $('.inner__wrap').removeClass('.move--right').addClass('move--left');
- $('.sidebar--left').removeClass('move--right');
- $('.sidebar--right').addClass('move--left');
- $('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
-
- setTimeout(() => {
- $('.sidebar__overlay').fadeOut('200', function fadeOverlay() {
- $(this).remove();
- });
- }, 500);
-
var content = '';
if (this.state.search_visible) {
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index fddc98c9d..9350bbd42 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx');
@@ -51,7 +52,7 @@ export default class SidebarRightMenu extends React.Component {
data-toggle='modal'
data-target='#get_link'
data-title='Team Invite'
- data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
+ data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a>
</li>
);
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 1858703ef..37760a2a2 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -36,18 +36,88 @@ export default class TeamSignUp extends React.Component {
}
render() {
+ var teamListing = null;
+
+ if (global.window.mm_config.EnableTeamListing === 'true') {
+ if (this.props.teams.length === 0) {
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ teamListing = (<div>{'There are no teams include in the Team Directory and team creation has been disabled.'}</div>);
+ }
+ } else {
+ teamListing = (
+ <div>
+ <h3>{'Choose a Team'}</h3>
+ <div className='signup-team-all'>
+ {
+ this.props.teams.map((team) => {
+ return (
+ <div
+ key={'team_' + team.name}
+ className='signup-team-dir'
+ >
+ <a
+ href={'/' + team.name}
+ >
+ <div className='signup-team-dir__group'>
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
+ </div>
+ </a>
+ </div>
+ );
+ })
+ }
+ </div>
+ </div>
+ );
+ }
+ }
+
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ if (teamListing == null) {
+ return (<div>{'Team creation has been disabled. Please contact an administrator for access.'}</div>);
+ }
+
+ return (
+ <div>
+ {teamListing}
+ </div>
+ );
+ }
+
if (this.state.page === 'choose') {
return (
- <ChoosePage
- updatePage={this.updatePage}
- />
+ <div>
+ {teamListing}
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ </div>
);
}
if (this.state.page === 'email') {
- return <EmailSignUpPage />;
+ return (
+ <div>
+ {teamListing}
+ <EmailSignUpPage />
+ </div>
+ );
} else if (this.state.page === 'gitlab') {
- return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
+ return (
+ <div>
+ {teamListing}
+ <SSOSignupPage service={Constants.GITLAB_SERVICE} />
+ </div>
+ );
}
}
}
+
+TeamSignUp.propTypes = {
+ teams: React.PropTypes.array
+};
+
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 923180e27..587ef5ec2 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -6,29 +6,111 @@ const SettingItemMax = require('./setting_item_max.jsx');
const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
+const TeamStore = require('../stores/team_store.jsx');
export default class GeneralTab extends React.Component {
constructor(props) {
super(props);
this.handleNameSubmit = this.handleNameSubmit.bind(this);
+ this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this);
+ this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this);
+ this.handleTeamListingSubmit = this.handleTeamListingSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
- this.onUpdateSection = this.onUpdateSection.bind(this);
+ this.onUpdateNameSection = this.onUpdateNameSection.bind(this);
this.updateName = this.updateName.bind(this);
+ this.onUpdateInviteIdSection = this.onUpdateInviteIdSection.bind(this);
+ this.updateInviteId = this.updateInviteId.bind(this);
+ this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this);
+ this.handleOpenInviteRadio = this.handleOpenInviteRadio.bind(this);
+ this.onUpdateTeamListingSection = this.onUpdateTeamListingSection.bind(this);
+ this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this);
+ this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this);
- this.state = {name: this.props.teamDisplayName, serverError: '', clientError: ''};
+ this.state = {
+ name: props.team.display_name,
+ invite_id: props.team.invite_id,
+ allow_open_invite: props.team.allow_open_invite,
+ allow_team_listing: props.team.allow_team_listing,
+ serverError: '',
+ clientError: ''
+ };
}
+
+ handleGenerateInviteId(e) {
+ e.preventDefault();
+
+ var newId = '';
+ for (var i = 0; i < 32; i++) {
+ newId += Math.floor(Math.random() * 16).toString(16);
+ }
+
+ this.setState({invite_id: newId});
+ }
+
+ handleOpenInviteRadio(openInvite) {
+ this.setState({allow_open_invite: openInvite});
+ }
+
+ handleTeamListingRadio(listing) {
+ if (global.window.mm_config.EnableTeamListing !== 'true' && listing) {
+ this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'});
+ } else {
+ this.setState({allow_team_listing: listing});
+ }
+ }
+
+ handleOpenInviteSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+
+ var data = this.props.team;
+ data.allow_open_invite = this.state.allow_open_invite;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
+ handleTeamListingSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+
+ var data = this.props.team;
+ data.allow_team_listing = this.state.allow_team_listing;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
handleNameSubmit(e) {
e.preventDefault();
- let state = {serverError: '', clientError: ''};
+ var state = {serverError: '', clientError: ''};
let valid = true;
const name = this.state.name.trim();
if (!name) {
state.clientError = 'This field is required';
valid = false;
- } else if (name === this.props.teamDisplayName) {
+ } else if (name === this.props.team.display_name) {
state.clientError = 'Please choose a new name for your team';
valid = false;
} else {
@@ -41,37 +123,76 @@ export default class GeneralTab extends React.Component {
return;
}
- let data = {};
- data.new_name = name;
+ var data = this.props.team;
+ data.display_name = this.state.name;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
+ handleInviteIdSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+ let valid = true;
+
+ const inviteId = this.state.invite_id.trim();
+ if (inviteId) {
+ state.clientError = '';
+ } else {
+ state.clientError = 'This field is required';
+ valid = false;
+ }
+
+ this.setState(state);
- Client.updateTeamDisplayName(data,
- function nameChangeSuccess() {
+ if (!valid) {
+ return;
+ }
+
+ var data = this.props.team;
+ data.invite_id = this.state.invite_id;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
this.props.updateSection('');
- $('#team_settings').modal('hide');
- window.location.reload();
- }.bind(this),
- function nameChangeFail(err) {
+ },
+ (err) => {
state.serverError = err.message;
this.setState(state);
- }.bind(this)
+ }
);
}
+
componentWillReceiveProps(newProps) {
if (newProps.team && newProps.teamDisplayName) {
this.setState({name: newProps.teamDisplayName});
}
}
+
handleClose() {
this.setState({clientError: '', serverError: ''});
this.props.updateSection('');
}
+
componentDidMount() {
$('#team_settings').on('hidden.bs.modal', this.handleClose);
}
+
componentWillUnmount() {
$('#team_settings').off('hidden.bs.modal', this.handleClose);
}
- onUpdateSection(e) {
+
+ onUpdateNameSection(e) {
e.preventDefault();
if (this.props.activeSection === 'name') {
this.props.updateSection('');
@@ -79,10 +200,44 @@ export default class GeneralTab extends React.Component {
this.props.updateSection('name');
}
}
+
+ onUpdateInviteIdSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'invite_id') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('invite_id');
+ }
+ }
+
+ onUpdateOpenInviteSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'open_invite') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('open_invite');
+ }
+ }
+
+ onUpdateTeamListingSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'team_listing') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('team_listing');
+ }
+ }
+
updateName(e) {
e.preventDefault();
this.setState({name: e.target.value});
}
+
+ updateInviteId(e) {
+ e.preventDefault();
+ this.setState({invite_id: e.target.value});
+ }
+
render() {
let clientError = null;
let serverError = null;
@@ -93,10 +248,179 @@ export default class GeneralTab extends React.Component {
serverError = this.state.serverError;
}
+ let teamListingSection;
+ if (this.props.activeSection === 'team_listing') {
+ const inputs = [
+ <div key='userTeamListingOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, true)}
+ />
+ {'Yes'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ ref='teamListingRadioNo'
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={!this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, false)}
+ />
+ {'No'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div>
+ </div>
+ ];
+
+ teamListingSection = (
+ <SettingItemMax
+ title='Include this team in the Team Directory'
+ inputs={inputs}
+ submit={this.handleTeamListingSubmit}
+ server_error={serverError}
+ client_error={clientError}
+ updateSection={this.onUpdateTeamListingSection}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.allow_team_listing === true) {
+ describe = 'Yes';
+ } else {
+ describe = 'No';
+ }
+
+ teamListingSection = (
+ <SettingItemMin
+ title='Include this team in the Team Directory'
+ describe={describe}
+ updateSection={this.onUpdateTeamListingSection}
+ />
+ );
+ }
+
+ let openInviteSection;
+ if (this.props.activeSection === 'open_invite') {
+ const inputs = [
+ <div key='userOpenInviteOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ name='userOpenInviteOptions'
+ type='radio'
+ defaultChecked={this.state.allow_open_invite}
+ onChange={this.handleOpenInviteRadio.bind(this, true)}
+ />
+ {'Yes'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ name='userOpenInviteOptions'
+ type='radio'
+ defaultChecked={!this.state.allow_open_invite}
+ onChange={this.handleOpenInviteRadio.bind(this, false)}
+ />
+ {'No'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'When allowed, a link to account creation will be included on the sign-in page of this team and allow any visitor to sign-up.'}</div>
+ </div>
+ ];
+
+ openInviteSection = (
+ <SettingItemMax
+ title='Allow anyone to sign-up from login page'
+ inputs={inputs}
+ submit={this.handleOpenInviteSubmit}
+ server_error={serverError}
+ updateSection={this.onUpdateOpenInviteSection}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.allow_open_invite === true) {
+ describe = 'Yes';
+ } else {
+ describe = 'No';
+ }
+
+ openInviteSection = (
+ <SettingItemMin
+ title='Allow anyone to sign-up from login page'
+ describe={describe}
+ updateSection={this.onUpdateOpenInviteSection}
+ />
+ );
+ }
+
+ let inviteSection;
+
+ if (this.props.activeSection === 'invite_id') {
+ const inputs = [];
+
+ inputs.push(
+ <div key='teamInviteSetting'>
+ <div className='row'>
+ <label className='col-sm-5 control-label'>{'Invite Code'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateInviteId}
+ value={this.state.invite_id}
+ maxLength='32'
+ />
+ <div className='padding-top x2'>
+ <a
+ href='#'
+ onClick={this.handleGenerateInviteId}
+ >
+ {'Re-Generate'}
+ </a>
+ </div>
+ </div>
+ </div>
+ <div className='setting-list__hint'>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div>
+ </div>
+ );
+
+ inviteSection = (
+ <SettingItemMax
+ title={`Invite Code`}
+ inputs={inputs}
+ submit={this.handleInviteIdSubmit}
+ server_error={serverError}
+ client_error={clientError}
+ updateSection={this.onUpdateInviteIdSection}
+ />
+ );
+ } else {
+ inviteSection = (
+ <SettingItemMin
+ title={`Invite Code`}
+ describe={`Click 'Edit' to regenerate Invite Code.`}
+ updateSection={this.onUpdateInviteIdSection}
+ />
+ );
+ }
+
let nameSection;
if (this.props.activeSection === 'name') {
- let inputs = [];
+ const inputs = [];
let teamNameLabel = 'Team Name';
if (Utils.isMobile()) {
@@ -127,17 +451,17 @@ export default class GeneralTab extends React.Component {
submit={this.handleNameSubmit}
server_error={serverError}
client_error={clientError}
- updateSection={this.onUpdateSection}
+ updateSection={this.onUpdateNameSection}
/>
);
} else {
- let describe = this.state.name;
+ var describe = this.state.name;
nameSection = (
<SettingItemMin
title={`Team Name`}
describe={describe}
- updateSection={this.onUpdateSection}
+ updateSection={this.onUpdateNameSection}
/>
);
}
@@ -158,16 +482,22 @@ export default class GeneralTab extends React.Component {
ref='title'
>
<i className='modal-back'></i>
- General Settings
+ {'General Settings'}
</h4>
</div>
<div
ref='wrapper'
className='user-settings'
>
- <h3 className='tab-header'>General Settings</h3>
+ <h3 className='tab-header'>{'General Settings'}</h3>
<div className='divider-dark first'/>
{nameSection}
+ <div className='divider-light'/>
+ {openInviteSection}
+ <div className='divider-light'/>
+ {teamListingSection}
+ <div className='divider-light'/>
+ {inviteSection}
<div className='divider-dark'/>
</div>
</div>
@@ -178,6 +508,5 @@ export default class GeneralTab extends React.Component {
GeneralTab.propTypes = {
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
- activeSection: React.PropTypes.string.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ activeSection: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index e14da4f04..09674f1ef 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -37,7 +37,6 @@ export default class TeamSettings extends React.Component {
team={this.state.team}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
- teamDisplayName={this.props.teamDisplayName}
/>
</div>
);
@@ -72,12 +71,11 @@ export default class TeamSettings extends React.Component {
TeamSettings.defaultProps = {
activeTab: '',
- activeSection: '',
- teamDisplayName: ''
+ activeSection: ''
};
+
TeamSettings.propTypes = {
activeTab: React.PropTypes.string.isRequired,
activeSection: React.PropTypes.string.isRequired,
- updateSection: React.PropTypes.func.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ updateSection: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 5c5995020..17fe31c65 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -82,7 +82,6 @@ export default class TeamSettingsModal extends React.Component {
activeTab={this.state.activeTab}
activeSection={this.state.activeSection}
updateSection={this.updateSection}
- teamDisplayName={this.props.teamDisplayName}
/>
</div>
</div>
@@ -95,5 +94,4 @@ export default class TeamSettingsModal extends React.Component {
}
TeamSettingsModal.propTypes = {
- teamDisplayName: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 4d08274e4..2005ecc31 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -25,6 +25,9 @@ export default class TeamSignupDisplayNamePage extends React.Component {
if (!displayName) {
this.setState({nameError: 'This field is required'});
return;
+ } else if (displayName.length < 4 || displayName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
+ return;
}
this.props.state.wizard = 'team_url';
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 02d5cab8e..8972fda1a 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -35,8 +35,8 @@ export default class TeamSignupUrlPage extends React.Component {
if (cleanedName !== name || !urlRegex.test(name)) {
this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."});
return;
- } else if (cleanedName.length <= 2 || cleanedName.length > 15) {
- this.setState({nameError: 'Name must be 3 or more characters up to a maximum of 15'});
+ } else if (cleanedName.length < 4 || cleanedName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
return;
}
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index ff4ccd4d8..021713f04 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component {
className='btn btn-md btn-primary'
type='submit'
>
- {'Sign up'}
+ {'Create Team'}
</button>
{serverError}
</div>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 86bb42f62..707033d8f 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const PostStore = require('../stores/post_store.jsx');
+const SearchStore = require('../stores/search_store.jsx');
const CommandList = require('./command_list.jsx');
const ErrorStore = require('../stores/error_store.jsx');
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
- PostStore.addAddMentionListener(this.onListenerChange);
+ SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
}
componentWillUnmount() {
- PostStore.removeAddMentionListener(this.onListenerChange);
+ SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
new file mode 100644
index 000000000..c37739b9c
--- /dev/null
+++ b/web/react/components/time_since.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../utils/utils.jsx');
+
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class TimeSince extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ componentDidMount() {
+ this.intervalId = setInterval(() => {
+ this.forceUpdate();
+ }, 30000);
+ }
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+ }
+ render() {
+ const displayDate = Utils.displayDate(this.props.eventTime);
+ const displayTime = Utils.displayTime(this.props.eventTime);
+
+ const tooltip = (
+ <Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
+ {displayDate + ' at ' + displayTime}
+ </Tooltip>
+ );
+
+ return (
+ <OverlayTrigger
+ delayShow={400}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time className='post-profile-time'>
+ {Utils.displayDateTime(this.props.eventTime)}
+ </time>
+ </OverlayTrigger>
+ );
+ }
+}
+TimeSince.defaultProps = {
+ eventTime: 0
+};
+
+TimeSince.propTypes = {
+ eventTime: React.PropTypes.number.isRequired
+};
diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx
new file mode 100644
index 000000000..8fc38b1e3
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_intro_screens.jsx
@@ -0,0 +1,161 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+const TeamStore = require('../../stores/team_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const Utils = require('../../utils/utils.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+export default class TutorialIntroScreens extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.createScreen = this.createScreen.bind(this);
+
+ this.state = {currentScreen: 0};
+ }
+ handleNext() {
+ if (this.state.currentScreen < 2) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ componentDidMount() {
+ $('.tutorials__scroll').perfectScrollbar();
+ }
+ createScreen() {
+ switch (this.state.currentScreen) {
+ case 0:
+ return this.createScreenOne();
+ case 1:
+ return this.createScreenTwo();
+ case 2:
+ return this.createScreenThree();
+ }
+ }
+ createScreenOne() {
+ return (
+ <div>
+ <h3>{'Welcome to:'}</h3>
+ <h1>{'Mattermost'}</h1>
+ <p>{'Your team communications all in one place, instantly searchable and available anywhere.'}</p>
+ <p>{'Keep your team connected to help them achieve what matters most.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle active'/>
+ <div className='circle'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenTwo() {
+ return (
+ <div>
+ <h3>{'How Mattermost works:'}</h3>
+ <p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p>
+ <p>{'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle active'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenThree() {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ }
+
+ return (
+ <div>
+ <h3>{'You’re all set'}</h3>
+ <p>
+ {inviteModalLink}
+ {' when you’re ready.'}
+ </p>
+ <p>
+ {'Need anything, just email us at '}
+ <a
+ href='mailto:feedback@mattermost.com'
+ target='_blank'
+ >
+ {'feedback@mattermost.com'}
+ </a>
+ {'.'}
+ </p>
+ {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle'/>
+ <div className='circle active'/>
+ </div>
+ </div>
+ );
+ }
+ render() {
+ const height = Utils.windowHeight() - 100;
+ const screen = this.createScreen();
+
+ return (
+ <div
+ className='tutorials__scroll'
+ style={{height}}
+ >
+ <div className='tutorial-steps__container'>
+ <div className='tutorial__content'>
+ <div className='tutorial__steps'>
+ {screen}
+ <button
+ className='btn btn-primary'
+ tabIndex='1'
+ onClick={this.handleNext}
+ >
+ {'Next'}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx
new file mode 100644
index 000000000..c85acb346
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_tip.jsx
@@ -0,0 +1,131 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+const Overlay = ReactBootstrap.Overlay;
+
+export default class TutorialTip extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.toggle = this.toggle.bind(this);
+
+ this.state = {currentScreen: 0, show: false};
+ }
+ toggle() {
+ const show = !this.state.show;
+ this.setState({show});
+
+ if (!show && this.state.currentScreen >= this.props.screens.length - 1) {
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ }
+ handleNext() {
+ if (this.state.currentScreen < this.props.screens.length - 1) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ this.toggle();
+ }
+ skipTutorial(e) {
+ e.preventDefault();
+ const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
+ AsyncClient.savePreferences([preference]);
+ }
+ render() {
+ const buttonText = this.state.currentScreen === this.props.screens.length - 1 ? 'Okay' : 'Next';
+
+ const dots = [];
+ if (this.props.screens.length > 1) {
+ for (let i = 0; i < this.props.screens.length; i++) {
+ if (i === this.state.currentScreen) {
+ dots.push(
+ <div
+ className='circle active'
+ key={'dotactive' + i}
+ />
+ );
+ } else {
+ dots.push(
+ <div
+ className='circle'
+ key={'dotinactive' + i}
+ />
+ );
+ }
+ }
+ }
+
+ return (
+ <div className={'tip-div ' + this.props.overlayClass}>
+ <img
+ className='tip-button'
+ src='/static/images/tutorialTip.gif'
+ width='35'
+ onClick={this.toggle}
+ ref='target'
+ />
+
+ <Overlay
+ show={this.state.show}
+ >
+ <div className='tip-backdrop'/>
+ </Overlay>
+
+ <Overlay
+ placement={this.props.placement}
+ show={this.state.show}
+ rootClose={true}
+ onHide={this.toggle}
+ target={() => this.refs.target}
+ >
+ <div className={'tip-overlay ' + this.props.overlayClass}>
+ <div className='arrow'></div>
+ {this.props.screens[this.state.currentScreen]}
+ <div className='tutorial__circles'>{dots}</div>
+ <div className='text-right'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleNext}
+ >
+ {buttonText}
+ </button>
+ <div className='tip-opt'>
+ {'Seen this before? '}
+ <a
+ href='#'
+ onClick={this.skipTutorial}
+ >
+ {'Opt out of these tips.'}
+ </a>
+ </div>
+ </div>
+ </div>
+ </Overlay>
+ </div>
+ );
+ }
+}
+
+TutorialTip.defaultProps = {
+ overlayClass: ''
+};
+
+TutorialTip.propTypes = {
+ screens: React.PropTypes.array.isRequired,
+ placement: React.PropTypes.string.isRequired,
+ overlayClass: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx
new file mode 100644
index 000000000..eef4b24ba
--- /dev/null
+++ b/web/react/components/user_settings/code_theme_chooser.jsx
@@ -0,0 +1,55 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Constants = require('../../utils/constants.jsx');
+
+export default class CodeThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const premadeThemes = [];
+ for (const k in Constants.CODE_THEMES) {
+ if (Constants.CODE_THEMES.hasOwnProperty(k)) {
+ let activeClass = '';
+ if (k === theme.codeTheme) {
+ activeClass = 'active';
+ }
+
+ premadeThemes.push(
+ <div
+ className='col-xs-6 col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
+ <div
+ className={activeClass}
+ onClick={() => this.props.updateTheme(k)}
+ >
+ <label>
+ <img
+ className='img-responsive'
+ src={'/static/images/themes/code_themes/' + k + '.png'}
+ />
+ <div className='theme-label'>{Constants.CODE_THEMES[k]}</div>
+ </label>
+ </div>
+ </div>
+ );
+ }
+ }
+
+ return (
+ <div className='row'>
+ {premadeThemes}
+ </div>
+ );
+ }
+}
+
+CodeThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 44b3f4544..095e5b622 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -40,11 +40,12 @@ export default class CustomThemeChooser extends React.Component {
const theme = {type: 'custom'};
let index = 0;
Constants.THEME_ELEMENTS.forEach((element) => {
- if (index < colors.length) {
+ if (index < colors.length - 1) {
theme[element.id] = colors[index];
}
index++;
});
+ theme.codeTheme = colors[colors.length - 1];
this.props.updateTheme(theme);
}
@@ -78,6 +79,8 @@ export default class CustomThemeChooser extends React.Component {
colors += theme[element.id] + ',';
});
+ colors += theme.codeTheme;
+
const pasteBox = (
<div className='col-sm-12'>
<label className='custom-label'>
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 6e9b2205d..93be988d1 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Constants = require('../../utils/constants.jsx');
-var ChannelStore = require('../../stores/channel_store.jsx');
-var LoadingScreen = require('../loading_screen.jsx');
+const LoadingScreen = require('../loading_screen.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+
+const Client = require('../../utils/client.jsx');
+const Constants = require('../../utils/constants.jsx');
export default class ManageOutgoingHooks extends React.Component {
constructor() {
@@ -45,10 +46,10 @@ export default class ManageOutgoingHooks extends React.Component {
hooks = [];
}
hooks.push(data);
- this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({addError: err.message});
}
);
}
@@ -75,7 +76,7 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({hooks});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -94,10 +95,10 @@ export default class ManageOutgoingHooks extends React.Component {
}
}
- this.setState({hooks, serverError: null});
+ this.setState({hooks, editError: null});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -105,11 +106,11 @@ export default class ManageOutgoingHooks extends React.Component {
Client.listOutgoingHooks(
(data) => {
if (data) {
- this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ this.setState({hooks: data, getHooksComplete: true, editError: null});
}
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -123,9 +124,13 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({callbackURLs: e.target.value});
}
render() {
- let serverError;
- if (this.state.serverError) {
- serverError = <label className='has-error'>{this.state.serverError}</label>;
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
}
const channels = ChannelStore.getAll();
@@ -235,7 +240,9 @@ export default class ManageOutgoingHooks extends React.Component {
return (
<div key='addOutgoingHook'>
+ {'Create webhooks to send new message events to an external integration. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
<label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top divider-light'></div>
<div className='padding-top'>
<div>
<label className='control-label'>{'Channel'}</label>
@@ -274,10 +281,11 @@ export default class ManageOutgoingHooks extends React.Component {
resize={false}
rows={3}
onChange={this.updateCallbackURLs}
+ placeholder='Each URL must start with http:// or https://'
/>
</div>
<div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
- {serverError}
+ {addError}
</div>
<div className='padding-top padding-bottom'>
<a
@@ -291,6 +299,7 @@ export default class ManageOutgoingHooks extends React.Component {
</div>
</div>
{existingHooks}
+ {editError}
</div>
);
}
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 15bf961d6..546e26ca3 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
var IntegrationsTab = require('./user_settings_integrations.jsx');
var DisplayTab = require('./user_settings_display.jsx');
+var AdvancedTab = require('./user_settings_advanced.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -110,6 +111,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'advanced') {
+ return (
+ <div>
+ <AdvancedTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
new file mode 100644
index 000000000..910444735
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Client = require('../../utils/client.jsx');
+const SettingItemMin = require('../setting_item_min.jsx');
+const SettingItemMax = require('../setting_item_max.jsx');
+const Constants = require('../../utils/constants.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+
+export default class AdvancedSettingsDisplay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.updateSetting = this.updateSetting.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState();
+ }
+
+ setupInitialState() {
+ const sendOnCtrlEnter = PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value;
+
+ return {
+ settings: {send_on_ctrl_enter: sendOnCtrlEnter}
+ };
+ }
+
+ updateSetting(setting, value) {
+ const settings = this.state.settings;
+ settings[setting] = value;
+ this.setState(settings);
+ }
+
+ handleSubmit(setting) {
+ const preference = PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ this.state.settings[setting]
+ );
+
+ Client.savePreferences([preference],
+ () => {
+ PreferenceStore.emitChange();
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ updateSection(section) {
+ this.props.updateSection(section);
+ }
+
+ handleClose() {
+ this.updateSection('');
+ }
+
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+
+ render() {
+ const serverError = this.state.serverError || null;
+ let ctrlSendSection;
+
+ if (this.props.activeSection === 'advancedCtrlSend') {
+ const ctrlSendActive = [
+ this.state.settings.send_on_ctrl_enter === 'true',
+ this.state.settings.send_on_ctrl_enter === 'false'
+ ];
+
+ const inputs = [
+ <div key='ctrlSendSetting'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[0]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
+ />
+ {'On'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[1]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
+ />
+ {'Off'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div>
+ </div>
+ ];
+
+ ctrlSendSection = (
+ <SettingItemMax
+ title='Send messages on Ctrl + Enter'
+ inputs={inputs}
+ submit={() => this.handleSubmit('send_on_ctrl_enter')}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ ctrlSendSection = (
+ <SettingItemMin
+ title='Send messages on Ctrl + Enter'
+ describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'}
+ updateSection={() => this.props.updateSection('advancedCtrlSend')}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Advanced Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Advanced Settings'}</h3>
+ <div className='divider-dark first'/>
+ {ctrlSendSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+AdvancedSettingsDisplay.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 8c62a189d..7b4b54e27 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -7,6 +7,7 @@ var Utils = require('../../utils/utils.jsx');
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
+const CodeThemeChooser = require('./code_theme_chooser.jsx');
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
@@ -18,12 +19,14 @@ export default class UserSettingsAppearance extends React.Component {
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
+ this.updateCodeTheme = this.updateCodeTheme.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
this.state = this.getStateFromStores();
this.originalTheme = this.state.theme;
+ this.originalCodeTheme = this.state.theme.codeTheme;
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -58,6 +61,10 @@ export default class UserSettingsAppearance extends React.Component {
type = 'custom';
}
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ }
+
return {theme, type};
}
onChange() {
@@ -93,6 +100,15 @@ export default class UserSettingsAppearance extends React.Component {
);
}
updateTheme(theme) {
+ if (!theme.codeTheme) {
+ theme.codeTheme = this.state.theme.codeTheme;
+ }
+ this.setState({theme});
+ Utils.applyTheme(theme);
+ }
+ updateCodeTheme(codeTheme) {
+ var theme = this.state.theme;
+ theme.codeTheme = codeTheme;
this.setState({theme});
Utils.applyTheme(theme);
}
@@ -102,6 +118,7 @@ export default class UserSettingsAppearance extends React.Component {
handleClose() {
const state = this.getStateFromStores();
state.serverError = null;
+ state.theme.codeTheme = this.originalCodeTheme;
Utils.applyTheme(state.theme);
@@ -170,7 +187,13 @@ export default class UserSettingsAppearance extends React.Component {
</div>
{custom}
<hr />
- {serverError}
+ <strong className='radio'>{'Code Theme'}</strong>
+ <CodeThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateCodeTheme}
+ />
+ <hr />
+ {serverError}
<a
className='btn btn-sm btn-primary'
href='#'
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 22a62273c..d086c78a9 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -9,8 +9,12 @@ import PreferenceStore from '../../stores/preference_store.jsx';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
+ const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
- return {militaryTime: militaryTime.value};
+ return {
+ militaryTime: militaryTime.value,
+ nameFormat: nameFormat.value
+ };
}
export default class UserSettingsDisplay extends React.Component {
@@ -19,15 +23,17 @@ export default class UserSettingsDisplay extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClockRadio = this.handleClockRadio.bind(this);
+ this.handleNameRadio = this.handleNameRadio.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = getDisplayStateFromStores();
}
handleSubmit() {
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
- savePreferences([preference],
+ savePreferences([timePreference, namePreference],
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -40,6 +46,9 @@ export default class UserSettingsDisplay extends React.Component {
handleClockRadio(militaryTime) {
this.setState({militaryTime});
}
+ handleNameRadio(nameFormat) {
+ this.setState({nameFormat});
+ }
updateSection(section) {
this.setState(getDisplayStateFromStores());
this.props.updateSection(section);
@@ -56,6 +65,7 @@ export default class UserSettingsDisplay extends React.Component {
render() {
const serverError = this.state.serverError || null;
let clockSection;
+ let nameFormatSection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -127,6 +137,88 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === 'name_format') {
+ const nameFormat = [false, false, false];
+ if (this.state.nameFormat === 'nickname_full_name') {
+ nameFormat[0] = true;
+ } else if (this.state.nameFormat === 'full_name') {
+ nameFormat[2] = true;
+ } else {
+ nameFormat[1] = true;
+ }
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[0]}
+ onChange={this.handleNameRadio.bind(this, 'nickname_full_name')}
+ />
+ {'Show nickname if one exists, otherwise show first and last name (team default)'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[1]}
+ onChange={this.handleNameRadio.bind(this, 'username')}
+ />
+ {'Show username'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[2]}
+ onChange={this.handleNameRadio.bind(this, 'full_name')}
+ />
+ {'Show first and last name'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'How should other users be shown in Direct Messages list?'}</div>
+ </div>
+ ];
+
+ nameFormatSection = (
+ <SettingItemMax
+ title='Show real names, nick names or usernames?'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.nameFormat === 'username') {
+ describe = 'Show username';
+ } else if (this.state.nameFormat === 'full_name') {
+ describe = 'Show first and last name';
+ } else {
+ describe = 'Show nickname if one exists, otherwise show first and last name (team default)';
+ }
+
+ nameFormatSection = (
+ <SettingItemMin
+ title='Show real names, nick names or usernames?'
+ describe={describe}
+ updateSection={() => {
+ this.props.updateSection('name_format');
+ }}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -151,6 +243,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark first'/>
{clockSection}
<div className='divider-dark'/>
+ {nameFormatSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 70e559c30..3adac197a 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}.bind(this),
function imageUploadFailure(err) {
var state = this.setupInitialState(this.props);
- state.serverError = err;
+ state.serverError = err.message;
this.setState(state);
}.bind(this)
);
@@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
}
+
return (
<div>
<div className='modal-header'>
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 4b1e5e532..4a9915a1f 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -54,7 +55,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
- describe='Manage your incoming webhooks (Developer feature)'
+ width='medium'
+ describe='Manage your incoming webhooks'
updateSection={() => {
this.updateSection('incoming-hooks');
}}
@@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMax
title='Outgoing Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMin
title='Outgoing Webhooks'
+ width='medium'
describe='Manage your outgoing webhooks'
updateSection={() => {
this.updateSection('outgoing-hooks');
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 5449ae91e..18dd490e7 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
+ tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'});
return (
<div
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 61d49acb2..2b904763c 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -37,18 +37,18 @@ function getNotificationsStateFromStores() {
if (user.notify_props.mention_keys) {
var keys = user.notify_props.mention_keys.split(',');
- if (keys.indexOf(user.username) !== -1) {
+ if (keys.indexOf(user.username) === -1) {
+ usernameKey = false;
+ } else {
usernameKey = true;
keys.splice(keys.indexOf(user.username), 1);
- } else {
- usernameKey = false;
}
- if (keys.indexOf('@' + user.username) !== -1) {
+ if (keys.indexOf('@' + user.username) === -1) {
+ mentionKey = false;
+ } else {
mentionKey = true;
keys.splice(keys.indexOf('@' + user.username), 1);
- } else {
- mentionKey = false;
}
customKeys = keys.join(',');
diff --git a/web/react/package.json b/web/react/package.json
index e6a662375..9af6f5880 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -6,6 +6,7 @@
"autolinker": "0.18.1",
"babel-runtime": "5.8.24",
"flux": "2.1.1",
+ "highlight.js": "^8.9.1",
"keymirror": "0.1.1",
"marked": "0.3.5",
"object-assign": "3.0.0",
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 03e049db0..067dcde50 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -2,13 +2,12 @@
// See License.txt for license information.
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Navbar = require('../components/navbar.jsx');
-var Sidebar = require('../components/sidebar.jsx');
-var ChannelHeader = require('../components/channel_header.jsx');
-var PostListContainer = require('../components/post_list_container.jsx');
-var CreatePost = require('../components/create_post.jsx');
-var SidebarRight = require('../components/sidebar_right.jsx');
-var SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+var ChannelView = require('../components/channel_view.jsx');
+var ChannelLoader = require('../components/channel_loader.jsx');
+var ErrorBar = require('../components/error_bar.jsx');
+var ErrorStore = require('../stores/error_store.jsx');
+
+var MentionList = require('../components/mention_list.jsx');
var GetLinkModal = require('../components/get_link_modal.jsx');
var MemberInviteModal = require('../components/invite_member_modal.jsx');
var EditChannelModal = require('../components/edit_channel_modal.jsx');
@@ -24,15 +23,10 @@ var TeamSettingsModal = require('../components/team_settings_modal.jsx');
var ChannelMembersModal = require('../components/channel_members.jsx');
var ChannelInviteModal = require('../components/channel_invite_modal.jsx');
var TeamMembersModal = require('../components/team_members.jsx');
-var ErrorBar = require('../components/error_bar.jsx');
-var ErrorStore = require('../stores/error_store.jsx');
-var ChannelLoader = require('../components/channel_loader.jsx');
-var MentionList = require('../components/mention_list.jsx');
var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var AccessHistoryModal = require('../components/access_history_modal.jsx');
var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
-var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
@@ -61,20 +55,29 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <Navbar teamDisplayName={props.TeamDisplayName} />,
- document.getElementById('navbar')
+ <ChannelView/>,
+ document.getElementById('channel_view')
);
ReactDOM.render(
- <Sidebar
- teamDisplayName={props.TeamDisplayName}
- teamName={props.TeamName}
- teamType={props.TeamType}
- />,
- document.getElementById('sidebar-left')
+ <MentionList id='post_textbox' />,
+ document.getElementById('post_mention_tab')
);
ReactDOM.render(
+ <MentionList id='reply_textbox' />,
+ document.getElementById('reply_mention_tab')
+ );
+
+ ReactDOM.render(
+ <MentionList id='edit_textbox' />,
+ document.getElementById('edit_mention_tab')
+ );
+
+ //
+ // Modals
+ //
+ ReactDOM.render(
<GetLinkModal />,
document.getElementById('get_link_modal')
);
@@ -90,7 +93,7 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <TeamSettingsModal teamDisplayName={props.TeamDisplayName} />,
+ <TeamSettingsModal />,
document.getElementById('team_settings_modal')
);
@@ -105,11 +108,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <ChannelHeader />,
- document.getElementById('channel-header')
- );
-
- ReactDOM.render(
<EditChannelModal />,
document.getElementById('edit_channel_modal')
);
@@ -150,11 +148,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <PostListContainer />,
- document.getElementById('post-list')
- );
-
- ReactDOM.render(
<EditPostModal />,
document.getElementById('edit_post_modal')
);
@@ -170,39 +163,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <CreatePost />,
- document.getElementById('post-create')
- );
-
- ReactDOM.render(
- <SidebarRight />,
- document.getElementById('sidebar-right')
- );
-
- ReactDOM.render(
- <SidebarRightMenu
- teamDisplayName={props.TeamDisplayName}
- teamType={props.TeamType}
- />,
- document.getElementById('sidebar-menu')
- );
-
- ReactDOM.render(
- <MentionList id='post_textbox' />,
- document.getElementById('post_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='reply_textbox' />,
- document.getElementById('reply_mention_tab')
- );
-
- ReactDOM.render(
- <MentionList id='edit_textbox' />,
- document.getElementById('edit_mention_tab')
- );
-
- ReactDOM.render(
<AccessHistoryModal />,
document.getElementById('access_history_modal')
);
@@ -218,13 +178,6 @@ function setupChannelPage(props) {
);
ReactDOM.render(
- <FileUploadOverlay
- overlayType='center'
- />,
- document.getElementById('file_upload_overlay')
- );
-
- ReactDOM.render(
<RegisterAppModal />,
document.getElementById('register_app_modal')
);
diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx
index 430de980c..9865e6fd2 100644
--- a/web/react/pages/login.jsx
+++ b/web/react/pages/login.jsx
@@ -8,6 +8,7 @@ function setupLoginPage(props) {
<Login
teamDisplayName={props.TeamDisplayName}
teamName={props.TeamName}
+ inviteId={props.InviteId}
/>,
document.getElementById('login')
);
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
index dc8394a77..caa93b5bf 100644
--- a/web/react/pages/signup_team.jsx
+++ b/web/react/pages/signup_team.jsx
@@ -3,9 +3,19 @@
var SignupTeam = require('../components/signup_team.jsx');
-function setupSignupTeamPage() {
+function setupSignupTeamPage(props) {
+ var teams = [];
+
+ for (var prop in props) {
+ if (props.hasOwnProperty(prop)) {
+ if (prop !== 'Title') {
+ teams.push({name: prop, display_name: props[prop]});
+ }
+ }
+ }
+
ReactDOM.render(
- <SignupTeam />,
+ <SignupTeam teams={teams} />,
document.getElementById('signup-team')
);
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 4a9314b31..0fe253310 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -12,12 +12,9 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var CHANGE_EVENT = 'change';
-var SEARCH_CHANGE_EVENT = 'search_change';
-var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
-var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
-var ADD_MENTION_EVENT = 'add_mention';
var EDIT_POST_EVENT = 'edit_post';
+var POSTS_VIEW_JUMP_EVENT = 'post_list_jump';
class PostStoreClass extends EventEmitter {
constructor() {
@@ -26,21 +23,19 @@ class PostStoreClass extends EventEmitter {
this.emitChange = this.emitChange.bind(this);
this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this);
- this.emitSearchChange = this.emitSearchChange.bind(this);
- this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
- this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
- this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
- this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
- this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
+
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
- this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
- this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
- this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
- this.emitAddMention = this.emitAddMention.bind(this);
- this.addAddMentionListener = this.addAddMentionListener.bind(this);
- this.removeAddMentionListener = this.removeAddMentionListener.bind(this);
+
+ this.emitEditPost = this.emitEditPost.bind(this);
+ this.addEditPostListener = this.addEditPostListener.bind(this);
+ this.removeEditPostListener = this.removeEditPostListner.bind(this);
+
+ this.emitPostsViewJump = this.emitPostsViewJump.bind(this);
+ this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this);
+ this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this);
+
this.getCurrentPosts = this.getCurrentPosts.bind(this);
this.storePosts = this.storePosts.bind(this);
this.pStorePosts = this.pStorePosts.bind(this);
@@ -59,13 +54,8 @@ class PostStoreClass extends EventEmitter {
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
this.clearPendingPosts = this.clearPendingPosts.bind(this);
this.updatePendingPost = this.updatePendingPost.bind(this);
- this.storeSearchResults = this.storeSearchResults.bind(this);
- this.getSearchResults = this.getSearchResults.bind(this);
- this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
this.storeSelectedPost = this.storeSelectedPost.bind(this);
this.getSelectedPost = this.getSelectedPost.bind(this);
- this.storeSearchTerm = this.storeSearchTerm.bind(this);
- this.getSearchTerm = this.getSearchTerm.bind(this);
this.getEmptyDraft = this.getEmptyDraft.bind(this);
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
this.getCurrentDraft = this.getCurrentDraft.bind(this);
@@ -77,9 +67,6 @@ class PostStoreClass extends EventEmitter {
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
this.getLatestUpdate = this.getLatestUpdate.bind(this);
- this.emitEditPost = this.emitEditPost.bind(this);
- this.addEditPostListener = this.addEditPostListener.bind(this);
- this.removeEditPostListener = this.removeEditPostListener.bind(this);
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
}
emitChange() {
@@ -94,30 +81,6 @@ class PostStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
- emitSearchChange() {
- this.emit(SEARCH_CHANGE_EVENT);
- }
-
- addSearchChangeListener(callback) {
- this.on(SEARCH_CHANGE_EVENT, callback);
- }
-
- removeSearchChangeListener(callback) {
- this.removeListener(SEARCH_CHANGE_EVENT, callback);
- }
-
- emitSearchTermChange(doSearch, isMentionSearch) {
- this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
- }
-
- addSearchTermChangeListener(callback) {
- this.on(SEARCH_TERM_CHANGE_EVENT, callback);
- }
-
- removeSearchTermChangeListener(callback) {
- this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
- }
-
emitSelectedPostChange(fromSearch) {
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
}
@@ -130,40 +93,40 @@ class PostStoreClass extends EventEmitter {
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
}
- emitMentionDataChange(id, mentionText) {
- this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
+ emitEditPost(post) {
+ this.emit(EDIT_POST_EVENT, post);
}
- addMentionDataChangeListener(callback) {
- this.on(MENTION_DATA_CHANGE_EVENT, callback);
+ addEditPostListener(callback) {
+ this.on(EDIT_POST_EVENT, callback);
}
- removeMentionDataChangeListener(callback) {
- this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
+ removeEditPostListner(callback) {
+ this.removeListener(EDIT_POST_EVENT, callback);
}
- emitAddMention(id, username) {
- this.emit(ADD_MENTION_EVENT, id, username);
+ emitPostsViewJump(type, post) {
+ this.emit(POSTS_VIEW_JUMP_EVENT, type, post);
}
- addAddMentionListener(callback) {
- this.on(ADD_MENTION_EVENT, callback);
+ addPostsViewJumpListener(callback) {
+ this.on(POSTS_VIEW_JUMP_EVENT, callback);
}
- removeAddMentionListener(callback) {
- this.removeListener(ADD_MENTION_EVENT, callback);
+ removePostsViewJumpListener(callback) {
+ this.removeListener(POSTS_VIEW_JUMP_EVENT, callback);
}
- emitEditPost(post) {
- this.emit(EDIT_POST_EVENT, post);
+ jumpPostsViewToBottom() {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.BOTTOM, null);
}
- addEditPostListener(callback) {
- this.on(EDIT_POST_EVENT, callback);
+ jumpPostsViewToPost(post) {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.POST, post);
}
- removeEditPostListener(callback) {
- this.removeListener(EDIT_POST_EVENT, callback);
+ jumpPostsViewSidebarOpen() {
+ this.emitPostsViewJump(Constants.PostsViewJumpTypes.SIDEBAR_OPEN, null);
}
getCurrentPosts() {
@@ -174,16 +137,16 @@ class PostStoreClass extends EventEmitter {
}
return null;
}
- storePosts(channelId, newPostList) {
- if (isPostListNull(newPostList)) {
+ storePosts(channelId, newPostsView) {
+ if (isPostListNull(newPostsView)) {
return;
}
var postList = makePostListNonNull(this.getPosts(channelId));
- for (let pid in newPostList.posts) {
- if (newPostList.posts.hasOwnProperty(pid)) {
- var np = newPostList.posts[pid];
+ for (const pid in newPostsView.posts) {
+ if (newPostsView.posts.hasOwnProperty(pid)) {
+ const np = newPostsView.posts[pid];
if (np.delete_at === 0) {
postList.posts[pid] = np;
if (postList.order.indexOf(pid) === -1) {
@@ -194,7 +157,7 @@ class PostStoreClass extends EventEmitter {
delete postList.posts[pid];
}
- var index = postList.order.indexOf(pid);
+ const index = postList.order.indexOf(pid);
if (index !== -1) {
postList.order.splice(index, 1);
}
@@ -202,7 +165,7 @@ class PostStoreClass extends EventEmitter {
}
}
- postList.order.sort(function postSort(a, b) {
+ postList.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1;
}
@@ -238,14 +201,15 @@ class PostStoreClass extends EventEmitter {
var lastPost = null;
for (i; i < len; i++) {
- if (postList.posts[postList.order[i]].user_id === userId) {
+ let post = postList.posts[postList.order[i]];
+ if (post.user_id === userId && (post.props && !post.props.from_webhook || !post.props)) {
if (rootId) {
- if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
- lastPost = postList.posts[postList.order[i]];
+ if (post.root_id === rootId || post.id === rootId) {
+ lastPost = post;
break;
}
} else {
- lastPost = postList.posts[postList.order[i]];
+ lastPost = post;
break;
}
}
@@ -306,7 +270,7 @@ class PostStoreClass extends EventEmitter {
var posts = postList.posts;
// sort failed posts to the bottom
- postList.order.sort(function postSort(a, b) {
+ postList.order.sort((a, b) => {
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
return 1;
}
@@ -371,7 +335,7 @@ class PostStoreClass extends EventEmitter {
this.pStorePendingPosts(channelId, postList);
}
clearPendingPosts() {
- BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
BrowserStore.removeItem(key);
});
}
@@ -387,28 +351,12 @@ class PostStoreClass extends EventEmitter {
this.pStorePendingPosts(post.channel_id, postList);
this.emitChange();
}
- storeSearchResults(results, isMentionSearch) {
- BrowserStore.setItem('search_results', results);
- BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
- }
- getSearchResults() {
- return BrowserStore.getItem('search_results');
- }
- getIsMentionSearch() {
- return BrowserStore.getItem('is_mention_search');
- }
storeSelectedPost(postList) {
BrowserStore.setItem('select_post', postList);
}
getSelectedPost() {
return BrowserStore.getItem('select_post');
}
- storeSearchTerm(term) {
- BrowserStore.setItem('search_term', term);
- }
- getSearchTerm() {
- return BrowserStore.getItem('search_term');
- }
getEmptyDraft() {
return {message: '', uploadsInProgress: [], previews: []};
}
@@ -433,7 +381,7 @@ class PostStoreClass extends EventEmitter {
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
}
clearDraftUploads() {
- BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
@@ -441,7 +389,7 @@ class PostStoreClass extends EventEmitter {
});
}
clearCommentDraftUploads() {
- BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
@@ -458,7 +406,7 @@ class PostStoreClass extends EventEmitter {
var PostStore = new PostStoreClass();
-PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+PostStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
@@ -469,24 +417,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
PostStore.pStorePost(action.post);
PostStore.emitChange();
break;
- case ActionTypes.RECIEVED_SEARCH:
- PostStore.storeSearchResults(action.results, action.is_mention_search);
- PostStore.emitSearchChange();
- break;
- case ActionTypes.RECIEVED_SEARCH_TERM:
- PostStore.storeSearchTerm(action.term);
- PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
- break;
case ActionTypes.RECIEVED_POST_SELECTED:
PostStore.storeSelectedPost(action.post_list);
PostStore.emitSelectedPostChange(action.from_search);
break;
- case ActionTypes.RECIEVED_MENTION_DATA:
- PostStore.emitMentionDataChange(action.id, action.mention_text);
- break;
- case ActionTypes.RECIEVED_ADD_MENTION:
- PostStore.emitAddMention(action.id, action.username);
- break;
case ActionTypes.RECIEVED_EDIT_POST:
PostStore.emitEditPost(action);
break;
diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx
new file mode 100644
index 000000000..95f0ea845
--- /dev/null
+++ b/web/react/stores/search_store.jsx
@@ -0,0 +1,153 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var EventEmitter = require('events').EventEmitter;
+
+var BrowserStore = require('../stores/browser_store.jsx');
+
+var Constants = require('../utils/constants.jsx');
+var ActionTypes = Constants.ActionTypes;
+
+var CHANGE_EVENT = 'change';
+var SEARCH_CHANGE_EVENT = 'search_change';
+var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
+var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
+var ADD_MENTION_EVENT = 'add_mention';
+
+class SearchStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.emitChange = this.emitChange.bind(this);
+ this.addChangeListener = this.addChangeListener.bind(this);
+ this.removeChangeListener = this.removeChangeListener.bind(this);
+
+ this.emitSearchChange = this.emitSearchChange.bind(this);
+ this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
+ this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
+
+ this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
+ this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
+ this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
+
+ this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
+ this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
+ this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
+
+ this.getSearchResults = this.getSearchResults.bind(this);
+ this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
+
+ this.storeSearchTerm = this.storeSearchTerm.bind(this);
+ this.getSearchTerm = this.getSearchTerm.bind(this);
+
+ this.storeSearchResults = this.storeSearchResults.bind(this);
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ emitSearchChange() {
+ this.emit(SEARCH_CHANGE_EVENT);
+ }
+
+ addSearchChangeListener(callback) {
+ this.on(SEARCH_CHANGE_EVENT, callback);
+ }
+
+ removeSearchChangeListener(callback) {
+ this.removeListener(SEARCH_CHANGE_EVENT, callback);
+ }
+
+ emitSearchTermChange(doSearch, isMentionSearch) {
+ this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
+ }
+
+ addSearchTermChangeListener(callback) {
+ this.on(SEARCH_TERM_CHANGE_EVENT, callback);
+ }
+
+ removeSearchTermChangeListener(callback) {
+ this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
+ }
+
+ getSearchResults() {
+ return BrowserStore.getItem('search_results');
+ }
+
+ getIsMentionSearch() {
+ return BrowserStore.getItem('is_mention_search');
+ }
+
+ storeSearchTerm(term) {
+ BrowserStore.setItem('search_term', term);
+ }
+
+ getSearchTerm() {
+ return BrowserStore.getItem('search_term');
+ }
+
+ emitMentionDataChange(id, mentionText) {
+ this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
+ }
+
+ addMentionDataChangeListener(callback) {
+ this.on(MENTION_DATA_CHANGE_EVENT, callback);
+ }
+
+ removeMentionDataChangeListener(callback) {
+ this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
+ }
+
+ emitAddMention(id, username) {
+ this.emit(ADD_MENTION_EVENT, id, username);
+ }
+
+ addAddMentionListener(callback) {
+ this.on(ADD_MENTION_EVENT, callback);
+ }
+
+ removeAddMentionListener(callback) {
+ this.removeListener(ADD_MENTION_EVENT, callback);
+ }
+
+ storeSearchResults(results, isMentionSearch) {
+ BrowserStore.setItem('search_results', results);
+ BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
+ }
+}
+
+var SearchStore = new SearchStoreClass();
+
+SearchStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIEVED_SEARCH:
+ SearchStore.storeSearchResults(action.results, action.is_mention_search);
+ SearchStore.emitSearchChange();
+ break;
+ case ActionTypes.RECIEVED_SEARCH_TERM:
+ SearchStore.storeSearchTerm(action.term);
+ SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
+ break;
+ case ActionTypes.RECIEVED_MENTION_DATA:
+ SearchStore.emitMentionDataChange(action.id, action.mention_text);
+ break;
+ case ActionTypes.RECIEVED_ADD_MENTION:
+ SearchStore.emitAddMention(action.id, action.username);
+ break;
+ default:
+ }
+});
+
+export default SearchStore;
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 9410c1e9c..4efeb7c8f 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -86,7 +86,7 @@ class SocketStoreClass extends EventEmitter {
this.failCount = this.failCount + 1;
- ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.'});
+ ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'});
ErrorStore.emitChange();
};
@@ -160,12 +160,12 @@ function handleNewPostEvent(msg) {
if (window.isActive) {
AsyncClient.updateLastViewedAt(true);
}
- } else {
+ } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
AsyncClient.getChannel(msg.channel_id);
}
// Send desktop notification
- if (UserStore.getCurrentId() !== msg.user_id) {
+ if (UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') {
const msgProps = msg.props;
let mentions = [];
@@ -189,7 +189,9 @@ function handleNewPostEvent(msg) {
}
let username = 'Someone';
- if (UserStore.hasProfile(msg.user_id)) {
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
+ username = post.props.override_username;
+ } else if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index b1bc71d54..75dd35e3f 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -132,7 +132,7 @@ export function getChannel(id) {
callTracker['getChannel' + id] = utils.getTimestamp();
client.getChannel(id,
- function getChannelSuccess(data, textStatus, xhr) {
+ (data, textStatus, xhr) => {
callTracker['getChannel' + id] = 0;
if (xhr.status === 304 || !data) {
@@ -145,7 +145,7 @@ export function getChannel(id) {
member: data.member
});
},
- function getChannelFailure(err) {
+ (err) => {
callTracker['getChannel' + id] = 0;
dispatchError(err, 'getChannel');
}
diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx
new file mode 100644
index 000000000..b3f868456
--- /dev/null
+++ b/web/react/utils/channel_intro_mssages.jsx
@@ -0,0 +1,218 @@
+
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Utils = require('./utils.jsx');
+const UserProfile = require('../components/user_profile.jsx');
+const ChannelStore = require('../stores/channel_store.jsx');
+const Constants = require('../utils/constants.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+
+export function createChannelIntroMessage(channel) {
+ if (channel.type === 'D') {
+ return createDMIntroMessage(channel);
+ } else if (ChannelStore.isDefault(channel)) {
+ return createDefaultIntroMessage(channel);
+ } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
+ return createOffTopicIntroMessage(channel);
+ } else if (channel.type === 'O' || channel.type === 'P') {
+ return createStandardIntroMessage(channel);
+ }
+}
+
+export function createDMIntroMessage(channel) {
+ var teammate = Utils.getDirectTeammate(channel.id);
+
+ if (teammate) {
+ var teammateName = teammate.username;
+ if (teammate.nickname.length > 0) {
+ teammateName = teammate.nickname;
+ }
+
+ return (
+ <div className='channel-intro'>
+ <div className='post-profile-img__container channel-intro-img'>
+ <img
+ className='post-profile-img'
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
+ height='50'
+ width='50'
+ />
+ </div>
+ <div className='channel-intro-profile'>
+ <strong>
+ <UserProfile userId={teammate.id} />
+ </strong>
+ </div>
+ <p className='channel-intro-text'>
+ {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
+ {'Direct messages and files shared here are not shown to people outside this area.'}
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ </div>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
+ </div>
+ );
+}
+
+export function createOffTopicIntroMessage(channel) {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this channel'}
+ </a>
+ </div>
+ );
+}
+
+export function createDefaultIntroMessage(channel) {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this team'}
+ </a>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
+ <p className='channel-intro__content'>
+ <strong>{'Welcome to ' + channel.display_name + '!'}</strong>
+ <br/><br/>
+ {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ </p>
+ {inviteModalLink}
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <br/>
+ </div>
+ );
+}
+
+export function createStandardIntroMessage(channel) {
+ var uiName = channel.display_name;
+ var creatorName = '';
+
+ var uiType;
+ var memberMessage;
+ if (channel.type === 'P') {
+ uiType = 'private group';
+ memberMessage = ' Only invited members can see this private group.';
+ } else {
+ uiType = 'channel';
+ memberMessage = ' Any member can join and read this channel.';
+ }
+
+ var createMessage;
+ if (creatorName === '') {
+ createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + Utils.displayDate(channel.create_at) + '.';
+ } else {
+ createMessage = (
+ <span>
+ {'This is the start of the '}
+ <strong>{uiName}</strong>
+ {' '}
+ {uiType}{', created by '}
+ <strong>{creatorName}</strong>
+ {' on '}
+ <strong>{Utils.displayDate(channel.create_at)}</strong>
+ </span>
+ );
+ }
+
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
+ <p className='channel-intro__content'>
+ {createMessage}
+ {memberMessage}
+ <br/>
+ </p>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#edit_channel'
+ data-header={channel.header}
+ data-title={channel.display_name}
+ data-channelid={channel.id}
+ >
+ <i className='fa fa-pencil'></i>{'Set a header'}
+ </a>
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#channel_invite'
+ >
+ <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
+ </a>
+ </div>
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index bc73f3c64..7ce1346f9 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -34,7 +34,7 @@ function handleError(methodName, xhr, status, err) {
if (oldError && oldError.connErrorCount) {
errorCount += oldError.connErrorCount;
- connectError = 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.';
+ connectError = 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.';
}
e = {message: connectError, connErrorCount: errorCount};
@@ -328,6 +328,20 @@ export function getConfig(success, error) {
});
}
+export function getAnalytics(teamId, name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + teamId + '/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function saveConfig(config, success, error) {
$.ajax({
url: '/api/v1/admin/save_config',
@@ -428,16 +442,16 @@ export function inviteMembers(data, success, error) {
track('api', 'api_teams_invite_members');
}
-export function updateTeamDisplayName(data, success, error) {
+export function updateTeam(team, success, error) {
$.ajax({
- url: '/api/v1/teams/update_name',
+ url: '/api/v1/teams/update',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify(data),
+ data: JSON.stringify(team),
success,
- error: function onError(xhr, status, err) {
- var e = handleError('updateTeamDisplayName', xhr, status, err);
+ error: (xhr, status, err) => {
+ var e = handleError('updateTeam', xhr, status, err);
error(e);
}
});
@@ -575,21 +589,38 @@ export function updateChannel(channel, success, error) {
track('api', 'api_channels_update');
}
-export function updateChannelDesc(data, success, error) {
+export function updateChannelHeader(data, success, error) {
+ $.ajax({
+ url: '/api/v1/channels/update_header',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('updateChannelHeader', xhr, status, err);
+ error(e);
+ }
+ });
+
+ track('api', 'api_channels_header');
+}
+
+export function updateChannelPurpose(data, success, error) {
$.ajax({
- url: '/api/v1/channels/update_desc',
+ url: '/api/v1/channels/update_purpose',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
- var e = handleError('updateChannelDesc', xhr, status, err);
+ var e = handleError('updateChannelPurpose', xhr, status, err);
error(e);
}
});
- track('api', 'api_channels_desc');
+ track('api', 'api_channels_purpose');
}
export function updateNotifyProps(data, success, error) {
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 72773bf05..fd64b1554 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -98,6 +98,7 @@ module.exports = {
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
+ POST_TYPE_JOIN_LEAVE: 'join_leave',
RESERVED_TEAM_NAMES: [
'www',
'web',
@@ -126,12 +127,15 @@ module.exports = {
MAX_DMS: 20,
DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
+ INVITE_TEAM: 'I',
+ 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>",
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,
THEMES: {
default: {
type: 'Mattermost',
@@ -300,9 +304,24 @@ module.exports = {
uiName: 'Mention Highlight Link'
}
],
+ CODE_THEMES: {
+ github: 'GitHub',
+ solarized_light: 'Solarized light',
+ monokai: 'Monokai',
+ solarized_dark: 'Solarized Dark'
+ },
+ DEFAULT_CODE_THEME: 'github',
Preferences: {
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
- CATEGORY_DISPLAY_SETTINGS: 'display_settings'
+ CATEGORY_DISPLAY_SETTINGS: 'display_settings',
+ CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
+ TUTORIAL_STEP: 'tutorial_step'
+ },
+ TutorialSteps: {
+ INTRO_SCREENS: 0,
+ POST_POPOVER: 1,
+ CHANNEL_POPOVER: 2,
+ MENU_POPOVER: 3
},
KeyCodes: {
UP: 38,
@@ -313,5 +332,35 @@ module.exports = {
ENTER: 13,
ESCAPE: 27,
SPACE: 32
+ },
+ HighlightedLanguages: {
+ diff: 'Diff',
+ apache: 'Apache',
+ makefile: 'Makefile',
+ http: 'HTTP',
+ json: 'JSON',
+ markdown: 'Markdown',
+ javascript: 'JavaScript',
+ css: 'CSS',
+ nginx: 'nginx',
+ objectivec: 'Objective-C',
+ python: 'Python',
+ xml: 'XML',
+ perl: 'Perl',
+ bash: 'Bash',
+ php: 'PHP',
+ coffeescript: 'CoffeeScript',
+ cs: 'C#',
+ cpp: 'C++',
+ sql: 'SQL',
+ go: 'Go',
+ ruby: 'Ruby',
+ java: 'Java',
+ ini: 'ini'
+ },
+ PostsViewJumpTypes: {
+ BOTTOM: 1,
+ POST: 2,
+ SIDEBAR_OPEN: 3
}
};
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 7a4e70054..179416ea0 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -6,7 +6,71 @@ const Utils = require('./utils.jsx');
const marked = require('marked');
-export class MattermostMarkdownRenderer extends marked.Renderer {
+const highlightJs = require('highlight.js/lib/highlight.js');
+const highlightJsDiff = require('highlight.js/lib/languages/diff.js');
+const highlightJsApache = require('highlight.js/lib/languages/apache.js');
+const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js');
+const highlightJsHttp = require('highlight.js/lib/languages/http.js');
+const highlightJsJson = require('highlight.js/lib/languages/json.js');
+const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js');
+const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js');
+const highlightJsCss = require('highlight.js/lib/languages/css.js');
+const highlightJsNginx = require('highlight.js/lib/languages/nginx.js');
+const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js');
+const highlightJsPython = require('highlight.js/lib/languages/python.js');
+const highlightJsXml = require('highlight.js/lib/languages/xml.js');
+const highlightJsPerl = require('highlight.js/lib/languages/perl.js');
+const highlightJsBash = require('highlight.js/lib/languages/bash.js');
+const highlightJsPhp = require('highlight.js/lib/languages/php.js');
+const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js');
+const highlightJsCs = require('highlight.js/lib/languages/cs.js');
+const highlightJsCpp = require('highlight.js/lib/languages/cpp.js');
+const highlightJsSql = require('highlight.js/lib/languages/sql.js');
+const highlightJsGo = require('highlight.js/lib/languages/go.js');
+const highlightJsRuby = require('highlight.js/lib/languages/ruby.js');
+const highlightJsJava = require('highlight.js/lib/languages/java.js');
+const highlightJsIni = require('highlight.js/lib/languages/ini.js');
+
+const Constants = require('../utils/constants.jsx');
+const HighlightedLanguages = Constants.HighlightedLanguages;
+
+class MattermostInlineLexer extends marked.InlineLexer {
+ constructor(links, options) {
+ super(links, options);
+
+ this.rules = Object.assign({}, this.rules);
+
+ // modified version of the regex that doesn't break up words in snake_case,
+ // allows for links starting with www, and allows links succounded by parentheses
+ // the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/
+ this.rules.text = /^[\s\S]+?(?=[^\w\/]_|[\\<!\[*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/;
+
+ // modified version of the regex that allows links starting with www and those surrounded
+ // by parentheses
+ // the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/
+ this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/;
+
+ // modified version of the regex that allows <links> starting with www.
+ // the original is /^<([^ >]+(@|:\/)[^ >]+)>/
+ this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/;
+ }
+}
+
+class MattermostParser extends marked.Parser {
+ parse(src) {
+ this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer);
+ this.tokens = src.reverse();
+
+ var out = '';
+ while (this.next()) {
+ out += this.tok();
+ }
+
+ return out;
+ }
+}
+
+class MattermostMarkdownRenderer extends marked.Renderer {
constructor(options, formattingOptions = {}) {
super(options);
@@ -15,6 +79,49 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
+
+ highlightJs.registerLanguage('diff', highlightJsDiff);
+ highlightJs.registerLanguage('apache', highlightJsApache);
+ highlightJs.registerLanguage('makefile', highlightJsMakefile);
+ highlightJs.registerLanguage('http', highlightJsHttp);
+ highlightJs.registerLanguage('json', highlightJsJson);
+ highlightJs.registerLanguage('markdown', highlightJsMarkdown);
+ highlightJs.registerLanguage('javascript', highlightJsJavascript);
+ highlightJs.registerLanguage('css', highlightJsCss);
+ highlightJs.registerLanguage('nginx', highlightJsNginx);
+ highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
+ highlightJs.registerLanguage('python', highlightJsPython);
+ highlightJs.registerLanguage('xml', highlightJsXml);
+ highlightJs.registerLanguage('perl', highlightJsPerl);
+ highlightJs.registerLanguage('bash', highlightJsBash);
+ highlightJs.registerLanguage('php', highlightJsPhp);
+ highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
+ highlightJs.registerLanguage('cs', highlightJsCs);
+ highlightJs.registerLanguage('cpp', highlightJsCpp);
+ highlightJs.registerLanguage('sql', highlightJsSql);
+ highlightJs.registerLanguage('go', highlightJsGo);
+ highlightJs.registerLanguage('ruby', highlightJsRuby);
+ highlightJs.registerLanguage('java', highlightJsJava);
+ highlightJs.registerLanguage('ini', highlightJsIni);
+ }
+
+ code(code, language) {
+ let usedLanguage = language;
+
+ if (String(usedLanguage).toLocaleLowerCase() === 'html') {
+ usedLanguage = 'xml';
+ }
+
+ if (!usedLanguage || highlightJs.listLanguages().indexOf(usedLanguage) < 0) {
+ let parsed = super.code(code, usedLanguage);
+ return '<div class="post-body--code"><code class="hljs">' + TextFormatting.sanitizeHtml($(parsed).text()) + '</code></div>';
+ }
+
+ let parsed = highlightJs.highlight(usedLanguage, code);
+ return '<div class="post-body--code">' +
+ '<span class="post-body--code__language">' + HighlightedLanguages[usedLanguage] + '</span>' +
+ '<code class="hljs">' + parsed.value + '</code>' +
+ '</div>';
}
br() {
@@ -32,8 +139,20 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
link(href, title, text) {
let outHref = href;
+ let outText = text;
+ let prefix = '';
+ let suffix = '';
+
+ // some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets
+ // and we try our best to differentiate those from ones just wrapped in brackets when autolinking
+ if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) {
+ prefix = '(';
+ suffix = ')';
+ outText = text.substring(1, text.length - 1);
+ outHref = outHref.substring(1, outHref.length - 1);
+ }
- if (!(/^(mailto|https?|ftp)/.test(outHref))) {
+ if (!(/[a-z+.-]+:/i).test(outHref)) {
outHref = `http://${outHref}`;
}
@@ -48,30 +167,37 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
output += ' target="_blank">';
}
- output += text + '</a>';
+ output += outText + '</a>';
- return output;
+ return prefix + output + suffix;
}
paragraph(text) {
- let outText = text;
-
- if (!('emoticons' in this.options) || this.options.emoticon) {
- outText = TextFormatting.doFormatEmoticons(text);
- }
-
if (this.formattingOptions.singleline) {
- return `<p class="markdown__paragraph-inline">${outText}</p>`;
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
}
- return super.paragraph(outText);
+ return super.paragraph(text);
}
table(header, body) {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
- text(text) {
- return TextFormatting.doFormatText(text, this.formattingOptions);
+ text(txt) {
+ return TextFormatting.doFormatText(txt, this.formattingOptions);
}
}
+
+export function format(text, options) {
+ const markdownOptions = {
+ renderer: new MattermostMarkdownRenderer(null, options),
+ sanitize: true,
+ gfm: true
+ };
+
+ const tokens = marked.lexer(text, markdownOptions);
+
+ return new MattermostParser(markdownOptions).parse(tokens);
+}
+
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 5c2e68f1e..2de858a17 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -8,8 +8,6 @@ const Markdown = require('./markdown.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('./utils.jsx');
-const marked = require('marked');
-
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
// as part of the second parameter:
@@ -22,11 +20,8 @@ export function formatText(text, options = {}) {
let output;
if (!('markdown' in options) || options.markdown) {
- // the markdown renderer will call doFormatText as necessary so just call marked
- output = marked(text, {
- renderer: new Markdown.MattermostMarkdownRenderer(null, options),
- sanitize: true
- });
+ // the markdown renderer will call doFormatText as necessary
+ output = Markdown.format(text, options);
} else {
output = sanitizeHtml(text);
output = doFormatText(output, options);
@@ -47,8 +42,8 @@ export function doFormatText(text, options) {
const tokens = new Map();
// replace important words and phrases with tokens
- output = autolinkUrls(output, tokens);
output = autolinkAtMentions(output, tokens);
+ output = autolinkEmails(output, tokens);
output = autolinkHashtags(output, tokens);
if (!('emoticons' in options) || options.emoticon) {
@@ -78,6 +73,13 @@ export function doFormatEmoticons(text) {
return output;
}
+export function doFormatMentions(text) {
+ const tokens = new Map();
+ let output = autolinkAtMentions(text, tokens);
+ output = replaceTokens(output, tokens);
+ return output;
+}
+
export function sanitizeHtml(text) {
let output = text;
@@ -91,27 +93,21 @@ export function sanitizeHtml(text) {
return output;
}
-function autolinkUrls(text, tokens) {
- function replaceUrlWithToken(autolinker, match) {
+// Convert emails into tokens
+function autolinkEmails(text, tokens) {
+ function replaceEmailWithToken(autolinker, match) {
const linkText = match.getMatchedText();
let url = linkText;
if (match.getType() === 'email') {
url = `mailto:${url}`;
- } else if (!(/^(mailto|https?|ftp)/.test(url))) {
- url = `http://${url}`;
}
const index = tokens.size;
- const alias = `MM_LINK${index}`;
-
- var target = 'target="_blank"';
- if (url.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) {
- target = '';
- }
+ const alias = `MM_EMAIL${index}`;
tokens.set(alias, {
- value: `<a class="theme" ${target} href="${url}">${linkText}</a>`,
+ value: `<a class="theme" href="${url}">${linkText}</a>`,
originalText: linkText
});
@@ -120,38 +116,73 @@ function autolinkUrls(text, tokens) {
// we can't just use a static autolinker because we need to set replaceFn
const autolinker = new Autolinker({
- urls: true,
+ urls: false,
email: true,
phone: false,
twitter: false,
hashtag: false,
- replaceFn: replaceUrlWithToken
+ replaceFn: replaceEmailWithToken
});
return autolinker.link(text);
}
function autolinkAtMentions(text, tokens) {
- let output = text;
+ // Return true if provided character is punctuation
+ function isPunctuation(character) {
+ const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
+ return re.test(character);
+ }
+
+ // Test if provided text needs to be highlighted, special mention or current user
+ function mentionExists(u) {
+ return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u));
+ }
+
+ function addToken(username, mention, extraText) {
+ const index = tokens.size;
+ const alias = `MM_ATMENTION${index}`;
+
+ tokens.set(alias, {
+ value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`,
+ originalText: mention,
+ extraText
+ });
+ return alias;
+ }
function replaceAtMentionWithToken(fullMatch, prefix, mention, username) {
- const usernameLower = username.toLowerCase();
- if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) {
- const index = tokens.size;
- const alias = `MM_ATMENTION${index}`;
-
- tokens.set(alias, {
- value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`,
- originalText: mention
- });
+ let usernameLower = username.toLowerCase();
+ if (mentionExists(usernameLower)) {
+ // Exact match
+ const alias = addToken(usernameLower, mention, '');
return prefix + alias;
}
+ // Not an exact match, attempt to truncate any punctuation to see if we can find a user
+ const originalUsername = usernameLower;
+
+ for (let c = usernameLower.length; c > 0; c--) {
+ if (isPunctuation(usernameLower[c - 1])) {
+ usernameLower = usernameLower.substring(0, c - 1);
+
+ if (mentionExists(usernameLower)) {
+ const extraText = originalUsername.substr(c - 1);
+ const alias = addToken(usernameLower, '@' + usernameLower, extraText);
+ return prefix + alias;
+ }
+ } else {
+ // If the last character is not punctuation, no point in going any further
+ break;
+ }
+ }
+
return fullMatch;
}
- output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken);
+ let output = text;
+ output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
return output;
}
@@ -169,10 +200,9 @@ function highlightCurrentMentions(text, tokens) {
const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
- value: `<span class='mention-highlight'>${alias}</span>`,
+ value: `<span class='mention-highlight'>${alias}</span>` + token.extraText,
originalText: token.originalText
});
-
output = output.replace(alias, newAlias);
}
}
@@ -246,7 +276,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
var newTokens = new Map();
for (const [alias, token] of tokens) {
- if (token.originalText === searchTerm) {
+ if (token.originalText.indexOf(searchTerm.replace(/\*$/, '')) > -1) {
const index = tokens.size + newTokens.size;
const newAlias = `MM_SEARCHTERM${index}`;
@@ -276,7 +306,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
return prefix + alias;
}
- return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken);
+ return output.replace(new RegExp(`()(${searchTerm})`, 'gi'), replaceSearchTermWithToken);
}
function replaceTokens(text, tokens) {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 67a9d6983..c82bd1065 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -8,6 +8,7 @@ var PreferenceStore = require('../stores/preference_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
+var Client = require('./client.jsx');
var AsyncClient = require('./async_client.jsx');
var client = require('./client.jsx');
var Autolinker = require('autolinker');
@@ -20,6 +21,7 @@ export function isEmail(email) {
export function cleanUpUrlable(input) {
var cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
+ cleaned = cleaned.replace(/-{2,}/, '-');
cleaned = cleaned.replace(/^\-+/, '');
cleaned = cleaned.replace(/\-+$/, '');
return cleaned;
@@ -57,6 +59,20 @@ export function isTestDomain() {
return false;
}
+export function isChrome() {
+ if (navigator.userAgent.indexOf('Chrome') > -1) {
+ return true;
+ }
+ return false;
+}
+
+export function isSafari() {
+ if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
+ return true;
+ }
+ return false;
+}
+
export function isInRole(roles, inRole) {
var parts = roles.split(' ');
for (var i = 0; i < parts.length; i++) {
@@ -209,11 +225,15 @@ export function displayDateTime(ticks) {
}
interval = Math.floor(seconds / 60);
- if (interval > 1) {
+ if (interval >= 2) {
return interval + ' minutes ago';
}
- return '1 minute ago';
+ if (interval >= 1) {
+ return '1 minute ago';
+ }
+
+ return 'just now';
}
export function displayCommentDateTime(ticks) {
@@ -225,46 +245,62 @@ export function getTimestamp() {
return Date.now();
}
-function testUrlMatch(text) {
- var urlMatcher = new Autolinker.matchParser.MatchParser({
+// extracts links not styled by Markdown
+export function extractLinks(text) {
+ const urlMatcher = new Autolinker.matchParser.MatchParser({
urls: true,
emails: false,
twitter: false,
phone: false,
hashtag: false
});
- var result = [];
+ const links = [];
+ let replaceText = text;
+
+ // pull out the Markdown code blocks
+ const codeBlocks = [];
+ const splitText = replaceText.split('`'); // also handles ```
+ for (let i = 1; i < splitText.length; i += 2) {
+ if (splitText[i].trim() !== '') {
+ codeBlocks.push(splitText[i]);
+ }
+ }
+
function replaceFn(match) {
- var linkData = {};
- var matchText = match.getMatchedText();
+ let link = '';
+ const matchText = match.getMatchedText();
+ const tempText = replaceText;
+
+ const start = replaceText.indexOf(matchText);
+ const end = start + matchText.length;
+
+ replaceText = replaceText.substring(0, start) + replaceText.substring(end);
+
+ // if it's a Markdown link, just skip it
+ if (start > 1) {
+ if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') {
+ return;
+ }
+ }
+
+ // if it's in a Markdown code block, skip it
+ for (const i in codeBlocks) {
+ if (codeBlocks[i].indexOf(matchText) === 0) {
+ codeBlocks[i] = codeBlocks[i].replace(matchText, '');
+ return;
+ }
+ }
- linkData.text = matchText;
if (matchText.trim().indexOf('http') === 0) {
- linkData.link = matchText;
+ link = matchText;
} else {
- linkData.link = 'http://' + matchText;
+ link = 'http://' + matchText;
}
- result.push(linkData);
+ links.push(link);
}
urlMatcher.replace(text, replaceFn, this);
- return result;
-}
-
-export function extractLinks(text) {
- var repRegex = new RegExp('<br>', 'g');
- var matches = testUrlMatch(text.replace(repRegex, '\n'));
-
- if (!matches.length) {
- return {links: null, text: text};
- }
-
- var links = [];
- for (var i = 0; i < matches.length; i++) {
- links.push(matches[i].link);
- }
-
- return {links: links, text: text};
+ return {links, text};
}
export function escapeRegExp(string) {
@@ -402,6 +438,11 @@ export function toTitleCase(str) {
}
export function applyTheme(theme) {
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ }
+ updateCodeTheme(theme.codeTheme);
+
if (theme.sidebarBg) {
changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1);
}
@@ -432,6 +473,8 @@ export function applyTheme(theme) {
if (theme.sidebarTextActiveColor) {
changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1);
+ changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.05), 1);
+ changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.15), 1);
}
if (theme.sidebarHeaderBg) {
@@ -471,16 +514,16 @@ export function applyTheme(theme) {
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1);
- changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.sidebar--right, .dropdown-menu, .popover, .tip-overlay', 'background:' + theme.centerChannelBg, 1);
changeCss('.popover.bottom>.arrow:after', 'border-bottom-color:' + theme.centerChannelBg, 1);
- changeCss('.popover.right>.arrow:after', 'border-right-color:' + theme.centerChannelBg, 1);
+ changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1);
changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1);
- changeCss('.popover.top>.arrow:after', 'border-top-color:' + theme.centerChannelBg, 1);
+ changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1);
}
if (theme.centerChannelColor) {
- changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name, .tip-overlay', 'color:' + theme.centerChannelColor, 1);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.7), 1);
changeCss('.channel-header__links a:hover, .channel-header__links a:active', 'fill:' + theme.centerChannelColor, 2);
@@ -490,7 +533,7 @@ export function applyTheme(theme) {
changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3);
changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2);
changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1);
- changeCss('.post-body hr, .loading-screen .loading__content .round', 'background:' + theme.centerChannelColor, 1);
+ changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle, .tip-overlay .tutorial__circles .circle.active', 'background:' + theme.centerChannelColor, 1);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
@@ -539,7 +582,7 @@ export function applyTheme(theme) {
}
if (theme.buttonBg) {
- changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
+ changeCss('.btn.btn-primary, .tutorial__circles .circle.active', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
}
@@ -588,6 +631,27 @@ export function rgb2hex(rgbIn) {
return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
+export function updateCodeTheme(theme) {
+ const path = '/static/css/highlight/' + theme + '.css';
+ const $link = $('link.code_theme');
+ if (path !== $link.attr('href')) {
+ changeCss('code.hljs', 'visibility: hidden');
+ var xmlHTTP = new XMLHttpRequest();
+ xmlHTTP.open('GET', path, true);
+ xmlHTTP.onload = function onLoad() {
+ $link.attr('href', path);
+ if (isBrowserFirefox()) {
+ $link.one('load', () => {
+ changeCss('code.hljs', 'visibility: visible');
+ });
+ } else {
+ changeCss('code.hljs', 'visibility: visible');
+ }
+ };
+ xmlHTTP.send();
+ }
+}
+
export function placeCaretAtEnd(el) {
el.focus();
if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {
@@ -834,6 +898,23 @@ export function getDisplayName(user) {
return user.username;
}
+export function displayUsername(userId) {
+ const user = UserStore.getProfile(userId);
+ const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value;
+
+ let username = '';
+ if (nameFormat === 'nickname_full_name') {
+ username = user.nickname || getFullName(user);
+ } else if (nameFormat === 'full_name') {
+ username = getFullName(user);
+ }
+ if (!username.trim().length) {
+ username = user.username;
+ }
+
+ return username;
+}
+
//IE10 does not set window.location.origin automatically so this must be called instead when using it
export function getWindowLocationOrigin() {
var windowLocationOrigin = window.location.origin;
@@ -926,7 +1007,7 @@ export function isBrowserIE() {
}
export function isBrowserEdge() {
- return window.naviagtor && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
+ return window.navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1;
}
export function getDirectChannelName(id, otherId) {
@@ -982,3 +1063,44 @@ export function windowWidth() {
export function windowHeight() {
return $(window).height();
}
+
+export function openDirectChannelToUser(user, successCb, errorCb) {
+ const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
+ let channel = ChannelStore.getByName(channelName);
+
+ const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
+ AsyncClient.savePreferences([preference]);
+
+ if (channel) {
+ if ($.isFunction(successCb)) {
+ successCb(channel, true);
+ }
+ } else {
+ channel = {
+ name: channelName,
+ last_post_at: 0,
+ total_msg_count: 0,
+ type: 'D',
+ display_name: user.username,
+ teammate_id: user.id,
+ status: UserStore.getStatus(user.id)
+ };
+
+ Client.createDirectChannel(
+ channel,
+ user.id,
+ (data) => {
+ AsyncClient.getChannel(data.id);
+ if ($.isFunction(successCb)) {
+ successCb(data, false);
+ }
+ },
+ () => {
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
+ if ($.isFunction(errorCb)) {
+ errorCb();
+ }
+ }
+ );
+ }
+}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 14f1d9c2f..206d5bfca 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -2,9 +2,18 @@
> div {
height: 100%;
}
+
+ h3 {
+ font-weight: 600;
+ border-bottom: 1px solid #ddd;
+ padding-bottom: 0.5em;
+ margin: 1em 0;
+ }
+
.table {
background: #fff;
}
+
.sidebar--left {
&.sidebar--collapsable {
background: #333;
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 6399b8fd8..2830026c9 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -9,34 +9,45 @@ body {
position: relative;
height: 100%;
&.white {
- background: #fff;
- > .container-fluid {
- overflow: auto;
- }
- .inner__wrap {
- > .row.content {
- min-height: 100%;
- margin-bottom: -89px;
+ background: #fff;
+ > .container-fluid {
+ overflow: auto;
+ }
+ .inner__wrap {
+ > .row.content {
+ min-height: 100%;
+ margin-bottom: -89px;
+ }
}
- }
}
- .inner__wrap {
+}
+
+.inner__wrap {
height: 100%;
> .row.main {
- height: 100%;
- position: relative;
+ height: 100%;
+ position: relative;
}
- }
- > .container-fluid {
+}
+
+.container-fluid {
+ @include clearfix;
+ height: 100%;
+ position: relative;
+}
+
+.channel-view {
@include clearfix;
height: 100%;
position: relative;
- }
}
img {
max-width: 100%;
height: auto;
+ &.rounded {
+ @include border-radius(100%);
+ }
}
.popover {
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 8bf163f90..feb392234 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -1,3 +1,6 @@
+#channel-header {
+ padding: 3px 0;
+}
.row {
&.header {
position: relative;
@@ -215,7 +218,7 @@
color: #999;
cursor: pointer;
.fa {
- margin-left: 3px;
+ margin-left: 4px;
font-size: 16px;
}
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 1dcdbf348..9314b4980 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -113,11 +113,10 @@
text-align: center;
padding: 2em 1em;
.primary-message {
- font-size: 1.2em;
+ font-size: 1.25em;
}
.secondary-message {
- font-size: 1.25em;
- color: #888;
+ @include opacity(0.8);
margin: 1em 0 0;
}
}
@@ -377,7 +376,7 @@
@include opacity(0.8);
}
- .more-description {
+ .more-purpose {
@include opacity(0.7);
}
diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss
index 484e63c7c..430813e63 100644
--- a/web/sass-files/sass/partials/_popover.scss
+++ b/web/sass-files/sass/partials/_popover.scss
@@ -28,6 +28,39 @@
@include single-transition(opacity, 0.3s, ease-in);
font-size: em(13px);
+ &.autocomplete {
+ display: block;
+ .popover-content {
+ padding: 10px;
+ position: relative;
+ }
+ }
+
+ .search-autocomplete__item {
+ cursor: pointer;
+ padding: 6px 8px;
+ margin: 3px 0;
+ @include border-radius(2px);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background: rgba(black, 0.1);
+ }
+
+ &.selected {
+ background: rgba(black, 0.2);
+ }
+
+ .profile-img {
+ margin-top: -1px;
+ height: 16px;
+ margin-right: 6px;
+ width: 16px;
+ }
+ }
+
&.bottom > .arrow {
top: -18px;
border-width: 9px;
@@ -61,3 +94,23 @@
@include opacity(1);
}
}
+
+#member-list-popover {
+ max-width: initial;
+ .popover-content {
+ position: relative;
+ padding: 0;
+ width: 260px;
+ max-height: 350px;
+ .text-nowrap {
+ padding: 6px 10px;
+ width: 100%;
+ overflow: hidden;
+ line-height: 26px;
+ font-size: 13px;
+ }
+ .more-name {
+ margin-left: 6px;
+ }
+ }
+}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index f5fc1631f..e11f9b640 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -397,11 +397,18 @@ body.ios {
padding: 0;
}
p {
- margin: 0 0 5px;
+ margin: 0 0 1em;
+ line-height: 1.6em;
font-size: 0.97em;
white-space: pre-wrap;
}
+ span {
+ p:last-child {
+ margin-bottom: 0.5em;
+ }
+ }
+
.post-loading-gif {
height:10px;
width:10px;
@@ -434,7 +441,10 @@ body.ios {
&.post-profile-img__container {
float: left;
.post-profile-img {
+ width: 36px;
+ height: 36px;
margin-right: 10px;
+ vertical-align: inherit;
@include border-radius(50px);
}
}
@@ -466,6 +476,22 @@ body.ios {
white-space: nowrap;
cursor: pointer;
}
+ .post-body--code {
+ font-size: .97em;
+ position:relative;
+ .post-body--code__language {
+ position: absolute;
+ right: 0;
+ background: #fff;
+ cursor: default;
+ padding: 0.3em 0.5em 0.1em;
+ border-bottom-left-radius: 4px;
+ @include opacity(.3);
+ }
+ code {
+ white-space: pre;
+ }
+ }
}
.create-reply-form-wrap {
width: 100%;
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index 2cd5560ef..8f25f58cd 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -179,6 +179,15 @@
}
@media screen and (max-width: 1140px) {
+ .tip-overlay {
+ &.tip-overlay--chat {
+ margin: -10px 0 0 -10px;
+ .arrow {
+ right: 15px;
+ left: auto;
+ }
+ }
+ }
.inner__wrap {
&.move--left {
.file-overlay {
@@ -266,6 +275,26 @@
}
}
@media screen and (max-width: 768px) {
+ .form-control {
+ &.min-height {
+ min-height: 100px;
+ }
+ }
+ .gif-div {
+ max-width: 100%;
+ }
+ .tip-div {
+ left: 15px;
+ right: auto;
+ }
+ .tip-overlay {
+ &.tip-overlay--chat {
+ margin-left: 10px;
+ .arrow {
+ left: 32px;
+ }
+ }
+ }
.file-details__container {
display: block;
.file-details__preview {
@@ -379,7 +408,9 @@
}
.btn {
&.btn-primary {
- margin: 8px 0 0 -10px;
+ display: block;
+ margin: 10px 0 6px;
+ width: 100%;
float: none;
}
}
@@ -401,7 +432,13 @@
&.minimize-settings {
display: block;
.section-edit {
- text-align: left;
+ position: absolute;
+ top: 7px;
+ right: 0;
+ width: 50px;
+ .fa {
+ display: inline-block;
+ }
}
}
.no-padding--left {
@@ -431,7 +468,7 @@
}
}
.settings-table {
- .nav {
+ .nav, .nav.absolute {
position: relative;
top: auto;
width: 100%;
@@ -474,7 +511,6 @@
padding-bottom: 10px;
display: table;
width: 100%;
- table-layout: fixed;
.post-body__cell {
display: table-cell;
padding-left: 45px;
@@ -755,7 +791,7 @@
.channel__wrap & {
padding-top: 45px;
}
- .channel-header {
+ #channel-header {
display: none;
}
}
diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss
index ce3563885..73b69c866 100644
--- a/web/sass-files/sass/partials/_search.scss
+++ b/web/sass-files/sass/partials/_search.scss
@@ -1,5 +1,8 @@
+#channel-header .search-bar__container {
+ padding: 8px 8px 8px 0;
+}
.search-bar__container {
- padding: 8px 8px 8px 0;
+ padding: 12px 8px 12px 0;
}
.search__clear {
display: none;
@@ -108,44 +111,4 @@
.search-highlight {
background-color: #FFF2BB;
-}
-
-.search-autocomplete {
- background-color: #fff;
- border: $border-gray;
- line-height: 36px;
- overflow-x: hidden;
- overflow-y: scroll;
- position: absolute;
- text-align: left;
- width: 100%;
- z-index: 100;
- @extend %popover-box-shadow;
-}
-
-.search-autocomplete__channel {
- cursor: pointer;
- height: 36px;
- padding: 0px 6px;
-
- &.selected {
- background-color:rgba(51, 51, 51, 0.15);
- }
-}
-
-.search-autocomplete__user {
- cursor: pointer;
- height: 36px;
- padding: 0px;
-
- .profile-img {
- height: 32px;
- margin-right: 6px;
- width: 32px;
- @include border-radius(16px);
- }
-
- &.selected {
- background-color:rgba(51, 51, 51, 0.15);
- }
-}
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index c881f9073..96a6cf2ab 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -33,6 +33,33 @@
label {
font-weight: 600;
}
+ .no-padding--left {
+ padding-left: 0;
+ }
+ .padding-top {
+ padding-top: 7px;
+ &.x2 {
+ padding-top: 14px;
+ }
+ &.x3 {
+ padding-top: 21px;
+ }
+ }
+ .padding-bottom {
+ padding-bottom: 7px;
+ &.x2 {
+ padding-bottom: 14px;
+ }
+ &.x3 {
+ padding-bottom: 21px;
+ }
+ .control-label {
+ font-weight: 600;
+ &.text-left {
+ text-align: left;
+ }
+ }
+ }
.settings-table {
display: table;
table-layout: fixed;
@@ -44,7 +71,11 @@
.nav {
position: fixed;
top: 57px;
- width: 180px;
+ width: 179px;
+ &.absolute {
+ position: absolute;
+ top: 0;
+ }
}
.security-links {
margin-right: 20px;
@@ -66,6 +97,7 @@
padding: 1em 0;
margin-bottom: 0;
cursor: pointer;
+ position: relative;
@include clearfix;
&:hover {
background: #f9f9f9;
@@ -170,33 +202,6 @@
.has-error {
color: #a94442;
}
- .no-padding--left {
- padding-left: 0;
- }
- .padding-top {
- padding-top: 7px;
- &.x2 {
- padding-top: 14px;
- }
- &.x3 {
- padding-top: 21px;
- }
- }
- .padding-bottom {
- padding-bottom: 7px;
- &.x2 {
- padding-bottom: 14px;
- }
- &.x3 {
- padding-bottom: 21px;
- }
- .control-label {
- font-weight: 600;
- &.text-left {
- text-align: left;
- }
- }
- }
.file-status {
font-size: 13px;
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index c954b03d8..a4267294c 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -80,13 +80,18 @@
}
.sidebar--right__subheader {
font-size: 1em;
- text-transform: uppercase;
- color: #999;
- font-weight: 400;
- color: #888;
- height: 44px;
- line-height: 44px;
- padding: 0 1em;
+ padding: 1em 1em 0;
+ h4 {
+ font-size: 1em;
+ }
+ ul {
+ @include opacity(0.7);
+ padding: 10px 0 0 30px;
+ }
+ li {
+ font-size: 0.95em;
+ padding-bottom: 10px;
+ }
}
}
diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss
index 6d0256142..84f9892f4 100644
--- a/web/sass-files/sass/partials/_signup.scss
+++ b/web/sass-files/sass/partials/_signup.scss
@@ -313,6 +313,34 @@
}
+.signup-team-all {
+ width: 280px;
+ box-shadow: 3px 3px 1px #d5d5d5;
+ margin: 0px 0px 50px 5px;
+}
+
+.signup-team-dir {
+ background: #fafafa;
+ border-bottom: 1px solid #d5d5d5;
+}
+
+.signup-team-dir__group {
+ padding: 15px 10px 15px 10px;
+}
+
+.signup-team-dir__name {
+ line-height: 1.3 !important;
+ font-size: 1.5em !important;
+ font-weight: 300 !important;
+}
+
+.signup-team-dir__arrow {
+ float: right;
+ line-height: 1.3 !important;
+ font-size: 1.5em !important;
+ font-weight: 300 !important;
+}
+
.authorize-box {
margin: 100px auto;
width:500px;
diff --git a/web/sass-files/sass/partials/_statistics.scss b/web/sass-files/sass/partials/_statistics.scss
new file mode 100644
index 000000000..a2401a70f
--- /dev/null
+++ b/web/sass-files/sass/partials/_statistics.scss
@@ -0,0 +1,84 @@
+.team_statistics {
+ .total-count {
+ margin: 1em 0;
+ text-align: center;
+ background: #fff;
+ border: 1px solid #ddd;
+ width: 100%;
+ @include border-radius(3px);
+
+ .title {
+ font-weight: 400;
+ padding: 7px 10px;
+ border-bottom: 1px solid #ddd;
+ text-align: left;
+
+ .fa {
+ float: right;
+ margin: 3px 0 0;
+ color: #555;
+ font-size: 16px;
+ }
+
+ }
+
+ .content {
+ font-size: 30px;
+ font-weight: 600;
+ color: #555;
+ padding: 0.3em 0 0.35em;
+ overflow: auto;
+ }
+
+ }
+
+ .total-count--day {
+ width: 760px;
+ height: 275px;
+ border: 1px solid #ddd;
+ padding: 5px 10px 10px 10px;
+ margin: 10px 10px 10px 10px;
+ background: #fff;
+ clear: both;
+
+ > div {
+ font-size: 18px;
+ font-weight: 300;
+ }
+ }
+
+ .recent-active-users {
+
+ table {
+ width: 100%;
+ table-layout: fixed;
+ }
+
+ .content {
+ max-height: 400px;
+ overflow: auto;
+ padding: 0;
+ }
+
+ tr {
+ &:first-child {
+ td {
+ border-top: none;
+ }
+ }
+ td {
+ font-weight: 400;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 13px;
+ border-left: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+ padding: 5px 5px 6px;
+ @include clearfix;
+ &:first-child {
+ border-left: none;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss
new file mode 100644
index 000000000..70216aa97
--- /dev/null
+++ b/web/sass-files/sass/partials/_tutorial.scss
@@ -0,0 +1,189 @@
+.tip-backdrop {
+ background: rgba(black, 0.5);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+}
+
+.tip-overlay {
+ width: 350px;
+ max-width: 90%;
+ position:absolute;
+ background-color: #fff;
+ @include border-radius(3px);
+ padding: 20px;
+ z-index: 1000;
+
+ .arrow {
+ border-width: 10px;
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ }
+
+ &.tip-overlay--sidebar {
+ max-width: 75%;
+ margin: 50px 0 0 10px;
+ .arrow {
+ top: 80px;
+ left: -10px;
+ margin-top: -10px;
+ border-left-width: 0;
+ border-right-color: #fff;
+ }
+ }
+
+ &.tip-overlay--header {
+ margin: 10px 0 0 10px;
+ .arrow {
+ top: 15px;
+ left: -10px;
+ border-left-width: 0;
+ border-right-color: #fff;
+ }
+ }
+
+ &.tip-overlay--chat {
+ margin-top: -10px;
+ .arrow {
+ left: 50%;
+ margin-left: -10px;
+ border-bottom-width: 0;
+ border-top-color: #fff;
+ bottom: -10px;
+ }
+ }
+
+ h4 {
+ font-size: em(16px);
+ font-weight: 600;
+ margin: 5px 0 15px;
+ }
+
+ p {
+ font-size: 13px;
+ line-height: 1.6;
+
+ strong {
+ font-weight: 600;
+ }
+
+ }
+
+ .btn {
+ background: #cccccc;
+ color: #fff;
+ @include border-radius(3px);
+ border: none;
+ margin-bottom: 10px;
+
+ &:hover, &:active, &:focus {
+ color: #fff;
+ border: none;
+ background: #bbb;
+ }
+
+ }
+
+ .tip-opt {
+ font-size: 12px;
+
+ span {
+ @include opacity(0.5);
+ }
+
+ }
+
+ .tutorial__circles {
+ margin: 1.5em 0px -1.7em -4px;
+
+ .circle {
+ width: 7px;
+ height: 7px;
+ margin: 0 4px;
+ &.active {
+ background: #000;
+ @include opacity(0.4);
+ }
+
+ }
+
+ }
+
+}
+
+.tip-button {
+ z-index: 998;
+ right: -10px;
+ top: -10px;
+ position: relative;
+ cursor: pointer;
+}
+
+.tip-div {
+ position:absolute;
+ top:0px;
+ right:0px;
+
+ &.tip-overlay--header {
+ top: 20px;
+ }
+
+ &.tip-overlay--sidebar {
+ left: 0;
+ top: -9px;
+ }
+
+}
+
+.tutorial-steps__container {
+ text-align: center;
+ width: 100%;
+ display: table;
+ height: 100%;
+ .tutorial__content {
+ display: table-cell;
+ vertical-align: middle;
+ padding-bottom: 100px;
+ padding: 20px 40px 40px;
+ .tutorial__steps {
+ max-width: 310px;
+ min-height: 420px;
+ text-align: left;
+ display: inline-block;
+ }
+ }
+ h1 {
+ font-size: em(40px);
+ margin: -20px 0 30px;
+ font-weight: 600;
+ }
+ h3 {
+ font-size: em(24px);
+ margin-bottom: 30px;
+ font-weight: 600;
+ }
+}
+
+.tutorial__circles {
+ margin: 2em 0;
+ .circle {
+ width: 9px;
+ height: 9px;
+ @include border-radius(9px);
+ @include opacity(0.2);
+ background: #000;
+ display: inline-block;
+ margin: 0 5px;
+ &.active {
+ background: $primary-color;
+ @include opacity(1);
+ }
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index c614052da..5c83d5c5b 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -38,6 +38,8 @@
@import "partials/loading";
@import "partials/get-link";
@import "partials/markdown";
+@import "partials/tutorial";
+@import "partials/statistics";
// Responsive Css
@import "partials/responsive";
diff --git a/web/static/css/highlight b/web/static/css/highlight
new file mode 120000
index 000000000..c774cf397
--- /dev/null
+++ b/web/static/css/highlight
@@ -0,0 +1 @@
+../../react/node_modules/highlight.js/styles/ \ No newline at end of file
diff --git a/web/static/help/about.html b/web/static/help/about.html
index 4659aa9cc..6bdbee27e 100644
--- a/web/static/help/about.html
+++ b/web/static/help/about.html
@@ -5,7 +5,7 @@
</p>
<p>We built Mattermost to help teams focus on what matters most to them. It works for us, we hope it works for you too.
-Learn more, or download the source code from <a href=http://mattermost.com>http://mattermost.com</a>.</p>
+Learn more, or download the source code from <a href=http://mattermost.org>http://mattermost.org</a>.</p>
<h1>Join the community</h1>
<p>To take part in the community building Mattermost, please consider sharing comments, feature requests, votes, and contributions. If you like the project, please Tweet about us at <a href=https://twitter.com/mattermosthq>@mattermosthq</a>.</p>
diff --git a/web/static/images/themes/code_themes/github.png b/web/static/images/themes/code_themes/github.png
new file mode 100644
index 000000000..d0538d6c0
--- /dev/null
+++ b/web/static/images/themes/code_themes/github.png
Binary files differ
diff --git a/web/static/images/themes/code_themes/monokai.png b/web/static/images/themes/code_themes/monokai.png
new file mode 100644
index 000000000..8f92d2a18
--- /dev/null
+++ b/web/static/images/themes/code_themes/monokai.png
Binary files differ
diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized_dark.png
new file mode 100644
index 000000000..76055c678
--- /dev/null
+++ b/web/static/images/themes/code_themes/solarized_dark.png
Binary files differ
diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized_light.png
new file mode 100644
index 000000000..b9595c22d
--- /dev/null
+++ b/web/static/images/themes/code_themes/solarized_light.png
Binary files differ
diff --git a/web/static/images/tutorialTip.gif b/web/static/images/tutorialTip.gif
new file mode 100644
index 000000000..f185ff4b9
--- /dev/null
+++ b/web/static/images/tutorialTip.gif
Binary files differ
diff --git a/web/static/js/Chart.min.js b/web/static/js/Chart.min.js
new file mode 100644
index 000000000..3a0a2c873
--- /dev/null
+++ b/web/static/js/Chart.min.js
@@ -0,0 +1,11 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 1.0.2
+ *
+ * Copyright 2015 Nick Downie
+ * Released under the MIT license
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
+ */
+(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))
+},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file
diff --git a/web/static/js/babel-es6-polyfill.js b/web/static/js/babel-es6-polyfill.js
new file mode 100644
index 000000000..29161d1aa
--- /dev/null
+++ b/web/static/js/babel-es6-polyfill.js
@@ -0,0 +1,2591 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+(function (global){
+"use strict";
+
+if (global._babelPolyfill) {
+ throw new Error("only one instance of babel/polyfill is allowed");
+}
+global._babelPolyfill = true;
+
+require("./es6-shim");
+
+require("regenerator-babel/runtime");
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./es6-shim":2,"regenerator-babel/runtime":60}],2:[function(require,module,exports){
+require('core-js/es6');
+module.exports = require('core-js/modules/$').core;
+},{"core-js/es6":3,"core-js/modules/$":16}],3:[function(require,module,exports){
+require('../modules/es6.symbol');
+require('../modules/es6.object.assign');
+require('../modules/es6.object.is');
+require('../modules/es6.object.set-prototype-of');
+require('../modules/es6.object.to-string');
+require('../modules/es6.object.statics-accept-primitives');
+require('../modules/es6.function.name');
+require('../modules/es6.number.constructor');
+require('../modules/es6.number.statics');
+require('../modules/es6.math');
+require('../modules/es6.string.from-code-point');
+require('../modules/es6.string.raw');
+require('../modules/es6.string.iterator');
+require('../modules/es6.string.code-point-at');
+require('../modules/es6.string.ends-with');
+require('../modules/es6.string.includes');
+require('../modules/es6.string.repeat');
+require('../modules/es6.string.starts-with');
+require('../modules/es6.array.from');
+require('../modules/es6.array.of');
+require('../modules/es6.array.species');
+require('../modules/es6.array.iterator');
+require('../modules/es6.array.copy-within');
+require('../modules/es6.array.fill');
+require('../modules/es6.array.find');
+require('../modules/es6.array.find-index');
+require('../modules/es6.regexp');
+require('../modules/es6.promise');
+require('../modules/es6.map');
+require('../modules/es6.set');
+require('../modules/es6.weak-map');
+require('../modules/es6.weak-set');
+require('../modules/es6.reflect');
+module.exports = require('../modules/$').core;
+},{"../modules/$":16,"../modules/es6.array.copy-within":27,"../modules/es6.array.fill":28,"../modules/es6.array.find":30,"../modules/es6.array.find-index":29,"../modules/es6.array.from":31,"../modules/es6.array.iterator":32,"../modules/es6.array.of":33,"../modules/es6.array.species":34,"../modules/es6.function.name":35,"../modules/es6.map":36,"../modules/es6.math":37,"../modules/es6.number.constructor":38,"../modules/es6.number.statics":39,"../modules/es6.object.assign":40,"../modules/es6.object.is":41,"../modules/es6.object.set-prototype-of":42,"../modules/es6.object.statics-accept-primitives":43,"../modules/es6.object.to-string":44,"../modules/es6.promise":45,"../modules/es6.reflect":46,"../modules/es6.regexp":47,"../modules/es6.set":48,"../modules/es6.string.code-point-at":49,"../modules/es6.string.ends-with":50,"../modules/es6.string.from-code-point":51,"../modules/es6.string.includes":52,"../modules/es6.string.iterator":53,"../modules/es6.string.raw":54,"../modules/es6.string.repeat":55,"../modules/es6.string.starts-with":56,"../modules/es6.symbol":57,"../modules/es6.weak-map":58,"../modules/es6.weak-set":59}],4:[function(require,module,exports){
+'use strict';
+// 0 -> Array#forEach
+// 1 -> Array#map
+// 2 -> Array#filter
+// 3 -> Array#some
+// 4 -> Array#every
+// 5 -> Array#find
+// 6 -> Array#findIndex
+var $ = require('./$')
+ , ctx = require('./$.ctx');
+module.exports = function(TYPE){
+ var IS_MAP = TYPE == 1
+ , IS_FILTER = TYPE == 2
+ , IS_SOME = TYPE == 3
+ , IS_EVERY = TYPE == 4
+ , IS_FIND_INDEX = TYPE == 6
+ , NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
+ return function(callbackfn/*, that = undefined */){
+ var O = Object($.assertDefined(this))
+ , self = $.ES5Object(O)
+ , f = ctx(callbackfn, arguments[1], 3)
+ , length = $.toLength(self.length)
+ , index = 0
+ , result = IS_MAP ? Array(length) : IS_FILTER ? [] : undefined
+ , val, res;
+ for(;length > index; index++)if(NO_HOLES || index in self){
+ val = self[index];
+ res = f(val, index, O);
+ if(TYPE){
+ if(IS_MAP)result[index] = res; // map
+ else if(res)switch(TYPE){
+ case 3: return true; // some
+ case 5: return val; // find
+ case 6: return index; // findIndex
+ case 2: result.push(val); // filter
+ } else if(IS_EVERY)return false; // every
+ }
+ }
+ return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
+ };
+};
+},{"./$":16,"./$.ctx":11}],5:[function(require,module,exports){
+var $ = require('./$');
+function assert(condition, msg1, msg2){
+ if(!condition)throw TypeError(msg2 ? msg1 + msg2 : msg1);
+}
+assert.def = $.assertDefined;
+assert.fn = function(it){
+ if(!$.isFunction(it))throw TypeError(it + ' is not a function!');
+ return it;
+};
+assert.obj = function(it){
+ if(!$.isObject(it))throw TypeError(it + ' is not an object!');
+ return it;
+};
+assert.inst = function(it, Constructor, name){
+ if(!(it instanceof Constructor))throw TypeError(name + ": use the 'new' operator!");
+ return it;
+};
+module.exports = assert;
+},{"./$":16}],6:[function(require,module,exports){
+var $ = require('./$');
+// 19.1.2.1 Object.assign(target, source, ...)
+module.exports = Object.assign || function(target, source){ // eslint-disable-line no-unused-vars
+ var T = Object($.assertDefined(target))
+ , l = arguments.length
+ , i = 1;
+ while(l > i){
+ var S = $.ES5Object(arguments[i++])
+ , keys = $.getKeys(S)
+ , length = keys.length
+ , j = 0
+ , key;
+ while(length > j)T[key = keys[j++]] = S[key];
+ }
+ return T;
+};
+},{"./$":16}],7:[function(require,module,exports){
+var $ = require('./$')
+ , TAG = require('./$.wks')('toStringTag')
+ , toString = {}.toString;
+function cof(it){
+ return toString.call(it).slice(8, -1);
+}
+cof.classof = function(it){
+ var O, T;
+ return it == undefined ? it === undefined ? 'Undefined' : 'Null'
+ : typeof (T = (O = Object(it))[TAG]) == 'string' ? T : cof(O);
+};
+cof.set = function(it, tag, stat){
+ if(it && !$.has(it = stat ? it : it.prototype, TAG))$.hide(it, TAG, tag);
+};
+module.exports = cof;
+},{"./$":16,"./$.wks":26}],8:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , ctx = require('./$.ctx')
+ , safe = require('./$.uid').safe
+ , assert = require('./$.assert')
+ , $iter = require('./$.iter')
+ , has = $.has
+ , set = $.set
+ , isObject = $.isObject
+ , hide = $.hide
+ , step = $iter.step
+ , isFrozen = Object.isFrozen || $.core.Object.isFrozen
+ , ID = safe('id')
+ , O1 = safe('O1')
+ , LAST = safe('last')
+ , FIRST = safe('first')
+ , ITER = safe('iter')
+ , SIZE = $.DESC ? safe('size') : 'size'
+ , id = 0;
+
+function fastKey(it, create){
+ // return primitive with prefix
+ if(!isObject(it))return (typeof it == 'string' ? 'S' : 'P') + it;
+ // can't set id to frozen object
+ if(isFrozen(it))return 'F';
+ if(!has(it, ID)){
+ // not necessary to add id
+ if(!create)return 'E';
+ // add missing object id
+ hide(it, ID, ++id);
+ // return object id with prefix
+ } return 'O' + it[ID];
+}
+
+function getEntry(that, key){
+ // fast case
+ var index = fastKey(key), entry;
+ if(index != 'F')return that[O1][index];
+ // frozen object case
+ for(entry = that[FIRST]; entry; entry = entry.n){
+ if(entry.k == key)return entry;
+ }
+}
+
+module.exports = {
+ getConstructor: function(NAME, IS_MAP, ADDER){
+ function C(iterable){
+ var that = assert.inst(this, C, NAME);
+ set(that, O1, $.create(null));
+ set(that, SIZE, 0);
+ set(that, LAST, undefined);
+ set(that, FIRST, undefined);
+ if(iterable != undefined)$iter.forOf(iterable, IS_MAP, that[ADDER], that);
+ }
+ $.mix(C.prototype, {
+ // 23.1.3.1 Map.prototype.clear()
+ // 23.2.3.2 Set.prototype.clear()
+ clear: function(){
+ for(var that = this, data = that[O1], entry = that[FIRST]; entry; entry = entry.n){
+ entry.r = true;
+ if(entry.p)entry.p = entry.p.n = undefined;
+ delete data[entry.i];
+ }
+ that[FIRST] = that[LAST] = undefined;
+ that[SIZE] = 0;
+ },
+ // 23.1.3.3 Map.prototype.delete(key)
+ // 23.2.3.4 Set.prototype.delete(value)
+ 'delete': function(key){
+ var that = this
+ , entry = getEntry(that, key);
+ if(entry){
+ var next = entry.n
+ , prev = entry.p;
+ delete that[O1][entry.i];
+ entry.r = true;
+ if(prev)prev.n = next;
+ if(next)next.p = prev;
+ if(that[FIRST] == entry)that[FIRST] = next;
+ if(that[LAST] == entry)that[LAST] = prev;
+ that[SIZE]--;
+ } return !!entry;
+ },
+ // 23.2.3.6 Set.prototype.forEach(callbackfn, thisArg = undefined)
+ // 23.1.3.5 Map.prototype.forEach(callbackfn, thisArg = undefined)
+ forEach: function(callbackfn /*, that = undefined */){
+ var f = ctx(callbackfn, arguments[1], 3)
+ , entry;
+ while(entry = entry ? entry.n : this[FIRST]){
+ f(entry.v, entry.k, this);
+ // revert to the last existing entry
+ while(entry && entry.r)entry = entry.p;
+ }
+ },
+ // 23.1.3.7 Map.prototype.has(key)
+ // 23.2.3.7 Set.prototype.has(value)
+ has: function(key){
+ return !!getEntry(this, key);
+ }
+ });
+ if($.DESC)$.setDesc(C.prototype, 'size', {
+ get: function(){
+ return assert.def(this[SIZE]);
+ }
+ });
+ return C;
+ },
+ def: function(that, key, value){
+ var entry = getEntry(that, key)
+ , prev, index;
+ // change existing entry
+ if(entry){
+ entry.v = value;
+ // create new entry
+ } else {
+ that[LAST] = entry = {
+ i: index = fastKey(key, true), // <- index
+ k: key, // <- key
+ v: value, // <- value
+ p: prev = that[LAST], // <- previous entry
+ n: undefined, // <- next entry
+ r: false // <- removed
+ };
+ if(!that[FIRST])that[FIRST] = entry;
+ if(prev)prev.n = entry;
+ that[SIZE]++;
+ // add to index
+ if(index != 'F')that[O1][index] = entry;
+ } return that;
+ },
+ getEntry: getEntry,
+ getIterConstructor: function(){
+ return function(iterated, kind){
+ set(this, ITER, {o: iterated, k: kind});
+ };
+ },
+ next: function(){
+ var iter = this[ITER]
+ , kind = iter.k
+ , entry = iter.l;
+ // revert to the last existing entry
+ while(entry && entry.r)entry = entry.p;
+ // get next entry
+ if(!iter.o || !(iter.l = entry = entry ? entry.n : iter.o[FIRST])){
+ // or finish the iteration
+ iter.o = undefined;
+ return step(1);
+ }
+ // return step by kind
+ if(kind == 'key' )return step(0, entry.k);
+ if(kind == 'value')return step(0, entry.v);
+ return step(0, [entry.k, entry.v]);
+ }
+};
+},{"./$":16,"./$.assert":5,"./$.ctx":11,"./$.iter":15,"./$.uid":24}],9:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , safe = require('./$.uid').safe
+ , assert = require('./$.assert')
+ , forOf = require('./$.iter').forOf
+ , has = $.has
+ , isObject = $.isObject
+ , hide = $.hide
+ , isFrozen = Object.isFrozen || $.core.Object.isFrozen
+ , id = 0
+ , ID = safe('id')
+ , WEAK = safe('weak')
+ , LEAK = safe('leak')
+ , method = require('./$.array-methods')
+ , find = method(5)
+ , findIndex = method(6);
+function findFrozen(store, key){
+ return find.call(store.array, function(it){
+ return it[0] === key;
+ });
+}
+// fallback for frozen keys
+function leakStore(that){
+ return that[LEAK] || hide(that, LEAK, {
+ array: [],
+ get: function(key){
+ var entry = findFrozen(this, key);
+ if(entry)return entry[1];
+ },
+ has: function(key){
+ return !!findFrozen(this, key);
+ },
+ set: function(key, value){
+ var entry = findFrozen(this, key);
+ if(entry)entry[1] = value;
+ else this.array.push([key, value]);
+ },
+ 'delete': function(key){
+ var index = findIndex.call(this.array, function(it){
+ return it[0] === key;
+ });
+ if(~index)this.array.splice(index, 1);
+ return !!~index;
+ }
+ })[LEAK];
+}
+
+module.exports = {
+ getConstructor: function(NAME, IS_MAP, ADDER){
+ function C(iterable){
+ $.set(assert.inst(this, C, NAME), ID, id++);
+ if(iterable != undefined)forOf(iterable, IS_MAP, this[ADDER], this);
+ }
+ $.mix(C.prototype, {
+ // 23.3.3.2 WeakMap.prototype.delete(key)
+ // 23.4.3.3 WeakSet.prototype.delete(value)
+ 'delete': function(key){
+ if(!isObject(key))return false;
+ if(isFrozen(key))return leakStore(this)['delete'](key);
+ return has(key, WEAK) && has(key[WEAK], this[ID]) && delete key[WEAK][this[ID]];
+ },
+ // 23.3.3.4 WeakMap.prototype.has(key)
+ // 23.4.3.4 WeakSet.prototype.has(value)
+ has: function(key){
+ if(!isObject(key))return false;
+ if(isFrozen(key))return leakStore(this).has(key);
+ return has(key, WEAK) && has(key[WEAK], this[ID]);
+ }
+ });
+ return C;
+ },
+ def: function(that, key, value){
+ if(isFrozen(assert.obj(key))){
+ leakStore(that).set(key, value);
+ } else {
+ has(key, WEAK) || hide(key, WEAK, {});
+ key[WEAK][that[ID]] = value;
+ } return that;
+ },
+ leakStore: leakStore,
+ WEAK: WEAK,
+ ID: ID
+};
+},{"./$":16,"./$.array-methods":4,"./$.assert":5,"./$.iter":15,"./$.uid":24}],10:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , $def = require('./$.def')
+ , $iter = require('./$.iter')
+ , assertInstance = require('./$.assert').inst;
+
+module.exports = function(NAME, methods, common, IS_MAP, isWeak){
+ var Base = $.g[NAME]
+ , C = Base
+ , ADDER = IS_MAP ? 'set' : 'add'
+ , proto = C && C.prototype
+ , O = {};
+ function fixMethod(KEY, CHAIN){
+ var method = proto[KEY];
+ if($.FW)proto[KEY] = function(a, b){
+ var result = method.call(this, a === 0 ? 0 : a, b);
+ return CHAIN ? this : result;
+ };
+ }
+ if(!$.isFunction(C) || !(isWeak || !$iter.BUGGY && proto.forEach && proto.entries)){
+ // create collection constructor
+ C = common.getConstructor(NAME, IS_MAP, ADDER);
+ $.mix(C.prototype, methods);
+ } else {
+ var inst = new C
+ , chain = inst[ADDER](isWeak ? {} : -0, 1)
+ , buggyZero;
+ // wrap for init collections from iterable
+ if($iter.fail(function(iter){
+ new C(iter); // eslint-disable-line no-new
+ }) || $iter.DANGER_CLOSING){
+ C = function(iterable){
+ assertInstance(this, C, NAME);
+ var that = new Base;
+ if(iterable != undefined)$iter.forOf(iterable, IS_MAP, that[ADDER], that);
+ return that;
+ };
+ C.prototype = proto;
+ if($.FW)proto.constructor = C;
+ }
+ isWeak || inst.forEach(function(val, key){
+ buggyZero = 1 / key === -Infinity;
+ });
+ // fix converting -0 key to +0
+ if(buggyZero){
+ fixMethod('delete');
+ fixMethod('has');
+ IS_MAP && fixMethod('get');
+ }
+ // + fix .add & .set for chaining
+ if(buggyZero || chain !== inst)fixMethod(ADDER, true);
+ }
+
+ require('./$.cof').set(C, NAME);
+ require('./$.species')(C);
+
+ O[NAME] = C;
+ $def($def.G + $def.W + $def.F * (C != Base), O);
+
+ // add .keys, .values, .entries, [@@iterator]
+ // 23.1.3.4, 23.1.3.8, 23.1.3.11, 23.1.3.12, 23.2.3.5, 23.2.3.8, 23.2.3.10, 23.2.3.11
+ if(!isWeak)$iter.std(
+ C, NAME,
+ common.getIterConstructor(), common.next,
+ IS_MAP ? 'key+value' : 'value' , !IS_MAP, true
+ );
+
+ return C;
+};
+},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.def":12,"./$.iter":15,"./$.species":21}],11:[function(require,module,exports){
+// Optional / simple context binding
+var assertFunction = require('./$.assert').fn;
+module.exports = function(fn, that, length){
+ assertFunction(fn);
+ if(~length && that === undefined)return fn;
+ switch(length){
+ case 1: return function(a){
+ return fn.call(that, a);
+ };
+ case 2: return function(a, b){
+ return fn.call(that, a, b);
+ };
+ case 3: return function(a, b, c){
+ return fn.call(that, a, b, c);
+ };
+ } return function(/* ...args */){
+ return fn.apply(that, arguments);
+ };
+};
+},{"./$.assert":5}],12:[function(require,module,exports){
+var $ = require('./$')
+ , global = $.g
+ , core = $.core
+ , isFunction = $.isFunction;
+function ctx(fn, that){
+ return function(){
+ return fn.apply(that, arguments);
+ };
+}
+global.core = core;
+// type bitmap
+$def.F = 1; // forced
+$def.G = 2; // global
+$def.S = 4; // static
+$def.P = 8; // proto
+$def.B = 16; // bind
+$def.W = 32; // wrap
+function $def(type, name, source){
+ var key, own, out, exp
+ , isGlobal = type & $def.G
+ , target = isGlobal ? global : type & $def.S
+ ? global[name] : (global[name] || {}).prototype
+ , exports = isGlobal ? core : core[name] || (core[name] = {});
+ if(isGlobal)source = name;
+ for(key in source){
+ // contains in native
+ own = !(type & $def.F) && target && key in target;
+ // export native or passed
+ out = (own ? target : source)[key];
+ // bind timers to global for call from export context
+ if(type & $def.B && own)exp = ctx(out, global);
+ else exp = type & $def.P && isFunction(out) ? ctx(Function.call, out) : out;
+ // extend global
+ if(target && !own){
+ if(isGlobal)target[key] = out;
+ else delete target[key] && $.hide(target, key, out);
+ }
+ // export
+ if(exports[key] != out)$.hide(exports, key, exp);
+ }
+}
+module.exports = $def;
+},{"./$":16}],13:[function(require,module,exports){
+module.exports = function($){
+ $.FW = true;
+ $.path = $.g;
+ return $;
+};
+},{}],14:[function(require,module,exports){
+// Fast apply
+// http://jsperf.lnkit.com/fast-apply/5
+module.exports = function(fn, args, that){
+ var un = that === undefined;
+ switch(args.length){
+ case 0: return un ? fn()
+ : fn.call(that);
+ case 1: return un ? fn(args[0])
+ : fn.call(that, args[0]);
+ case 2: return un ? fn(args[0], args[1])
+ : fn.call(that, args[0], args[1]);
+ case 3: return un ? fn(args[0], args[1], args[2])
+ : fn.call(that, args[0], args[1], args[2]);
+ case 4: return un ? fn(args[0], args[1], args[2], args[3])
+ : fn.call(that, args[0], args[1], args[2], args[3]);
+ case 5: return un ? fn(args[0], args[1], args[2], args[3], args[4])
+ : fn.call(that, args[0], args[1], args[2], args[3], args[4]);
+ } return fn.apply(that, args);
+};
+},{}],15:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , ctx = require('./$.ctx')
+ , cof = require('./$.cof')
+ , $def = require('./$.def')
+ , assertObject = require('./$.assert').obj
+ , SYMBOL_ITERATOR = require('./$.wks')('iterator')
+ , FF_ITERATOR = '@@iterator'
+ , Iterators = {}
+ , IteratorPrototype = {};
+// Safari has byggy iterators w/o `next`
+var BUGGY = 'keys' in [] && !('next' in [].keys());
+// 25.1.2.1.1 %IteratorPrototype%[@@iterator]()
+setIterator(IteratorPrototype, $.that);
+function setIterator(O, value){
+ $.hide(O, SYMBOL_ITERATOR, value);
+ // Add iterator for FF iterator protocol
+ if(FF_ITERATOR in [])$.hide(O, FF_ITERATOR, value);
+}
+function defineIterator(Constructor, NAME, value, DEFAULT){
+ var proto = Constructor.prototype
+ , iter = proto[SYMBOL_ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT] || value;
+ // Define iterator
+ if($.FW)setIterator(proto, iter);
+ if(iter !== value){
+ var iterProto = $.getProto(iter.call(new Constructor));
+ // Set @@toStringTag to native iterators
+ cof.set(iterProto, NAME + ' Iterator', true);
+ // FF fix
+ if($.FW)$.has(proto, FF_ITERATOR) && setIterator(iterProto, $.that);
+ }
+ // Plug for library
+ Iterators[NAME] = iter;
+ // FF & v8 fix
+ Iterators[NAME + ' Iterator'] = $.that;
+ return iter;
+}
+function getIterator(it){
+ var Symbol = $.g.Symbol
+ , ext = it[Symbol && Symbol.iterator || FF_ITERATOR]
+ , getIter = ext || it[SYMBOL_ITERATOR] || Iterators[cof.classof(it)];
+ return assertObject(getIter.call(it));
+}
+function closeIterator(iterator){
+ var ret = iterator['return'];
+ if(ret !== undefined)assertObject(ret.call(iterator));
+}
+function stepCall(iterator, fn, value, entries){
+ try {
+ return entries ? fn(assertObject(value)[0], value[1]) : fn(value);
+ } catch(e){
+ closeIterator(iterator);
+ throw e;
+ }
+}
+var DANGER_CLOSING = true;
+!function(){
+ try {
+ var iter = [1].keys();
+ iter['return'] = function(){ DANGER_CLOSING = false; };
+ Array.from(iter, function(){ throw 2; });
+ } catch(e){ /* empty */ }
+}();
+var $iter = module.exports = {
+ BUGGY: BUGGY,
+ DANGER_CLOSING: DANGER_CLOSING,
+ fail: function(exec){
+ var fail = true;
+ try {
+ var arr = [[{}, 1]]
+ , iter = arr[SYMBOL_ITERATOR]()
+ , next = iter.next;
+ iter.next = function(){
+ fail = false;
+ return next.call(this);
+ };
+ arr[SYMBOL_ITERATOR] = function(){
+ return iter;
+ };
+ exec(arr);
+ } catch(e){ /* empty */ }
+ return fail;
+ },
+ Iterators: Iterators,
+ prototype: IteratorPrototype,
+ step: function(done, value){
+ return {value: value, done: !!done};
+ },
+ stepCall: stepCall,
+ close: closeIterator,
+ is: function(it){
+ var O = Object(it)
+ , Symbol = $.g.Symbol
+ , SYM = Symbol && Symbol.iterator || FF_ITERATOR;
+ return SYM in O || SYMBOL_ITERATOR in O || $.has(Iterators, cof.classof(O));
+ },
+ get: getIterator,
+ set: setIterator,
+ create: function(Constructor, NAME, next, proto){
+ Constructor.prototype = $.create(proto || $iter.prototype, {next: $.desc(1, next)});
+ cof.set(Constructor, NAME + ' Iterator');
+ },
+ define: defineIterator,
+ std: function(Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCE){
+ function createIter(kind){
+ return function(){
+ return new Constructor(this, kind);
+ };
+ }
+ $iter.create(Constructor, NAME, next);
+ var entries = createIter('key+value')
+ , values = createIter('value')
+ , proto = Base.prototype
+ , methods, key;
+ if(DEFAULT == 'value')values = defineIterator(Base, NAME, values, 'values');
+ else entries = defineIterator(Base, NAME, entries, 'entries');
+ if(DEFAULT){
+ methods = {
+ entries: entries,
+ keys: IS_SET ? values : createIter('key'),
+ values: values
+ };
+ $def($def.P + $def.F * BUGGY, NAME, methods);
+ if(FORCE)for(key in methods){
+ if(!(key in proto))$.hide(proto, key, methods[key]);
+ }
+ }
+ },
+ forOf: function(iterable, entries, fn, that){
+ var iterator = getIterator(iterable)
+ , f = ctx(fn, that, entries ? 2 : 1)
+ , step;
+ while(!(step = iterator.next()).done){
+ if(stepCall(iterator, f, step.value, entries) === false){
+ return closeIterator(iterator);
+ }
+ }
+ }
+};
+},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.ctx":11,"./$.def":12,"./$.wks":26}],16:[function(require,module,exports){
+'use strict';
+var global = typeof self != 'undefined' ? self : Function('return this')()
+ , core = {}
+ , defineProperty = Object.defineProperty
+ , hasOwnProperty = {}.hasOwnProperty
+ , ceil = Math.ceil
+ , floor = Math.floor
+ , max = Math.max
+ , min = Math.min;
+// The engine works fine with descriptors? Thank's IE8 for his funny defineProperty.
+var DESC = !!function(){
+ try {
+ return defineProperty({}, 'a', {get: function(){ return 2; }}).a == 2;
+ } catch(e){ /* empty */ }
+}();
+var hide = createDefiner(1);
+// 7.1.4 ToInteger
+function toInteger(it){
+ return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it);
+}
+function desc(bitmap, value){
+ return {
+ enumerable : !(bitmap & 1),
+ configurable: !(bitmap & 2),
+ writable : !(bitmap & 4),
+ value : value
+ };
+}
+function simpleSet(object, key, value){
+ object[key] = value;
+ return object;
+}
+function createDefiner(bitmap){
+ return DESC ? function(object, key, value){
+ return $.setDesc(object, key, desc(bitmap, value)); // eslint-disable-line no-use-before-define
+ } : simpleSet;
+}
+
+function isObject(it){
+ return it !== null && (typeof it == 'object' || typeof it == 'function');
+}
+function isFunction(it){
+ return typeof it == 'function';
+}
+function assertDefined(it){
+ if(it == undefined)throw TypeError("Can't call method on " + it);
+ return it;
+}
+
+var $ = module.exports = require('./$.fw')({
+ g: global,
+ core: core,
+ html: global.document && document.documentElement,
+ // http://jsperf.com/core-js-isobject
+ isObject: isObject,
+ isFunction: isFunction,
+ it: function(it){
+ return it;
+ },
+ that: function(){
+ return this;
+ },
+ // 7.1.4 ToInteger
+ toInteger: toInteger,
+ // 7.1.15 ToLength
+ toLength: function(it){
+ return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991
+ },
+ toIndex: function(index, length){
+ index = toInteger(index);
+ return index < 0 ? max(index + length, 0) : min(index, length);
+ },
+ has: function(it, key){
+ return hasOwnProperty.call(it, key);
+ },
+ create: Object.create,
+ getProto: Object.getPrototypeOf,
+ DESC: DESC,
+ desc: desc,
+ getDesc: Object.getOwnPropertyDescriptor,
+ setDesc: defineProperty,
+ getKeys: Object.keys,
+ getNames: Object.getOwnPropertyNames,
+ getSymbols: Object.getOwnPropertySymbols,
+ // Dummy, fix for not array-like ES3 string in es5 module
+ assertDefined: assertDefined,
+ ES5Object: Object,
+ toObject: function(it){
+ return $.ES5Object(assertDefined(it));
+ },
+ hide: hide,
+ def: createDefiner(0),
+ set: global.Symbol ? simpleSet : hide,
+ mix: function(target, src){
+ for(var key in src)hide(target, key, src[key]);
+ return target;
+ },
+ each: [].forEach
+});
+if(typeof __e != 'undefined')__e = core;
+if(typeof __g != 'undefined')__g = global;
+},{"./$.fw":13}],17:[function(require,module,exports){
+var $ = require('./$');
+module.exports = function(object, el){
+ var O = $.toObject(object)
+ , keys = $.getKeys(O)
+ , length = keys.length
+ , index = 0
+ , key;
+ while(length > index)if(O[key = keys[index++]] === el)return key;
+};
+},{"./$":16}],18:[function(require,module,exports){
+var $ = require('./$')
+ , assertObject = require('./$.assert').obj;
+module.exports = function(it){
+ assertObject(it);
+ return $.getSymbols ? $.getNames(it).concat($.getSymbols(it)) : $.getNames(it);
+};
+},{"./$":16,"./$.assert":5}],19:[function(require,module,exports){
+'use strict';
+module.exports = function(regExp, replace, isStatic){
+ var replacer = replace === Object(replace) ? function(part){
+ return replace[part];
+ } : replace;
+ return function(it){
+ return String(isStatic ? it : this).replace(regExp, replacer);
+ };
+};
+},{}],20:[function(require,module,exports){
+// Works with __proto__ only. Old v8 can't works with null proto objects.
+/*eslint-disable no-proto */
+var $ = require('./$')
+ , assert = require('./$.assert');
+module.exports = Object.setPrototypeOf || ('__proto__' in {} // eslint-disable-line
+ ? function(buggy, set){
+ try {
+ set = require('./$.ctx')(Function.call, $.getDesc(Object.prototype, '__proto__').set, 2);
+ set({}, []);
+ } catch(e){ buggy = true; }
+ return function(O, proto){
+ assert.obj(O);
+ assert(proto === null || $.isObject(proto), proto, ": can't set as prototype!");
+ if(buggy)O.__proto__ = proto;
+ else set(O, proto);
+ return O;
+ };
+ }()
+ : undefined);
+},{"./$":16,"./$.assert":5,"./$.ctx":11}],21:[function(require,module,exports){
+var $ = require('./$');
+module.exports = function(C){
+ if($.DESC && $.FW)$.setDesc(C, require('./$.wks')('species'), {
+ configurable: true,
+ get: $.that
+ });
+};
+},{"./$":16,"./$.wks":26}],22:[function(require,module,exports){
+'use strict';
+// true -> String#at
+// false -> String#codePointAt
+var $ = require('./$');
+module.exports = function(TO_STRING){
+ return function(pos){
+ var s = String($.assertDefined(this))
+ , i = $.toInteger(pos)
+ , l = s.length
+ , a, b;
+ if(i < 0 || i >= l)return TO_STRING ? '' : undefined;
+ a = s.charCodeAt(i);
+ return a < 0xd800 || a > 0xdbff || i + 1 === l
+ || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff
+ ? TO_STRING ? s.charAt(i) : a
+ : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000;
+ };
+};
+},{"./$":16}],23:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , ctx = require('./$.ctx')
+ , cof = require('./$.cof')
+ , invoke = require('./$.invoke')
+ , global = $.g
+ , isFunction = $.isFunction
+ , setTask = global.setImmediate
+ , clearTask = global.clearImmediate
+ , postMessage = global.postMessage
+ , addEventListener = global.addEventListener
+ , MessageChannel = global.MessageChannel
+ , counter = 0
+ , queue = {}
+ , ONREADYSTATECHANGE = 'onreadystatechange'
+ , defer, channel, port;
+function run(){
+ var id = +this;
+ if($.has(queue, id)){
+ var fn = queue[id];
+ delete queue[id];
+ fn();
+ }
+}
+function listner(event){
+ run.call(event.data);
+}
+// Node.js 0.9+ & IE10+ has setImmediate, otherwise:
+if(!isFunction(setTask) || !isFunction(clearTask)){
+ setTask = function(fn){
+ var args = [], i = 1;
+ while(arguments.length > i)args.push(arguments[i++]);
+ queue[++counter] = function(){
+ invoke(isFunction(fn) ? fn : Function(fn), args);
+ };
+ defer(counter);
+ return counter;
+ };
+ clearTask = function(id){
+ delete queue[id];
+ };
+ // Node.js 0.8-
+ if(cof(global.process) == 'process'){
+ defer = function(id){
+ global.process.nextTick(ctx(run, id, 1));
+ };
+ // Modern browsers, skip implementation for WebWorkers
+ // IE8 has postMessage, but it's sync & typeof its postMessage is object
+ } else if(addEventListener && isFunction(postMessage) && !$.g.importScripts){
+ defer = function(id){
+ postMessage(id, '*');
+ };
+ addEventListener('message', listner, false);
+ // WebWorkers
+ } else if(isFunction(MessageChannel)){
+ channel = new MessageChannel;
+ port = channel.port2;
+ channel.port1.onmessage = listner;
+ defer = ctx(port.postMessage, port, 1);
+ // IE8-
+ } else if($.g.document && ONREADYSTATECHANGE in document.createElement('script')){
+ defer = function(id){
+ $.html.appendChild(document.createElement('script'))[ONREADYSTATECHANGE] = function(){
+ $.html.removeChild(this);
+ run.call(id);
+ };
+ };
+ // Rest old browsers
+ } else {
+ defer = function(id){
+ setTimeout(ctx(run, id, 1), 0);
+ };
+ }
+}
+module.exports = {
+ set: setTask,
+ clear: clearTask
+};
+},{"./$":16,"./$.cof":7,"./$.ctx":11,"./$.invoke":14}],24:[function(require,module,exports){
+var sid = 0;
+function uid(key){
+ return 'Symbol(' + key + ')_' + (++sid + Math.random()).toString(36);
+}
+uid.safe = require('./$').g.Symbol || uid;
+module.exports = uid;
+},{"./$":16}],25:[function(require,module,exports){
+// 22.1.3.31 Array.prototype[@@unscopables]
+var $ = require('./$')
+ , UNSCOPABLES = require('./$.wks')('unscopables');
+if($.FW && !(UNSCOPABLES in []))$.hide(Array.prototype, UNSCOPABLES, {});
+module.exports = function(key){
+ if($.FW)[][UNSCOPABLES][key] = true;
+};
+},{"./$":16,"./$.wks":26}],26:[function(require,module,exports){
+var global = require('./$').g
+ , store = {};
+module.exports = function(name){
+ return store[name] || (store[name] =
+ global.Symbol && global.Symbol[name] || require('./$.uid').safe('Symbol.' + name));
+};
+},{"./$":16,"./$.uid":24}],27:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , $def = require('./$.def')
+ , toIndex = $.toIndex;
+$def($def.P, 'Array', {
+ // 22.1.3.3 Array.prototype.copyWithin(target, start, end = this.length)
+ copyWithin: function(target/* = 0 */, start /* = 0, end = @length */){
+ var O = Object($.assertDefined(this))
+ , len = $.toLength(O.length)
+ , to = toIndex(target, len)
+ , from = toIndex(start, len)
+ , end = arguments[2]
+ , fin = end === undefined ? len : toIndex(end, len)
+ , count = Math.min(fin - from, len - to)
+ , inc = 1;
+ if(from < to && to < from + count){
+ inc = -1;
+ from = from + count - 1;
+ to = to + count - 1;
+ }
+ while(count-- > 0){
+ if(from in O)O[to] = O[from];
+ else delete O[to];
+ to += inc;
+ from += inc;
+ } return O;
+ }
+});
+require('./$.unscope')('copyWithin');
+},{"./$":16,"./$.def":12,"./$.unscope":25}],28:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , $def = require('./$.def')
+ , toIndex = $.toIndex;
+$def($def.P, 'Array', {
+ // 22.1.3.6 Array.prototype.fill(value, start = 0, end = this.length)
+ fill: function(value /*, start = 0, end = @length */){
+ var O = Object($.assertDefined(this))
+ , length = $.toLength(O.length)
+ , index = toIndex(arguments[1], length)
+ , end = arguments[2]
+ , endPos = end === undefined ? length : toIndex(end, length);
+ while(endPos > index)O[index++] = value;
+ return O;
+ }
+});
+require('./$.unscope')('fill');
+},{"./$":16,"./$.def":12,"./$.unscope":25}],29:[function(require,module,exports){
+var $def = require('./$.def');
+$def($def.P, 'Array', {
+ // 22.1.3.9 Array.prototype.findIndex(predicate, thisArg = undefined)
+ findIndex: require('./$.array-methods')(6)
+});
+require('./$.unscope')('findIndex');
+},{"./$.array-methods":4,"./$.def":12,"./$.unscope":25}],30:[function(require,module,exports){
+var $def = require('./$.def');
+$def($def.P, 'Array', {
+ // 22.1.3.8 Array.prototype.find(predicate, thisArg = undefined)
+ find: require('./$.array-methods')(5)
+});
+require('./$.unscope')('find');
+},{"./$.array-methods":4,"./$.def":12,"./$.unscope":25}],31:[function(require,module,exports){
+var $ = require('./$')
+ , ctx = require('./$.ctx')
+ , $def = require('./$.def')
+ , $iter = require('./$.iter')
+ , stepCall = $iter.stepCall;
+$def($def.S + $def.F * $iter.DANGER_CLOSING, 'Array', {
+ // 22.1.2.1 Array.from(arrayLike, mapfn = undefined, thisArg = undefined)
+ from: function(arrayLike/*, mapfn = undefined, thisArg = undefined*/){
+ var O = Object($.assertDefined(arrayLike))
+ , mapfn = arguments[1]
+ , mapping = mapfn !== undefined
+ , f = mapping ? ctx(mapfn, arguments[2], 2) : undefined
+ , index = 0
+ , length, result, step, iterator;
+ if($iter.is(O)){
+ iterator = $iter.get(O);
+ // strange IE quirks mode bug -> use typeof instead of isFunction
+ result = new (typeof this == 'function' ? this : Array);
+ for(; !(step = iterator.next()).done; index++){
+ result[index] = mapping ? stepCall(iterator, f, [step.value, index], true) : step.value;
+ }
+ } else {
+ // strange IE quirks mode bug -> use typeof instead of isFunction
+ result = new (typeof this == 'function' ? this : Array)(length = $.toLength(O.length));
+ for(; length > index; index++){
+ result[index] = mapping ? f(O[index], index) : O[index];
+ }
+ }
+ result.length = index;
+ return result;
+ }
+});
+},{"./$":16,"./$.ctx":11,"./$.def":12,"./$.iter":15}],32:[function(require,module,exports){
+var $ = require('./$')
+ , setUnscope = require('./$.unscope')
+ , ITER = require('./$.uid').safe('iter')
+ , $iter = require('./$.iter')
+ , step = $iter.step
+ , Iterators = $iter.Iterators;
+
+// 22.1.3.4 Array.prototype.entries()
+// 22.1.3.13 Array.prototype.keys()
+// 22.1.3.29 Array.prototype.values()
+// 22.1.3.30 Array.prototype[@@iterator]()
+$iter.std(Array, 'Array', function(iterated, kind){
+ $.set(this, ITER, {o: $.toObject(iterated), i: 0, k: kind});
+// 22.1.5.2.1 %ArrayIteratorPrototype%.next()
+}, function(){
+ var iter = this[ITER]
+ , O = iter.o
+ , kind = iter.k
+ , index = iter.i++;
+ if(!O || index >= O.length){
+ iter.o = undefined;
+ return step(1);
+ }
+ if(kind == 'key' )return step(0, index);
+ if(kind == 'value')return step(0, O[index]);
+ return step(0, [index, O[index]]);
+}, 'value');
+
+// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7)
+Iterators.Arguments = Iterators.Array;
+
+setUnscope('keys');
+setUnscope('values');
+setUnscope('entries');
+},{"./$":16,"./$.iter":15,"./$.uid":24,"./$.unscope":25}],33:[function(require,module,exports){
+var $def = require('./$.def');
+$def($def.S, 'Array', {
+ // 22.1.2.3 Array.of( ...items)
+ of: function(/* ...args */){
+ var index = 0
+ , length = arguments.length
+ // strange IE quirks mode bug -> use typeof instead of isFunction
+ , result = new (typeof this == 'function' ? this : Array)(length);
+ while(length > index)result[index] = arguments[index++];
+ result.length = length;
+ return result;
+ }
+});
+},{"./$.def":12}],34:[function(require,module,exports){
+require('./$.species')(Array);
+},{"./$.species":21}],35:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , NAME = 'name'
+ , setDesc = $.setDesc
+ , FunctionProto = Function.prototype;
+// 19.2.4.2 name
+NAME in FunctionProto || $.FW && $.DESC && setDesc(FunctionProto, NAME, {
+ configurable: true,
+ get: function(){
+ var match = String(this).match(/^\s*function ([^ (]*)/)
+ , name = match ? match[1] : '';
+ $.has(this, NAME) || setDesc(this, NAME, $.desc(5, name));
+ return name;
+ },
+ set: function(value){
+ $.has(this, NAME) || setDesc(this, NAME, $.desc(0, value));
+ }
+});
+},{"./$":16}],36:[function(require,module,exports){
+'use strict';
+var strong = require('./$.collection-strong');
+
+// 23.1 Map Objects
+require('./$.collection')('Map', {
+ // 23.1.3.6 Map.prototype.get(key)
+ get: function(key){
+ var entry = strong.getEntry(this, key);
+ return entry && entry.v;
+ },
+ // 23.1.3.9 Map.prototype.set(key, value)
+ set: function(key, value){
+ return strong.def(this, key === 0 ? 0 : key, value);
+ }
+}, strong, true);
+},{"./$.collection":10,"./$.collection-strong":8}],37:[function(require,module,exports){
+var Infinity = 1 / 0
+ , $def = require('./$.def')
+ , E = Math.E
+ , pow = Math.pow
+ , abs = Math.abs
+ , exp = Math.exp
+ , log = Math.log
+ , sqrt = Math.sqrt
+ , ceil = Math.ceil
+ , floor = Math.floor
+ , sign = Math.sign || function(x){
+ return (x = +x) == 0 || x != x ? x : x < 0 ? -1 : 1;
+ };
+
+// 20.2.2.5 Math.asinh(x)
+function asinh(x){
+ return !isFinite(x = +x) || x == 0 ? x : x < 0 ? -asinh(-x) : log(x + sqrt(x * x + 1));
+}
+// 20.2.2.14 Math.expm1(x)
+function expm1(x){
+ return (x = +x) == 0 ? x : x > -1e-6 && x < 1e-6 ? x + x * x / 2 : exp(x) - 1;
+}
+
+$def($def.S, 'Math', {
+ // 20.2.2.3 Math.acosh(x)
+ acosh: function(x){
+ return (x = +x) < 1 ? NaN : isFinite(x) ? log(x / E + sqrt(x + 1) * sqrt(x - 1) / E) + 1 : x;
+ },
+ // 20.2.2.5 Math.asinh(x)
+ asinh: asinh,
+ // 20.2.2.7 Math.atanh(x)
+ atanh: function(x){
+ return (x = +x) == 0 ? x : log((1 + x) / (1 - x)) / 2;
+ },
+ // 20.2.2.9 Math.cbrt(x)
+ cbrt: function(x){
+ return sign(x = +x) * pow(abs(x), 1 / 3);
+ },
+ // 20.2.2.11 Math.clz32(x)
+ clz32: function(x){
+ return (x >>>= 0) ? 32 - x.toString(2).length : 32;
+ },
+ // 20.2.2.12 Math.cosh(x)
+ cosh: function(x){
+ return (exp(x = +x) + exp(-x)) / 2;
+ },
+ // 20.2.2.14 Math.expm1(x)
+ expm1: expm1,
+ // 20.2.2.16 Math.fround(x)
+ // TODO: fallback for IE9-
+ fround: function(x){
+ return new Float32Array([x])[0];
+ },
+ // 20.2.2.17 Math.hypot([value1[, value2[, … ]]])
+ hypot: function(value1, value2){ // eslint-disable-line no-unused-vars
+ var sum = 0
+ , len1 = arguments.length
+ , len2 = len1
+ , args = Array(len1)
+ , larg = -Infinity
+ , arg;
+ while(len1--){
+ arg = args[len1] = +arguments[len1];
+ if(arg == Infinity || arg == -Infinity)return Infinity;
+ if(arg > larg)larg = arg;
+ }
+ larg = arg || 1;
+ while(len2--)sum += pow(args[len2] / larg, 2);
+ return larg * sqrt(sum);
+ },
+ // 20.2.2.18 Math.imul(x, y)
+ imul: function(x, y){
+ var UInt16 = 0xffff
+ , xn = +x
+ , yn = +y
+ , xl = UInt16 & xn
+ , yl = UInt16 & yn;
+ return 0 | xl * yl + ((UInt16 & xn >>> 16) * yl + xl * (UInt16 & yn >>> 16) << 16 >>> 0);
+ },
+ // 20.2.2.20 Math.log1p(x)
+ log1p: function(x){
+ return (x = +x) > -1e-8 && x < 1e-8 ? x - x * x / 2 : log(1 + x);
+ },
+ // 20.2.2.21 Math.log10(x)
+ log10: function(x){
+ return log(x) / Math.LN10;
+ },
+ // 20.2.2.22 Math.log2(x)
+ log2: function(x){
+ return log(x) / Math.LN2;
+ },
+ // 20.2.2.28 Math.sign(x)
+ sign: sign,
+ // 20.2.2.30 Math.sinh(x)
+ sinh: function(x){
+ return abs(x = +x) < 1 ? (expm1(x) - expm1(-x)) / 2 : (exp(x - 1) - exp(-x - 1)) * (E / 2);
+ },
+ // 20.2.2.33 Math.tanh(x)
+ tanh: function(x){
+ var a = expm1(x = +x)
+ , b = expm1(-x);
+ return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (exp(x) + exp(-x));
+ },
+ // 20.2.2.34 Math.trunc(x)
+ trunc: function(it){
+ return (it > 0 ? floor : ceil)(it);
+ }
+});
+},{"./$.def":12}],38:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , isObject = $.isObject
+ , isFunction = $.isFunction
+ , NUMBER = 'Number'
+ , Number = $.g[NUMBER]
+ , Base = Number
+ , proto = Number.prototype;
+function toPrimitive(it){
+ var fn, val;
+ if(isFunction(fn = it.valueOf) && !isObject(val = fn.call(it)))return val;
+ if(isFunction(fn = it.toString) && !isObject(val = fn.call(it)))return val;
+ throw TypeError("Can't convert object to number");
+}
+function toNumber(it){
+ if(isObject(it))it = toPrimitive(it);
+ if(typeof it == 'string' && it.length > 2 && it.charCodeAt(0) == 48){
+ var binary = false;
+ switch(it.charCodeAt(1)){
+ case 66 : case 98 : binary = true;
+ case 79 : case 111 : return parseInt(it.slice(2), binary ? 2 : 8);
+ }
+ } return +it;
+}
+if($.FW && !(Number('0o1') && Number('0b1'))){
+ Number = function Number(it){
+ return this instanceof Number ? new Base(toNumber(it)) : toNumber(it);
+ };
+ $.each.call($.DESC ? $.getNames(Base) : (
+ // ES3:
+ 'MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,' +
+ // ES6 (in case, if modules with ES6 Number statics required before):
+ 'EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,' +
+ 'MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger'
+ ).split(','), function(key){
+ if($.has(Base, key) && !$.has(Number, key)){
+ $.setDesc(Number, key, $.getDesc(Base, key));
+ }
+ }
+ );
+ Number.prototype = proto;
+ proto.constructor = Number;
+ $.hide($.g, NUMBER, Number);
+}
+},{"./$":16}],39:[function(require,module,exports){
+var $ = require('./$')
+ , $def = require('./$.def')
+ , abs = Math.abs
+ , floor = Math.floor
+ , MAX_SAFE_INTEGER = 0x1fffffffffffff; // pow(2, 53) - 1 == 9007199254740991;
+function isInteger(it){
+ return !$.isObject(it) && isFinite(it) && floor(it) === it;
+}
+$def($def.S, 'Number', {
+ // 20.1.2.1 Number.EPSILON
+ EPSILON: Math.pow(2, -52),
+ // 20.1.2.2 Number.isFinite(number)
+ isFinite: function(it){
+ return typeof it == 'number' && isFinite(it);
+ },
+ // 20.1.2.3 Number.isInteger(number)
+ isInteger: isInteger,
+ // 20.1.2.4 Number.isNaN(number)
+ isNaN: function(number){
+ return number != number;
+ },
+ // 20.1.2.5 Number.isSafeInteger(number)
+ isSafeInteger: function(number){
+ return isInteger(number) && abs(number) <= MAX_SAFE_INTEGER;
+ },
+ // 20.1.2.6 Number.MAX_SAFE_INTEGER
+ MAX_SAFE_INTEGER: MAX_SAFE_INTEGER,
+ // 20.1.2.10 Number.MIN_SAFE_INTEGER
+ MIN_SAFE_INTEGER: -MAX_SAFE_INTEGER,
+ // 20.1.2.12 Number.parseFloat(string)
+ parseFloat: parseFloat,
+ // 20.1.2.13 Number.parseInt(string, radix)
+ parseInt: parseInt
+});
+},{"./$":16,"./$.def":12}],40:[function(require,module,exports){
+// 19.1.3.1 Object.assign(target, source)
+var $def = require('./$.def');
+$def($def.S, 'Object', {assign: require('./$.assign')});
+},{"./$.assign":6,"./$.def":12}],41:[function(require,module,exports){
+// 19.1.3.10 Object.is(value1, value2)
+var $def = require('./$.def');
+$def($def.S, 'Object', {
+ is: function(x, y){
+ return x === y ? x !== 0 || 1 / x === 1 / y : x != x && y != y;
+ }
+});
+},{"./$.def":12}],42:[function(require,module,exports){
+// 19.1.3.19 Object.setPrototypeOf(O, proto)
+var $def = require('./$.def');
+$def($def.S, 'Object', {setPrototypeOf: require('./$.set-proto')});
+},{"./$.def":12,"./$.set-proto":20}],43:[function(require,module,exports){
+var $ = require('./$')
+ , $def = require('./$.def')
+ , isObject = $.isObject
+ , toObject = $.toObject;
+function wrapObjectMethod(METHOD, MODE){
+ var fn = ($.core.Object || {})[METHOD] || Object[METHOD]
+ , f = 0
+ , o = {};
+ o[METHOD] = MODE == 1 ? function(it){
+ return isObject(it) ? fn(it) : it;
+ } : MODE == 2 ? function(it){
+ return isObject(it) ? fn(it) : true;
+ } : MODE == 3 ? function(it){
+ return isObject(it) ? fn(it) : false;
+ } : MODE == 4 ? function(it, key){
+ return fn(toObject(it), key);
+ } : MODE == 5 ? function(it){
+ return fn(Object($.assertDefined(it)));
+ } : function(it){
+ return fn(toObject(it));
+ };
+ try {
+ fn('z');
+ } catch(e){
+ f = 1;
+ }
+ $def($def.S + $def.F * f, 'Object', o);
+}
+wrapObjectMethod('freeze', 1);
+wrapObjectMethod('seal', 1);
+wrapObjectMethod('preventExtensions', 1);
+wrapObjectMethod('isFrozen', 2);
+wrapObjectMethod('isSealed', 2);
+wrapObjectMethod('isExtensible', 3);
+wrapObjectMethod('getOwnPropertyDescriptor', 4);
+wrapObjectMethod('getPrototypeOf', 5);
+wrapObjectMethod('keys');
+wrapObjectMethod('getOwnPropertyNames');
+},{"./$":16,"./$.def":12}],44:[function(require,module,exports){
+'use strict';
+// 19.1.3.6 Object.prototype.toString()
+var $ = require('./$')
+ , cof = require('./$.cof')
+ , tmp = {};
+tmp[require('./$.wks')('toStringTag')] = 'z';
+if($.FW && cof(tmp) != 'z')$.hide(Object.prototype, 'toString', function(){
+ return '[object ' + cof.classof(this) + ']';
+});
+},{"./$":16,"./$.cof":7,"./$.wks":26}],45:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , ctx = require('./$.ctx')
+ , cof = require('./$.cof')
+ , $def = require('./$.def')
+ , assert = require('./$.assert')
+ , $iter = require('./$.iter')
+ , SPECIES = require('./$.wks')('species')
+ , RECORD = require('./$.uid').safe('record')
+ , forOf = $iter.forOf
+ , PROMISE = 'Promise'
+ , global = $.g
+ , process = global.process
+ , asap = process && process.nextTick || require('./$.task').set
+ , Promise = global[PROMISE]
+ , Base = Promise
+ , isFunction = $.isFunction
+ , isObject = $.isObject
+ , assertFunction = assert.fn
+ , assertObject = assert.obj
+ , test;
+function getConstructor(C){
+ var S = assertObject(C)[SPECIES];
+ return S != undefined ? S : C;
+}
+isFunction(Promise) && isFunction(Promise.resolve)
+&& Promise.resolve(test = new Promise(function(){})) == test
+|| function(){
+ function isThenable(it){
+ var then;
+ if(isObject(it))then = it.then;
+ return isFunction(then) ? then : false;
+ }
+ function handledRejectionOrHasOnRejected(promise){
+ var record = promise[RECORD]
+ , chain = record.c
+ , i = 0
+ , react;
+ if(record.h)return true;
+ while(chain.length > i){
+ react = chain[i++];
+ if(react.fail || handledRejectionOrHasOnRejected(react.P))return true;
+ }
+ }
+ function notify(record, isReject){
+ var chain = record.c;
+ if(isReject || chain.length)asap(function(){
+ var promise = record.p
+ , value = record.v
+ , ok = record.s == 1
+ , i = 0;
+ if(isReject && !handledRejectionOrHasOnRejected(promise)){
+ setTimeout(function(){
+ if(!handledRejectionOrHasOnRejected(promise)){
+ if(cof(process) == 'process'){
+ process.emit('unhandledRejection', value, promise);
+ } else if(global.console && isFunction(console.error)){
+ console.error('Unhandled promise rejection', value);
+ }
+ }
+ }, 1e3);
+ } else while(chain.length > i)!function(react){
+ var cb = ok ? react.ok : react.fail
+ , ret, then;
+ try {
+ if(cb){
+ if(!ok)record.h = true;
+ ret = cb === true ? value : cb(value);
+ if(ret === react.P){
+ react.rej(TypeError(PROMISE + '-chain cycle'));
+ } else if(then = isThenable(ret)){
+ then.call(ret, react.res, react.rej);
+ } else react.res(ret);
+ } else react.rej(value);
+ } catch(err){
+ react.rej(err);
+ }
+ }(chain[i++]);
+ chain.length = 0;
+ });
+ }
+ function reject(value){
+ var record = this;
+ if(record.d)return;
+ record.d = true;
+ record = record.r || record; // unwrap
+ record.v = value;
+ record.s = 2;
+ notify(record, true);
+ }
+ function resolve(value){
+ var record = this
+ , then, wrapper;
+ if(record.d)return;
+ record.d = true;
+ record = record.r || record; // unwrap
+ try {
+ if(then = isThenable(value)){
+ wrapper = {r: record, d: false}; // wrap
+ then.call(value, ctx(resolve, wrapper, 1), ctx(reject, wrapper, 1));
+ } else {
+ record.v = value;
+ record.s = 1;
+ notify(record);
+ }
+ } catch(err){
+ reject.call(wrapper || {r: record, d: false}, err); // wrap
+ }
+ }
+ // 25.4.3.1 Promise(executor)
+ Promise = function(executor){
+ assertFunction(executor);
+ var record = {
+ p: assert.inst(this, Promise, PROMISE), // <- promise
+ c: [], // <- chain
+ s: 0, // <- state
+ d: false, // <- done
+ v: undefined, // <- value
+ h: false // <- handled rejection
+ };
+ $.hide(this, RECORD, record);
+ try {
+ executor(ctx(resolve, record, 1), ctx(reject, record, 1));
+ } catch(err){
+ reject.call(record, err);
+ }
+ };
+ $.mix(Promise.prototype, {
+ // 25.4.5.3 Promise.prototype.then(onFulfilled, onRejected)
+ then: function(onFulfilled, onRejected){
+ var S = assertObject(assertObject(this).constructor)[SPECIES];
+ var react = {
+ ok: isFunction(onFulfilled) ? onFulfilled : true,
+ fail: isFunction(onRejected) ? onRejected : false
+ };
+ var P = react.P = new (S != undefined ? S : Promise)(function(res, rej){
+ react.res = assertFunction(res);
+ react.rej = assertFunction(rej);
+ });
+ var record = this[RECORD];
+ record.c.push(react);
+ record.s && notify(record);
+ return P;
+ },
+ // 25.4.5.1 Promise.prototype.catch(onRejected)
+ 'catch': function(onRejected){
+ return this.then(undefined, onRejected);
+ }
+ });
+}();
+$def($def.G + $def.W + $def.F * (Promise != Base), {Promise: Promise});
+$def($def.S, PROMISE, {
+ // 25.4.4.5 Promise.reject(r)
+ reject: function(r){
+ return new (getConstructor(this))(function(res, rej){
+ rej(r);
+ });
+ },
+ // 25.4.4.6 Promise.resolve(x)
+ resolve: function(x){
+ return isObject(x) && RECORD in x && $.getProto(x) === this.prototype
+ ? x : new (getConstructor(this))(function(res){
+ res(x);
+ });
+ }
+});
+$def($def.S + $def.F * ($iter.fail(function(iter){
+ Promise.all(iter)['catch'](function(){});
+}) || $iter.DANGER_CLOSING), PROMISE, {
+ // 25.4.4.1 Promise.all(iterable)
+ all: function(iterable){
+ var C = getConstructor(this)
+ , values = [];
+ return new C(function(resolve, reject){
+ forOf(iterable, false, values.push, values);
+ var remaining = values.length
+ , results = Array(remaining);
+ if(remaining)$.each.call(values, function(promise, index){
+ C.resolve(promise).then(function(value){
+ results[index] = value;
+ --remaining || resolve(results);
+ }, reject);
+ });
+ else resolve(results);
+ });
+ },
+ // 25.4.4.4 Promise.race(iterable)
+ race: function(iterable){
+ var C = getConstructor(this);
+ return new C(function(resolve, reject){
+ forOf(iterable, false, function(promise){
+ C.resolve(promise).then(resolve, reject);
+ });
+ });
+ }
+});
+cof.set(Promise, PROMISE);
+require('./$.species')(Promise);
+},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.ctx":11,"./$.def":12,"./$.iter":15,"./$.species":21,"./$.task":23,"./$.uid":24,"./$.wks":26}],46:[function(require,module,exports){
+var $ = require('./$')
+ , $def = require('./$.def')
+ , setProto = require('./$.set-proto')
+ , $iter = require('./$.iter')
+ , ITER = require('./$.uid').safe('iter')
+ , step = $iter.step
+ , assert = require('./$.assert')
+ , isObject = $.isObject
+ , getDesc = $.getDesc
+ , setDesc = $.setDesc
+ , getProto = $.getProto
+ , apply = Function.apply
+ , assertObject = assert.obj
+ , isExtensible = Object.isExtensible || $.it;
+function Enumerate(iterated){
+ var keys = [], key;
+ for(key in iterated)keys.push(key);
+ $.set(this, ITER, {o: iterated, a: keys, i: 0});
+}
+$iter.create(Enumerate, 'Object', function(){
+ var iter = this[ITER]
+ , keys = iter.a
+ , key;
+ do {
+ if(iter.i >= keys.length)return step(1);
+ } while(!((key = keys[iter.i++]) in iter.o));
+ return step(0, key);
+});
+
+function wrap(fn){
+ return function(it){
+ assertObject(it);
+ try {
+ fn.apply(undefined, arguments);
+ return true;
+ } catch(e){
+ return false;
+ }
+ };
+}
+
+function reflectGet(target, propertyKey/*, receiver*/){
+ var receiver = arguments.length < 3 ? target : arguments[2]
+ , desc = getDesc(assertObject(target), propertyKey), proto;
+ if(desc)return $.has(desc, 'value')
+ ? desc.value
+ : desc.get === undefined
+ ? undefined
+ : desc.get.call(receiver);
+ return isObject(proto = getProto(target))
+ ? reflectGet(proto, propertyKey, receiver)
+ : undefined;
+}
+function reflectSet(target, propertyKey, V/*, receiver*/){
+ var receiver = arguments.length < 4 ? target : arguments[3]
+ , ownDesc = getDesc(assertObject(target), propertyKey)
+ , existingDescriptor, proto;
+ if(!ownDesc){
+ if(isObject(proto = getProto(target))){
+ return reflectSet(proto, propertyKey, V, receiver);
+ }
+ ownDesc = $.desc(0);
+ }
+ if($.has(ownDesc, 'value')){
+ if(ownDesc.writable === false || !isObject(receiver))return false;
+ existingDescriptor = getDesc(receiver, propertyKey) || $.desc(0);
+ existingDescriptor.value = V;
+ setDesc(receiver, propertyKey, existingDescriptor);
+ return true;
+ }
+ return ownDesc.set === undefined ? false : (ownDesc.set.call(receiver, V), true);
+}
+
+var reflect = {
+ // 26.1.1 Reflect.apply(target, thisArgument, argumentsList)
+ apply: require('./$.ctx')(Function.call, apply, 3),
+ // 26.1.2 Reflect.construct(target, argumentsList [, newTarget])
+ construct: function(target, argumentsList /*, newTarget*/){
+ var proto = assert.fn(arguments.length < 3 ? target : arguments[2]).prototype
+ , instance = $.create(isObject(proto) ? proto : Object.prototype)
+ , result = apply.call(target, instance, argumentsList);
+ return isObject(result) ? result : instance;
+ },
+ // 26.1.3 Reflect.defineProperty(target, propertyKey, attributes)
+ defineProperty: wrap(setDesc),
+ // 26.1.4 Reflect.deleteProperty(target, propertyKey)
+ deleteProperty: function(target, propertyKey){
+ var desc = getDesc(assertObject(target), propertyKey);
+ return desc && !desc.configurable ? false : delete target[propertyKey];
+ },
+ // 26.1.5 Reflect.enumerate(target)
+ enumerate: function(target){
+ return new Enumerate(assertObject(target));
+ },
+ // 26.1.6 Reflect.get(target, propertyKey [, receiver])
+ get: reflectGet,
+ // 26.1.7 Reflect.getOwnPropertyDescriptor(target, propertyKey)
+ getOwnPropertyDescriptor: function(target, propertyKey){
+ return getDesc(assertObject(target), propertyKey);
+ },
+ // 26.1.8 Reflect.getPrototypeOf(target)
+ getPrototypeOf: function(target){
+ return getProto(assertObject(target));
+ },
+ // 26.1.9 Reflect.has(target, propertyKey)
+ has: function(target, propertyKey){
+ return propertyKey in target;
+ },
+ // 26.1.10 Reflect.isExtensible(target)
+ isExtensible: function(target){
+ return !!isExtensible(assertObject(target));
+ },
+ // 26.1.11 Reflect.ownKeys(target)
+ ownKeys: require('./$.own-keys'),
+ // 26.1.12 Reflect.preventExtensions(target)
+ preventExtensions: wrap(Object.preventExtensions || $.it),
+ // 26.1.13 Reflect.set(target, propertyKey, V [, receiver])
+ set: reflectSet
+};
+// 26.1.14 Reflect.setPrototypeOf(target, proto)
+if(setProto)reflect.setPrototypeOf = function(target, proto){
+ setProto(assertObject(target), proto);
+ return true;
+};
+
+$def($def.G, {Reflect: {}});
+$def($def.S, 'Reflect', reflect);
+},{"./$":16,"./$.assert":5,"./$.ctx":11,"./$.def":12,"./$.iter":15,"./$.own-keys":18,"./$.set-proto":20,"./$.uid":24}],47:[function(require,module,exports){
+var $ = require('./$')
+ , cof = require('./$.cof')
+ , RegExp = $.g.RegExp
+ , Base = RegExp
+ , proto = RegExp.prototype;
+if($.FW && $.DESC){
+ // RegExp allows a regex with flags as the pattern
+ if(!function(){try{ return RegExp(/a/g, 'i') == '/a/i'; }catch(e){ /* empty */ }}()){
+ RegExp = function RegExp(pattern, flags){
+ return new Base(cof(pattern) == 'RegExp' && flags !== undefined
+ ? pattern.source : pattern, flags);
+ };
+ $.each.call($.getNames(Base), function(key){
+ key in RegExp || $.setDesc(RegExp, key, {
+ configurable: true,
+ get: function(){ return Base[key]; },
+ set: function(it){ Base[key] = it; }
+ });
+ });
+ proto.constructor = RegExp;
+ RegExp.prototype = proto;
+ $.hide($.g, 'RegExp', RegExp);
+ }
+ // 21.2.5.3 get RegExp.prototype.flags()
+ if(/./g.flags != 'g')$.setDesc(proto, 'flags', {
+ configurable: true,
+ get: require('./$.replacer')(/^.*\/(\w*)$/, '$1')
+ });
+}
+require('./$.species')(RegExp);
+},{"./$":16,"./$.cof":7,"./$.replacer":19,"./$.species":21}],48:[function(require,module,exports){
+'use strict';
+var strong = require('./$.collection-strong');
+
+// 23.2 Set Objects
+require('./$.collection')('Set', {
+ // 23.2.3.1 Set.prototype.add(value)
+ add: function(value){
+ return strong.def(this, value = value === 0 ? 0 : value, value);
+ }
+}, strong);
+},{"./$.collection":10,"./$.collection-strong":8}],49:[function(require,module,exports){
+var $def = require('./$.def');
+$def($def.P, 'String', {
+ // 21.1.3.3 String.prototype.codePointAt(pos)
+ codePointAt: require('./$.string-at')(false)
+});
+},{"./$.def":12,"./$.string-at":22}],50:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , cof = require('./$.cof')
+ , $def = require('./$.def')
+ , toLength = $.toLength;
+
+$def($def.P, 'String', {
+ // 21.1.3.6 String.prototype.endsWith(searchString [, endPosition])
+ endsWith: function(searchString /*, endPosition = @length */){
+ if(cof(searchString) == 'RegExp')throw TypeError();
+ var that = String($.assertDefined(this))
+ , endPosition = arguments[1]
+ , len = toLength(that.length)
+ , end = endPosition === undefined ? len : Math.min(toLength(endPosition), len);
+ searchString += '';
+ return that.slice(end - searchString.length, end) === searchString;
+ }
+});
+},{"./$":16,"./$.cof":7,"./$.def":12}],51:[function(require,module,exports){
+var $def = require('./$.def')
+ , toIndex = require('./$').toIndex
+ , fromCharCode = String.fromCharCode;
+
+$def($def.S, 'String', {
+ // 21.1.2.2 String.fromCodePoint(...codePoints)
+ fromCodePoint: function(x){ // eslint-disable-line no-unused-vars
+ var res = []
+ , len = arguments.length
+ , i = 0
+ , code;
+ while(len > i){
+ code = +arguments[i++];
+ if(toIndex(code, 0x10ffff) !== code)throw RangeError(code + ' is not a valid code point');
+ res.push(code < 0x10000
+ ? fromCharCode(code)
+ : fromCharCode(((code -= 0x10000) >> 10) + 0xd800, code % 0x400 + 0xdc00)
+ );
+ } return res.join('');
+ }
+});
+},{"./$":16,"./$.def":12}],52:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , cof = require('./$.cof')
+ , $def = require('./$.def');
+
+$def($def.P, 'String', {
+ // 21.1.3.7 String.prototype.includes(searchString, position = 0)
+ includes: function(searchString /*, position = 0 */){
+ if(cof(searchString) == 'RegExp')throw TypeError();
+ return !!~String($.assertDefined(this)).indexOf(searchString, arguments[1]);
+ }
+});
+},{"./$":16,"./$.cof":7,"./$.def":12}],53:[function(require,module,exports){
+var set = require('./$').set
+ , at = require('./$.string-at')(true)
+ , ITER = require('./$.uid').safe('iter')
+ , $iter = require('./$.iter')
+ , step = $iter.step;
+
+// 21.1.3.27 String.prototype[@@iterator]()
+$iter.std(String, 'String', function(iterated){
+ set(this, ITER, {o: String(iterated), i: 0});
+// 21.1.5.2.1 %StringIteratorPrototype%.next()
+}, function(){
+ var iter = this[ITER]
+ , O = iter.o
+ , index = iter.i
+ , point;
+ if(index >= O.length)return step(1);
+ point = at.call(O, index);
+ iter.i += point.length;
+ return step(0, point);
+});
+},{"./$":16,"./$.iter":15,"./$.string-at":22,"./$.uid":24}],54:[function(require,module,exports){
+var $ = require('./$')
+ , $def = require('./$.def');
+
+$def($def.S, 'String', {
+ // 21.1.2.4 String.raw(callSite, ...substitutions)
+ raw: function(callSite){
+ var raw = $.toObject(callSite.raw)
+ , len = $.toLength(raw.length)
+ , sln = arguments.length
+ , res = []
+ , i = 0;
+ while(len > i){
+ res.push(String(raw[i++]));
+ if(i < sln)res.push(String(arguments[i]));
+ } return res.join('');
+ }
+});
+},{"./$":16,"./$.def":12}],55:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , $def = require('./$.def');
+
+$def($def.P, 'String', {
+ // 21.1.3.13 String.prototype.repeat(count)
+ repeat: function(count){
+ var str = String($.assertDefined(this))
+ , res = ''
+ , n = $.toInteger(count);
+ if(n < 0 || n == Infinity)throw RangeError("Count can't be negative");
+ for(;n > 0; (n >>>= 1) && (str += str))if(n & 1)res += str;
+ return res;
+ }
+});
+},{"./$":16,"./$.def":12}],56:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , cof = require('./$.cof')
+ , $def = require('./$.def');
+
+$def($def.P, 'String', {
+ // 21.1.3.18 String.prototype.startsWith(searchString [, position ])
+ startsWith: function(searchString /*, position = 0 */){
+ if(cof(searchString) == 'RegExp')throw TypeError();
+ var that = String($.assertDefined(this))
+ , index = $.toLength(Math.min(arguments[1], that.length));
+ searchString += '';
+ return that.slice(index, index + searchString.length) === searchString;
+ }
+});
+},{"./$":16,"./$.cof":7,"./$.def":12}],57:[function(require,module,exports){
+'use strict';
+// ECMAScript 6 symbols shim
+var $ = require('./$')
+ , setTag = require('./$.cof').set
+ , uid = require('./$.uid')
+ , $def = require('./$.def')
+ , keyOf = require('./$.keyof')
+ , has = $.has
+ , hide = $.hide
+ , getNames = $.getNames
+ , toObject = $.toObject
+ , Symbol = $.g.Symbol
+ , Base = Symbol
+ , setter = false
+ , TAG = uid.safe('tag')
+ , SymbolRegistry = {}
+ , AllSymbols = {};
+
+function wrap(tag){
+ var sym = AllSymbols[tag] = $.set($.create(Symbol.prototype), TAG, tag);
+ $.DESC && setter && $.setDesc(Object.prototype, tag, {
+ configurable: true,
+ set: function(value){
+ hide(this, tag, value);
+ }
+ });
+ return sym;
+}
+
+// 19.4.1.1 Symbol([description])
+if(!$.isFunction(Symbol)){
+ Symbol = function(description){
+ if(this instanceof Symbol)throw TypeError('Symbol is not a constructor');
+ return wrap(uid(description));
+ };
+ hide(Symbol.prototype, 'toString', function(){
+ return this[TAG];
+ });
+}
+$def($def.G + $def.W, {Symbol: Symbol});
+
+var symbolStatics = {
+ // 19.4.2.1 Symbol.for(key)
+ 'for': function(key){
+ return has(SymbolRegistry, key += '')
+ ? SymbolRegistry[key]
+ : SymbolRegistry[key] = Symbol(key);
+ },
+ // 19.4.2.5 Symbol.keyFor(sym)
+ keyFor: function(key){
+ return keyOf(SymbolRegistry, key);
+ },
+ pure: uid.safe,
+ set: $.set,
+ useSetter: function(){ setter = true; },
+ useSimple: function(){ setter = false; }
+};
+// 19.4.2.2 Symbol.hasInstance
+// 19.4.2.3 Symbol.isConcatSpreadable
+// 19.4.2.4 Symbol.iterator
+// 19.4.2.6 Symbol.match
+// 19.4.2.8 Symbol.replace
+// 19.4.2.9 Symbol.search
+// 19.4.2.10 Symbol.species
+// 19.4.2.11 Symbol.split
+// 19.4.2.12 Symbol.toPrimitive
+// 19.4.2.13 Symbol.toStringTag
+// 19.4.2.14 Symbol.unscopables
+$.each.call((
+ 'hasInstance,isConcatSpreadable,iterator,match,replace,search,' +
+ 'species,split,toPrimitive,toStringTag,unscopables'
+ ).split(','), function(it){
+ var sym = require('./$.wks')(it);
+ symbolStatics[it] = Symbol === Base ? sym : wrap(sym);
+ }
+);
+
+setter = true;
+
+$def($def.S, 'Symbol', symbolStatics);
+
+$def($def.S + $def.F * (Symbol != Base), 'Object', {
+ // 19.1.2.7 Object.getOwnPropertyNames(O)
+ getOwnPropertyNames: function(it){
+ var names = getNames(toObject(it)), result = [], key, i = 0;
+ while(names.length > i)has(AllSymbols, key = names[i++]) || result.push(key);
+ return result;
+ },
+ // 19.1.2.8 Object.getOwnPropertySymbols(O)
+ getOwnPropertySymbols: function(it){
+ var names = getNames(toObject(it)), result = [], key, i = 0;
+ while(names.length > i)has(AllSymbols, key = names[i++]) && result.push(AllSymbols[key]);
+ return result;
+ }
+});
+
+setTag(Symbol, 'Symbol');
+// 20.2.1.9 Math[@@toStringTag]
+setTag(Math, 'Math', true);
+// 24.3.3 JSON[@@toStringTag]
+setTag($.g.JSON, 'JSON', true);
+},{"./$":16,"./$.cof":7,"./$.def":12,"./$.keyof":17,"./$.uid":24,"./$.wks":26}],58:[function(require,module,exports){
+'use strict';
+var $ = require('./$')
+ , weak = require('./$.collection-weak')
+ , leakStore = weak.leakStore
+ , ID = weak.ID
+ , WEAK = weak.WEAK
+ , has = $.has
+ , isObject = $.isObject
+ , isFrozen = Object.isFrozen || $.core.Object.isFrozen
+ , tmp = {};
+
+// 23.3 WeakMap Objects
+var WeakMap = require('./$.collection')('WeakMap', {
+ // 23.3.3.3 WeakMap.prototype.get(key)
+ get: function(key){
+ if(isObject(key)){
+ if(isFrozen(key))return leakStore(this).get(key);
+ if(has(key, WEAK))return key[WEAK][this[ID]];
+ }
+ },
+ // 23.3.3.5 WeakMap.prototype.set(key, value)
+ set: function(key, value){
+ return weak.def(this, key, value);
+ }
+}, weak, true, true);
+
+// IE11 WeakMap frozen keys fix
+if($.FW && new WeakMap().set((Object.freeze || Object)(tmp), 7).get(tmp) != 7){
+ $.each.call(['delete', 'has', 'get', 'set'], function(key){
+ var method = WeakMap.prototype[key];
+ WeakMap.prototype[key] = function(a, b){
+ // store frozen objects on leaky map
+ if(isObject(a) && isFrozen(a)){
+ var result = leakStore(this)[key](a, b);
+ return key == 'set' ? this : result;
+ // store all the rest on native weakmap
+ } return method.call(this, a, b);
+ };
+ });
+}
+},{"./$":16,"./$.collection":10,"./$.collection-weak":9}],59:[function(require,module,exports){
+'use strict';
+var weak = require('./$.collection-weak');
+
+// 23.4 WeakSet Objects
+require('./$.collection')('WeakSet', {
+ // 23.4.3.1 WeakSet.prototype.add(value)
+ add: function(value){
+ return weak.def(this, value, true);
+ }
+}, weak, false, true);
+},{"./$.collection":10,"./$.collection-weak":9}],60:[function(require,module,exports){
+(function (global){
+/**
+ * Copyright (c) 2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * https://raw.github.com/facebook/regenerator/master/LICENSE file. An
+ * additional grant of patent rights can be found in the PATENTS file in
+ * the same directory.
+ */
+
+!(function(global) {
+ "use strict";
+
+ var hasOwn = Object.prototype.hasOwnProperty;
+ var undefined; // More compressible than void 0.
+ var iteratorSymbol =
+ typeof Symbol === "function" && Symbol.iterator || "@@iterator";
+
+ var inModule = typeof module === "object";
+ var runtime = global.regeneratorRuntime;
+ if (runtime) {
+ if (inModule) {
+ // If regeneratorRuntime is defined globally and we're in a module,
+ // make the exports object identical to regeneratorRuntime.
+ module.exports = runtime;
+ }
+ // Don't bother evaluating the rest of this file if the runtime was
+ // already defined globally.
+ return;
+ }
+
+ // Define the runtime globally (as expected by generated code) as either
+ // module.exports (if we're in a module) or a new, empty object.
+ runtime = global.regeneratorRuntime = inModule ? module.exports : {};
+
+ function wrap(innerFn, outerFn, self, tryLocsList) {
+ return new Generator(innerFn, outerFn, self || null, tryLocsList || []);
+ }
+ runtime.wrap = wrap;
+
+ // Try/catch helper to minimize deoptimizations. Returns a completion
+ // record like context.tryEntries[i].completion. This interface could
+ // have been (and was previously) designed to take a closure to be
+ // invoked without arguments, but in all the cases we care about we
+ // already have an existing method we want to call, so there's no need
+ // to create a new function object. We can even get away with assuming
+ // the method takes exactly one argument, since that happens to be true
+ // in every case, so we don't have to touch the arguments object. The
+ // only additional allocation required is the completion record, which
+ // has a stable shape and so hopefully should be cheap to allocate.
+ function tryCatch(fn, obj, arg) {
+ try {
+ return { type: "normal", arg: fn.call(obj, arg) };
+ } catch (err) {
+ return { type: "throw", arg: err };
+ }
+ }
+
+ var GenStateSuspendedStart = "suspendedStart";
+ var GenStateSuspendedYield = "suspendedYield";
+ var GenStateExecuting = "executing";
+ var GenStateCompleted = "completed";
+
+ // Returning this object from the innerFn has the same effect as
+ // breaking out of the dispatch switch statement.
+ var ContinueSentinel = {};
+
+ // Dummy constructor functions that we use as the .constructor and
+ // .constructor.prototype properties for functions that return Generator
+ // objects. For full spec compliance, you may wish to configure your
+ // minifier not to mangle the names of these two functions.
+ function GeneratorFunction() {}
+ function GeneratorFunctionPrototype() {}
+
+ var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype;
+ GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
+ GeneratorFunctionPrototype.constructor = GeneratorFunction;
+ GeneratorFunction.displayName = "GeneratorFunction";
+
+ runtime.isGeneratorFunction = function(genFun) {
+ var ctor = typeof genFun === "function" && genFun.constructor;
+ return ctor
+ ? ctor === GeneratorFunction ||
+ // For the native GeneratorFunction constructor, the best we can
+ // do is to check its .name property.
+ (ctor.displayName || ctor.name) === "GeneratorFunction"
+ : false;
+ };
+
+ runtime.mark = function(genFun) {
+ genFun.__proto__ = GeneratorFunctionPrototype;
+ genFun.prototype = Object.create(Gp);
+ return genFun;
+ };
+
+ runtime.async = function(innerFn, outerFn, self, tryLocsList) {
+ return new Promise(function(resolve, reject) {
+ var generator = wrap(innerFn, outerFn, self, tryLocsList);
+ var callNext = step.bind(generator.next);
+ var callThrow = step.bind(generator["throw"]);
+
+ function step(arg) {
+ var record = tryCatch(this, null, arg);
+ if (record.type === "throw") {
+ reject(record.arg);
+ return;
+ }
+
+ var info = record.arg;
+ if (info.done) {
+ resolve(info.value);
+ } else {
+ Promise.resolve(info.value).then(callNext, callThrow);
+ }
+ }
+
+ callNext();
+ });
+ };
+
+ function Generator(innerFn, outerFn, self, tryLocsList) {
+ var generator = outerFn ? Object.create(outerFn.prototype) : this;
+ var context = new Context(tryLocsList);
+ var state = GenStateSuspendedStart;
+
+ function invoke(method, arg) {
+ if (state === GenStateExecuting) {
+ throw new Error("Generator is already running");
+ }
+
+ if (state === GenStateCompleted) {
+ // Be forgiving, per 25.3.3.3.3 of the spec:
+ // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
+ return doneResult();
+ }
+
+ while (true) {
+ var delegate = context.delegate;
+ if (delegate) {
+ var record = tryCatch(
+ delegate.iterator[method],
+ delegate.iterator,
+ arg
+ );
+
+ if (record.type === "throw") {
+ context.delegate = null;
+
+ // Like returning generator.throw(uncaught), but without the
+ // overhead of an extra function call.
+ method = "throw";
+ arg = record.arg;
+
+ continue;
+ }
+
+ // Delegate generator ran and handled its own exceptions so
+ // regardless of what the method was, we continue as if it is
+ // "next" with an undefined arg.
+ method = "next";
+ arg = undefined;
+
+ var info = record.arg;
+ if (info.done) {
+ context[delegate.resultName] = info.value;
+ context.next = delegate.nextLoc;
+ } else {
+ state = GenStateSuspendedYield;
+ return info;
+ }
+
+ context.delegate = null;
+ }
+
+ if (method === "next") {
+ if (state === GenStateSuspendedStart &&
+ typeof arg !== "undefined") {
+ // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
+ throw new TypeError(
+ "attempt to send " + JSON.stringify(arg) + " to newborn generator"
+ );
+ }
+
+ if (state === GenStateSuspendedYield) {
+ context.sent = arg;
+ } else {
+ delete context.sent;
+ }
+
+ } else if (method === "throw") {
+ if (state === GenStateSuspendedStart) {
+ state = GenStateCompleted;
+ throw arg;
+ }
+
+ if (context.dispatchException(arg)) {
+ // If the dispatched exception was caught by a catch block,
+ // then let that catch block handle the exception normally.
+ method = "next";
+ arg = undefined;
+ }
+
+ } else if (method === "return") {
+ context.abrupt("return", arg);
+ }
+
+ state = GenStateExecuting;
+
+ var record = tryCatch(innerFn, self, context);
+ if (record.type === "normal") {
+ // If an exception is thrown from innerFn, we leave state ===
+ // GenStateExecuting and loop back for another invocation.
+ state = context.done
+ ? GenStateCompleted
+ : GenStateSuspendedYield;
+
+ var info = {
+ value: record.arg,
+ done: context.done
+ };
+
+ if (record.arg === ContinueSentinel) {
+ if (context.delegate && method === "next") {
+ // Deliberately forget the last sent value so that we don't
+ // accidentally pass it on to the delegate.
+ arg = undefined;
+ }
+ } else {
+ return info;
+ }
+
+ } else if (record.type === "throw") {
+ state = GenStateCompleted;
+
+ if (method === "next") {
+ context.dispatchException(record.arg);
+ } else {
+ arg = record.arg;
+ }
+ }
+ }
+ }
+
+ generator.next = invoke.bind(generator, "next");
+ generator["throw"] = invoke.bind(generator, "throw");
+ generator["return"] = invoke.bind(generator, "return");
+
+ return generator;
+ }
+
+ Gp[iteratorSymbol] = function() {
+ return this;
+ };
+
+ Gp.toString = function() {
+ return "[object Generator]";
+ };
+
+ function pushTryEntry(locs) {
+ var entry = { tryLoc: locs[0] };
+
+ if (1 in locs) {
+ entry.catchLoc = locs[1];
+ }
+
+ if (2 in locs) {
+ entry.finallyLoc = locs[2];
+ entry.afterLoc = locs[3];
+ }
+
+ this.tryEntries.push(entry);
+ }
+
+ function resetTryEntry(entry) {
+ var record = entry.completion || {};
+ record.type = "normal";
+ delete record.arg;
+ entry.completion = record;
+ }
+
+ function Context(tryLocsList) {
+ // The root entry object (effectively a try statement without a catch
+ // or a finally block) gives us a place to store values thrown from
+ // locations where there is no enclosing try statement.
+ this.tryEntries = [{ tryLoc: "root" }];
+ tryLocsList.forEach(pushTryEntry, this);
+ this.reset();
+ }
+
+ runtime.keys = function(object) {
+ var keys = [];
+ for (var key in object) {
+ keys.push(key);
+ }
+ keys.reverse();
+
+ // Rather than returning an object with a next method, we keep
+ // things simple and return the next function itself.
+ return function next() {
+ while (keys.length) {
+ var key = keys.pop();
+ if (key in object) {
+ next.value = key;
+ next.done = false;
+ return next;
+ }
+ }
+
+ // To avoid creating an additional object, we just hang the .value
+ // and .done properties off the next function object itself. This
+ // also ensures that the minifier will not anonymize the function.
+ next.done = true;
+ return next;
+ };
+ };
+
+ function values(iterable) {
+ if (iterable) {
+ var iteratorMethod = iterable[iteratorSymbol];
+ if (iteratorMethod) {
+ return iteratorMethod.call(iterable);
+ }
+
+ if (typeof iterable.next === "function") {
+ return iterable;
+ }
+
+ if (!isNaN(iterable.length)) {
+ var i = -1, next = function next() {
+ while (++i < iterable.length) {
+ if (hasOwn.call(iterable, i)) {
+ next.value = iterable[i];
+ next.done = false;
+ return next;
+ }
+ }
+
+ next.value = undefined;
+ next.done = true;
+
+ return next;
+ };
+
+ return next.next = next;
+ }
+ }
+
+ // Return an iterator with no values.
+ return { next: doneResult };
+ }
+ runtime.values = values;
+
+ function doneResult() {
+ return { value: undefined, done: true };
+ }
+
+ Context.prototype = {
+ constructor: Context,
+
+ reset: function() {
+ this.prev = 0;
+ this.next = 0;
+ this.sent = undefined;
+ this.done = false;
+ this.delegate = null;
+
+ this.tryEntries.forEach(resetTryEntry);
+
+ // Pre-initialize at least 20 temporary variables to enable hidden
+ // class optimizations for simple generators.
+ for (var tempIndex = 0, tempName;
+ hasOwn.call(this, tempName = "t" + tempIndex) || tempIndex < 20;
+ ++tempIndex) {
+ this[tempName] = null;
+ }
+ },
+
+ stop: function() {
+ this.done = true;
+
+ var rootEntry = this.tryEntries[0];
+ var rootRecord = rootEntry.completion;
+ if (rootRecord.type === "throw") {
+ throw rootRecord.arg;
+ }
+
+ return this.rval;
+ },
+
+ dispatchException: function(exception) {
+ if (this.done) {
+ throw exception;
+ }
+
+ var context = this;
+ function handle(loc, caught) {
+ record.type = "throw";
+ record.arg = exception;
+ context.next = loc;
+ return !!caught;
+ }
+
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ var record = entry.completion;
+
+ if (entry.tryLoc === "root") {
+ // Exception thrown outside of any try block that could handle
+ // it, so set the completion value of the entire function to
+ // throw the exception.
+ return handle("end");
+ }
+
+ if (entry.tryLoc <= this.prev) {
+ var hasCatch = hasOwn.call(entry, "catchLoc");
+ var hasFinally = hasOwn.call(entry, "finallyLoc");
+
+ if (hasCatch && hasFinally) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ } else if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+
+ } else if (hasCatch) {
+ if (this.prev < entry.catchLoc) {
+ return handle(entry.catchLoc, true);
+ }
+
+ } else if (hasFinally) {
+ if (this.prev < entry.finallyLoc) {
+ return handle(entry.finallyLoc);
+ }
+
+ } else {
+ throw new Error("try statement without catch or finally");
+ }
+ }
+ }
+ },
+
+ abrupt: function(type, arg) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ if (entry.tryLoc <= this.prev &&
+ hasOwn.call(entry, "finallyLoc") &&
+ this.prev < entry.finallyLoc) {
+ var finallyEntry = entry;
+ break;
+ }
+ }
+
+ if (finallyEntry &&
+ (type === "break" ||
+ type === "continue") &&
+ finallyEntry.tryLoc <= arg &&
+ arg < finallyEntry.finallyLoc) {
+ // Ignore the finally entry if control is not jumping to a
+ // location outside the try/catch block.
+ finallyEntry = null;
+ }
+
+ var record = finallyEntry ? finallyEntry.completion : {};
+ record.type = type;
+ record.arg = arg;
+
+ if (finallyEntry) {
+ this.next = finallyEntry.finallyLoc;
+ } else {
+ this.complete(record);
+ }
+
+ return ContinueSentinel;
+ },
+
+ complete: function(record, afterLoc) {
+ if (record.type === "throw") {
+ throw record.arg;
+ }
+
+ if (record.type === "break" ||
+ record.type === "continue") {
+ this.next = record.arg;
+ } else if (record.type === "return") {
+ this.rval = record.arg;
+ this.next = "end";
+ } else if (record.type === "normal" && afterLoc) {
+ this.next = afterLoc;
+ }
+
+ return ContinueSentinel;
+ },
+
+ finish: function(finallyLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ if (entry.finallyLoc === finallyLoc) {
+ return this.complete(entry.completion, entry.afterLoc);
+ }
+ }
+ },
+
+ "catch": function(tryLoc) {
+ for (var i = this.tryEntries.length - 1; i >= 0; --i) {
+ var entry = this.tryEntries[i];
+ if (entry.tryLoc === tryLoc) {
+ var record = entry.completion;
+ if (record.type === "throw") {
+ var thrown = record.arg;
+ resetTryEntry(entry);
+ }
+ return thrown;
+ }
+ }
+
+ // The context.catch method must only be called with a location
+ // argument that corresponds to a known catch block.
+ throw new Error("illegal catch attempt");
+ },
+
+ delegateYield: function(iterable, resultName, nextLoc) {
+ this.delegate = {
+ iterator: values(iterable),
+ resultName: resultName,
+ nextLoc: nextLoc
+ };
+
+ return ContinueSentinel;
+ }
+ };
+})(
+ // Among the various tricks for obtaining a reference to the global
+ // object, this seems to be the most reliable technique that does not
+ // use indirect eval (which violates Content Security Policy).
+ typeof global === "object" ? global :
+ typeof window === "object" ? window : this
+);
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}]},{},[1]);
diff --git a/web/static/js/babel-es6-polyfill.min.js b/web/static/js/babel-es6-polyfill.min.js
new file mode 100644
index 000000000..794476133
--- /dev/null
+++ b/web/static/js/babel-es6-polyfill.min.js
@@ -0,0 +1,2 @@
+!function t(e,n,r){function o(s,c){if(!n[s]){if(!e[s]){var u="function"==typeof require&&require;if(!c&&u)return u(s,!0);if(i)return i(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var f=n[s]={exports:{}};e[s][0].call(f.exports,function(t){var n=e[s][1][t];return o(n?n:t)},f,f.exports,t,e,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s<r.length;s++)o(r[s]);return o}({1:[function(t){(function(e){"use strict";if(e._babelPolyfill)throw new Error("only one instance of babel/polyfill is allowed");e._babelPolyfill=!0,t("./es6-shim"),t("regenerator-babel/runtime")}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./es6-shim":2,"regenerator-babel/runtime":60}],2:[function(t,e){t("core-js/es6"),e.exports=t("core-js/modules/$").core},{"core-js/es6":3,"core-js/modules/$":16}],3:[function(t,e){t("../modules/es6.symbol"),t("../modules/es6.object.assign"),t("../modules/es6.object.is"),t("../modules/es6.object.set-prototype-of"),t("../modules/es6.object.to-string"),t("../modules/es6.object.statics-accept-primitives"),t("../modules/es6.function.name"),t("../modules/es6.number.constructor"),t("../modules/es6.number.statics"),t("../modules/es6.math"),t("../modules/es6.string.from-code-point"),t("../modules/es6.string.raw"),t("../modules/es6.string.iterator"),t("../modules/es6.string.code-point-at"),t("../modules/es6.string.ends-with"),t("../modules/es6.string.includes"),t("../modules/es6.string.repeat"),t("../modules/es6.string.starts-with"),t("../modules/es6.array.from"),t("../modules/es6.array.of"),t("../modules/es6.array.species"),t("../modules/es6.array.iterator"),t("../modules/es6.array.copy-within"),t("../modules/es6.array.fill"),t("../modules/es6.array.find"),t("../modules/es6.array.find-index"),t("../modules/es6.regexp"),t("../modules/es6.promise"),t("../modules/es6.map"),t("../modules/es6.set"),t("../modules/es6.weak-map"),t("../modules/es6.weak-set"),t("../modules/es6.reflect"),e.exports=t("../modules/$").core},{"../modules/$":16,"../modules/es6.array.copy-within":27,"../modules/es6.array.fill":28,"../modules/es6.array.find":30,"../modules/es6.array.find-index":29,"../modules/es6.array.from":31,"../modules/es6.array.iterator":32,"../modules/es6.array.of":33,"../modules/es6.array.species":34,"../modules/es6.function.name":35,"../modules/es6.map":36,"../modules/es6.math":37,"../modules/es6.number.constructor":38,"../modules/es6.number.statics":39,"../modules/es6.object.assign":40,"../modules/es6.object.is":41,"../modules/es6.object.set-prototype-of":42,"../modules/es6.object.statics-accept-primitives":43,"../modules/es6.object.to-string":44,"../modules/es6.promise":45,"../modules/es6.reflect":46,"../modules/es6.regexp":47,"../modules/es6.set":48,"../modules/es6.string.code-point-at":49,"../modules/es6.string.ends-with":50,"../modules/es6.string.from-code-point":51,"../modules/es6.string.includes":52,"../modules/es6.string.iterator":53,"../modules/es6.string.raw":54,"../modules/es6.string.repeat":55,"../modules/es6.string.starts-with":56,"../modules/es6.symbol":57,"../modules/es6.weak-map":58,"../modules/es6.weak-set":59}],4:[function(t,e){"use strict";var n=t("./$"),r=t("./$.ctx");e.exports=function(t){var e=1==t,o=2==t,i=3==t,s=4==t,c=6==t,u=5==t||c;return function(a){for(var f,l,h=Object(n.assertDefined(this)),d=n.ES5Object(h),p=r(a,arguments[1],3),g=n.toLength(d.length),$=0,v=e?Array(g):o?[]:void 0;g>$;$++)if((u||$ in d)&&(f=d[$],l=p(f,$,h),t))if(e)v[$]=l;else if(l)switch(t){case 3:return!0;case 5:return f;case 6:return $;case 2:v.push(f)}else if(s)return!1;return c?-1:i||s?s:v}}},{"./$":16,"./$.ctx":11}],5:[function(t,e){function n(t,e,n){if(!t)throw TypeError(n?e+n:e)}var r=t("./$");n.def=r.assertDefined,n.fn=function(t){if(!r.isFunction(t))throw TypeError(t+" is not a function!");return t},n.obj=function(t){if(!r.isObject(t))throw TypeError(t+" is not an object!");return t},n.inst=function(t,e,n){if(!(t instanceof e))throw TypeError(n+": use the 'new' operator!");return t},e.exports=n},{"./$":16}],6:[function(t,e){var n=t("./$");e.exports=Object.assign||function(t){for(var e=Object(n.assertDefined(t)),r=arguments.length,o=1;r>o;)for(var i,s=n.ES5Object(arguments[o++]),c=n.getKeys(s),u=c.length,a=0;u>a;)e[i=c[a++]]=s[i];return e}},{"./$":16}],7:[function(t,e){function n(t){return i.call(t).slice(8,-1)}var r=t("./$"),o=t("./$.wks")("toStringTag"),i={}.toString;n.classof=function(t){var e,r;return void 0==t?void 0===t?"Undefined":"Null":"string"==typeof(r=(e=Object(t))[o])?r:n(e)},n.set=function(t,e,n){t&&!r.has(t=n?t:t.prototype,o)&&r.hide(t,o,e)},e.exports=n},{"./$":16,"./$.wks":26}],8:[function(t,e){"use strict";function n(t,e){if(!l(t))return("string"==typeof t?"S":"P")+t;if(p(t))return"F";if(!a(t,g)){if(!e)return"E";h(t,g,++w)}return"O"+t[g]}function r(t,e){var r,o=n(e);if("F"!=o)return t[$][o];for(r=t[y];r;r=r.n)if(r.k==e)return r}var o=t("./$"),i=t("./$.ctx"),s=t("./$.uid").safe,c=t("./$.assert"),u=t("./$.iter"),a=o.has,f=o.set,l=o.isObject,h=o.hide,d=u.step,p=Object.isFrozen||o.core.Object.isFrozen,g=s("id"),$=s("O1"),v=s("last"),y=s("first"),m=s("iter"),b=o.DESC?s("size"):"size",w=0;e.exports={getConstructor:function(t,e,n){function s(r){var i=c.inst(this,s,t);f(i,$,o.create(null)),f(i,b,0),f(i,v,void 0),f(i,y,void 0),void 0!=r&&u.forOf(r,e,i[n],i)}return o.mix(s.prototype,{clear:function(){for(var t=this,e=t[$],n=t[y];n;n=n.n)n.r=!0,n.p&&(n.p=n.p.n=void 0),delete e[n.i];t[y]=t[v]=void 0,t[b]=0},"delete":function(t){var e=this,n=r(e,t);if(n){var o=n.n,i=n.p;delete e[$][n.i],n.r=!0,i&&(i.n=o),o&&(o.p=i),e[y]==n&&(e[y]=o),e[v]==n&&(e[v]=i),e[b]--}return!!n},forEach:function(t){for(var e,n=i(t,arguments[1],3);e=e?e.n:this[y];)for(n(e.v,e.k,this);e&&e.r;)e=e.p},has:function(t){return!!r(this,t)}}),o.DESC&&o.setDesc(s.prototype,"size",{get:function(){return c.def(this[b])}}),s},def:function(t,e,o){var i,s,c=r(t,e);return c?c.v=o:(t[v]=c={i:s=n(e,!0),k:e,v:o,p:i=t[v],n:void 0,r:!1},t[y]||(t[y]=c),i&&(i.n=c),t[b]++,"F"!=s&&(t[$][s]=c)),t},getEntry:r,getIterConstructor:function(){return function(t,e){f(this,m,{o:t,k:e})}},next:function(){for(var t=this[m],e=t.k,n=t.l;n&&n.r;)n=n.p;return t.o&&(t.l=n=n?n.n:t.o[y])?"key"==e?d(0,n.k):"value"==e?d(0,n.v):d(0,[n.k,n.v]):(t.o=void 0,d(1))}}},{"./$":16,"./$.assert":5,"./$.ctx":11,"./$.iter":15,"./$.uid":24}],9:[function(t,e){"use strict";function n(t,e){return v.call(t.array,function(t){return t[0]===e})}function r(t){return t[g]||f(t,g,{array:[],get:function(t){var e=n(this,t);return e?e[1]:void 0},has:function(t){return!!n(this,t)},set:function(t,e){var r=n(this,t);r?r[1]=e:this.array.push([t,e])},"delete":function(t){var e=y.call(this.array,function(e){return e[0]===t});return~e&&this.array.splice(e,1),!!~e}})[g]}var o=t("./$"),i=t("./$.uid").safe,s=t("./$.assert"),c=t("./$.iter").forOf,u=o.has,a=o.isObject,f=o.hide,l=Object.isFrozen||o.core.Object.isFrozen,h=0,d=i("id"),p=i("weak"),g=i("leak"),$=t("./$.array-methods"),v=$(5),y=$(6);e.exports={getConstructor:function(t,e,n){function i(r){o.set(s.inst(this,i,t),d,h++),void 0!=r&&c(r,e,this[n],this)}return o.mix(i.prototype,{"delete":function(t){return a(t)?l(t)?r(this)["delete"](t):u(t,p)&&u(t[p],this[d])&&delete t[p][this[d]]:!1},has:function(t){return a(t)?l(t)?r(this).has(t):u(t,p)&&u(t[p],this[d]):!1}}),i},def:function(t,e,n){return l(s.obj(e))?r(t).set(e,n):(u(e,p)||f(e,p,{}),e[p][t[d]]=n),t},leakStore:r,WEAK:p,ID:d}},{"./$":16,"./$.array-methods":4,"./$.assert":5,"./$.iter":15,"./$.uid":24}],10:[function(t,e){"use strict";var n=t("./$"),r=t("./$.def"),o=t("./$.iter"),i=t("./$.assert").inst;e.exports=function(e,s,c,u,a){function f(t,e){var r=p[t];n.FW&&(p[t]=function(t,n){var o=r.call(this,0===t?0:t,n);return e?this:o})}var l=n.g[e],h=l,d=u?"set":"add",p=h&&h.prototype,g={};if(n.isFunction(h)&&(a||!o.BUGGY&&p.forEach&&p.entries)){var $,v=new h,y=v[d](a?{}:-0,1);(o.fail(function(t){new h(t)})||o.DANGER_CLOSING)&&(h=function(t){i(this,h,e);var n=new l;return void 0!=t&&o.forOf(t,u,n[d],n),n},h.prototype=p,n.FW&&(p.constructor=h)),a||v.forEach(function(t,e){$=1/e===-1/0}),$&&(f("delete"),f("has"),u&&f("get")),($||y!==v)&&f(d,!0)}else h=c.getConstructor(e,u,d),n.mix(h.prototype,s);return t("./$.cof").set(h,e),t("./$.species")(h),g[e]=h,r(r.G+r.W+r.F*(h!=l),g),a||o.std(h,e,c.getIterConstructor(),c.next,u?"key+value":"value",!u,!0),h}},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.def":12,"./$.iter":15,"./$.species":21}],11:[function(t,e){var n=t("./$.assert").fn;e.exports=function(t,e,r){if(n(t),~r&&void 0===e)return t;switch(r){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},{"./$.assert":5}],12:[function(t,e){function n(t,e){return function(){return t.apply(e,arguments)}}function r(t,e,u){var a,f,l,h,d=t&r.G,p=d?i:t&r.S?i[e]:(i[e]||{}).prototype,g=d?s:s[e]||(s[e]={});d&&(u=e);for(a in u)f=!(t&r.F)&&p&&a in p,l=(f?p:u)[a],h=t&r.B&&f?n(l,i):t&r.P&&c(l)?n(Function.call,l):l,p&&!f&&(d?p[a]=l:delete p[a]&&o.hide(p,a,l)),g[a]!=l&&o.hide(g,a,h)}var o=t("./$"),i=o.g,s=o.core,c=o.isFunction;i.core=s,r.F=1,r.G=2,r.S=4,r.P=8,r.B=16,r.W=32,e.exports=r},{"./$":16}],13:[function(t,e){e.exports=function(t){return t.FW=!0,t.path=t.g,t}},{}],14:[function(t,e){e.exports=function(t,e,n){var r=void 0===n;switch(e.length){case 0:return r?t():t.call(n);case 1:return r?t(e[0]):t.call(n,e[0]);case 2:return r?t(e[0],e[1]):t.call(n,e[0],e[1]);case 3:return r?t(e[0],e[1],e[2]):t.call(n,e[0],e[1],e[2]);case 4:return r?t(e[0],e[1],e[2],e[3]):t.call(n,e[0],e[1],e[2],e[3]);case 5:return r?t(e[0],e[1],e[2],e[3],e[4]):t.call(n,e[0],e[1],e[2],e[3],e[4])}return t.apply(n,e)}},{}],15:[function(t,e){"use strict";function n(t,e){c.hide(t,h,e),d in[]&&c.hide(t,d,e)}function r(t,e,r,o){var i=t.prototype,s=i[h]||i[d]||o&&i[o]||r;if(c.FW&&n(i,s),s!==r){var u=c.getProto(s.call(new t));a.set(u,e+" Iterator",!0),c.FW&&c.has(i,d)&&n(u,c.that)}return p[e]=s,p[e+" Iterator"]=c.that,s}function o(t){var e=c.g.Symbol,n=t[e&&e.iterator||d],r=n||t[h]||p[a.classof(t)];return l(r.call(t))}function i(t){var e=t["return"];void 0!==e&&l(e.call(t))}function s(t,e,n,r){try{return r?e(l(n)[0],n[1]):e(n)}catch(o){throw i(t),o}}var c=t("./$"),u=t("./$.ctx"),a=t("./$.cof"),f=t("./$.def"),l=t("./$.assert").obj,h=t("./$.wks")("iterator"),d="@@iterator",p={},g={},$="keys"in[]&&!("next"in[].keys());n(g,c.that);var v=!0;!function(){try{var t=[1].keys();t["return"]=function(){v=!1},Array.from(t,function(){throw 2})}catch(e){}}();var y=e.exports={BUGGY:$,DANGER_CLOSING:v,fail:function(t){var e=!0;try{var n=[[{},1]],r=n[h](),o=r.next;r.next=function(){return e=!1,o.call(this)},n[h]=function(){return r},t(n)}catch(i){}return e},Iterators:p,prototype:g,step:function(t,e){return{value:e,done:!!t}},stepCall:s,close:i,is:function(t){var e=Object(t),n=c.g.Symbol,r=n&&n.iterator||d;return r in e||h in e||c.has(p,a.classof(e))},get:o,set:n,create:function(t,e,n,r){t.prototype=c.create(r||y.prototype,{next:c.desc(1,n)}),a.set(t,e+" Iterator")},define:r,std:function(t,e,n,o,i,s,u){function a(t){return function(){return new n(this,t)}}y.create(n,e,o);var l,h,d=a("key+value"),p=a("value"),g=t.prototype;if("value"==i?p=r(t,e,p,"values"):d=r(t,e,d,"entries"),i&&(l={entries:d,keys:s?p:a("key"),values:p},f(f.P+f.F*$,e,l),u))for(h in l)h in g||c.hide(g,h,l[h])},forOf:function(t,e,n,r){for(var c,a=o(t),f=u(n,r,e?2:1);!(c=a.next()).done;)if(s(a,f,c.value,e)===!1)return i(a)}}},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.ctx":11,"./$.def":12,"./$.wks":26}],16:[function(t,e){"use strict";function n(t){return isNaN(t=+t)?0:(t>0?p:d)(t)}function r(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}function o(t,e,n){return t[e]=n,t}function i(t){return v?function(e,n,o){return m.setDesc(e,n,r(t,o))}:o}function s(t){return null!==t&&("object"==typeof t||"function"==typeof t)}function c(t){return"function"==typeof t}function u(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}var a="undefined"!=typeof self?self:Function("return this")(),f={},l=Object.defineProperty,h={}.hasOwnProperty,d=Math.ceil,p=Math.floor,g=Math.max,$=Math.min,v=!!function(){try{return 2==l({},"a",{get:function(){return 2}}).a}catch(t){}}(),y=i(1),m=e.exports=t("./$.fw")({g:a,core:f,html:a.document&&document.documentElement,isObject:s,isFunction:c,it:function(t){return t},that:function(){return this},toInteger:n,toLength:function(t){return t>0?$(n(t),9007199254740991):0},toIndex:function(t,e){return t=n(t),0>t?g(t+e,0):$(t,e)},has:function(t,e){return h.call(t,e)},create:Object.create,getProto:Object.getPrototypeOf,DESC:v,desc:r,getDesc:Object.getOwnPropertyDescriptor,setDesc:l,getKeys:Object.keys,getNames:Object.getOwnPropertyNames,getSymbols:Object.getOwnPropertySymbols,assertDefined:u,ES5Object:Object,toObject:function(t){return m.ES5Object(u(t))},hide:y,def:i(0),set:a.Symbol?o:y,mix:function(t,e){for(var n in e)y(t,n,e[n]);return t},each:[].forEach});"undefined"!=typeof __e&&(__e=f),"undefined"!=typeof __g&&(__g=a)},{"./$.fw":13}],17:[function(t,e){var n=t("./$");e.exports=function(t,e){for(var r,o=n.toObject(t),i=n.getKeys(o),s=i.length,c=0;s>c;)if(o[r=i[c++]]===e)return r}},{"./$":16}],18:[function(t,e){var n=t("./$"),r=t("./$.assert").obj;e.exports=function(t){return r(t),n.getSymbols?n.getNames(t).concat(n.getSymbols(t)):n.getNames(t)}},{"./$":16,"./$.assert":5}],19:[function(t,e){"use strict";e.exports=function(t,e,n){var r=e===Object(e)?function(t){return e[t]}:e;return function(e){return String(n?e:this).replace(t,r)}}},{}],20:[function(t,e){var n=t("./$"),r=t("./$.assert");e.exports=Object.setPrototypeOf||("__proto__"in{}?function(e,o){try{o=t("./$.ctx")(Function.call,n.getDesc(Object.prototype,"__proto__").set,2),o({},[])}catch(i){e=!0}return function(t,i){return r.obj(t),r(null===i||n.isObject(i),i,": can't set as prototype!"),e?t.__proto__=i:o(t,i),t}}():void 0)},{"./$":16,"./$.assert":5,"./$.ctx":11}],21:[function(t,e){var n=t("./$");e.exports=function(e){n.DESC&&n.FW&&n.setDesc(e,t("./$.wks")("species"),{configurable:!0,get:n.that})}},{"./$":16,"./$.wks":26}],22:[function(t,e){"use strict";var n=t("./$");e.exports=function(t){return function(e){var r,o,i=String(n.assertDefined(this)),s=n.toInteger(e),c=i.length;return 0>s||s>=c?t?"":void 0:(r=i.charCodeAt(s),55296>r||r>56319||s+1===c||(o=i.charCodeAt(s+1))<56320||o>57343?t?i.charAt(s):r:t?i.slice(s,s+2):(r-55296<<10)+(o-56320)+65536)}}},{"./$":16}],23:[function(t,e){"use strict";function n(){var t=+this;if(c.has(m,t)){var e=m[t];delete m[t],e()}}function r(t){n.call(t.data)}var o,i,s,c=t("./$"),u=t("./$.ctx"),a=t("./$.cof"),f=t("./$.invoke"),l=c.g,h=c.isFunction,d=l.setImmediate,p=l.clearImmediate,g=l.postMessage,$=l.addEventListener,v=l.MessageChannel,y=0,m={},b="onreadystatechange";h(d)&&h(p)||(d=function(t){for(var e=[],n=1;arguments.length>n;)e.push(arguments[n++]);return m[++y]=function(){f(h(t)?t:Function(t),e)},o(y),y},p=function(t){delete m[t]},"process"==a(l.process)?o=function(t){l.process.nextTick(u(n,t,1))}:$&&h(g)&&!c.g.importScripts?(o=function(t){g(t,"*")},$("message",r,!1)):h(v)?(i=new v,s=i.port2,i.port1.onmessage=r,o=u(s.postMessage,s,1)):o=c.g.document&&b in document.createElement("script")?function(t){c.html.appendChild(document.createElement("script"))[b]=function(){c.html.removeChild(this),n.call(t)}}:function(t){setTimeout(u(n,t,1),0)}),e.exports={set:d,clear:p}},{"./$":16,"./$.cof":7,"./$.ctx":11,"./$.invoke":14}],24:[function(t,e){function n(t){return"Symbol("+t+")_"+(++r+Math.random()).toString(36)}var r=0;n.safe=t("./$").g.Symbol||n,e.exports=n},{"./$":16}],25:[function(t,e){var n=t("./$"),r=t("./$.wks")("unscopables");!n.FW||r in[]||n.hide(Array.prototype,r,{}),e.exports=function(t){n.FW&&([][r][t]=!0)}},{"./$":16,"./$.wks":26}],26:[function(t,e){var n=t("./$").g,r={};e.exports=function(e){return r[e]||(r[e]=n.Symbol&&n.Symbol[e]||t("./$.uid").safe("Symbol."+e))}},{"./$":16,"./$.uid":24}],27:[function(t){"use strict";var e=t("./$"),n=t("./$.def"),r=e.toIndex;n(n.P,"Array",{copyWithin:function(t,n){var o=Object(e.assertDefined(this)),i=e.toLength(o.length),s=r(t,i),c=r(n,i),u=arguments[2],a=void 0===u?i:r(u,i),f=Math.min(a-c,i-s),l=1;for(s>c&&c+f>s&&(l=-1,c=c+f-1,s=s+f-1);f-->0;)c in o?o[s]=o[c]:delete o[s],s+=l,c+=l;return o}}),t("./$.unscope")("copyWithin")},{"./$":16,"./$.def":12,"./$.unscope":25}],28:[function(t){"use strict";var e=t("./$"),n=t("./$.def"),r=e.toIndex;n(n.P,"Array",{fill:function(t){for(var n=Object(e.assertDefined(this)),o=e.toLength(n.length),i=r(arguments[1],o),s=arguments[2],c=void 0===s?o:r(s,o);c>i;)n[i++]=t;return n}}),t("./$.unscope")("fill")},{"./$":16,"./$.def":12,"./$.unscope":25}],29:[function(t){var e=t("./$.def");e(e.P,"Array",{findIndex:t("./$.array-methods")(6)}),t("./$.unscope")("findIndex")},{"./$.array-methods":4,"./$.def":12,"./$.unscope":25}],30:[function(t){var e=t("./$.def");e(e.P,"Array",{find:t("./$.array-methods")(5)}),t("./$.unscope")("find")},{"./$.array-methods":4,"./$.def":12,"./$.unscope":25}],31:[function(t){var e=t("./$"),n=t("./$.ctx"),r=t("./$.def"),o=t("./$.iter"),i=o.stepCall;r(r.S+r.F*o.DANGER_CLOSING,"Array",{from:function(t){var r,s,c,u,a=Object(e.assertDefined(t)),f=arguments[1],l=void 0!==f,h=l?n(f,arguments[2],2):void 0,d=0;if(o.is(a))for(u=o.get(a),s=new("function"==typeof this?this:Array);!(c=u.next()).done;d++)s[d]=l?i(u,h,[c.value,d],!0):c.value;else for(s=new("function"==typeof this?this:Array)(r=e.toLength(a.length));r>d;d++)s[d]=l?h(a[d],d):a[d];return s.length=d,s}})},{"./$":16,"./$.ctx":11,"./$.def":12,"./$.iter":15}],32:[function(t){var e=t("./$"),n=t("./$.unscope"),r=t("./$.uid").safe("iter"),o=t("./$.iter"),i=o.step,s=o.Iterators;o.std(Array,"Array",function(t,n){e.set(this,r,{o:e.toObject(t),i:0,k:n})},function(){var t=this[r],e=t.o,n=t.k,o=t.i++;return!e||o>=e.length?(t.o=void 0,i(1)):"key"==n?i(0,o):"value"==n?i(0,e[o]):i(0,[o,e[o]])},"value"),s.Arguments=s.Array,n("keys"),n("values"),n("entries")},{"./$":16,"./$.iter":15,"./$.uid":24,"./$.unscope":25}],33:[function(t){var e=t("./$.def");e(e.S,"Array",{of:function(){for(var t=0,e=arguments.length,n=new("function"==typeof this?this:Array)(e);e>t;)n[t]=arguments[t++];return n.length=e,n}})},{"./$.def":12}],34:[function(t){t("./$.species")(Array)},{"./$.species":21}],35:[function(t){"use strict";var e=t("./$"),n="name",r=e.setDesc,o=Function.prototype;n in o||e.FW&&e.DESC&&r(o,n,{configurable:!0,get:function(){var t=String(this).match(/^\s*function ([^ (]*)/),o=t?t[1]:"";return e.has(this,n)||r(this,n,e.desc(5,o)),o},set:function(t){e.has(this,n)||r(this,n,e.desc(0,t))}})},{"./$":16}],36:[function(t){"use strict";var e=t("./$.collection-strong");t("./$.collection")("Map",{get:function(t){var n=e.getEntry(this,t);return n&&n.v},set:function(t,n){return e.def(this,0===t?0:t,n)}},e,!0)},{"./$.collection":10,"./$.collection-strong":8}],37:[function(t){function e(t){return isFinite(t=+t)&&0!=t?0>t?-e(-t):a(t+f(t*t+1)):t}function n(t){return 0==(t=+t)?t:t>-1e-6&&1e-6>t?t+t*t/2:u(t)-1}var r=1/0,o=t("./$.def"),i=Math.E,s=Math.pow,c=Math.abs,u=Math.exp,a=Math.log,f=Math.sqrt,l=Math.ceil,h=Math.floor,d=Math.sign||function(t){return 0==(t=+t)||t!=t?t:0>t?-1:1};o(o.S,"Math",{acosh:function(t){return(t=+t)<1?0/0:isFinite(t)?a(t/i+f(t+1)*f(t-1)/i)+1:t},asinh:e,atanh:function(t){return 0==(t=+t)?t:a((1+t)/(1-t))/2},cbrt:function(t){return d(t=+t)*s(c(t),1/3)},clz32:function(t){return(t>>>=0)?32-t.toString(2).length:32},cosh:function(t){return(u(t=+t)+u(-t))/2},expm1:n,fround:function(t){return new Float32Array([t])[0]},hypot:function(){for(var t,e=0,n=arguments.length,o=n,i=Array(n),c=-r;n--;){if(t=i[n]=+arguments[n],t==r||t==-r)return r;t>c&&(c=t)}for(c=t||1;o--;)e+=s(i[o]/c,2);return c*f(e)},imul:function(t,e){var n=65535,r=+t,o=+e,i=n&r,s=n&o;return 0|i*s+((n&r>>>16)*s+i*(n&o>>>16)<<16>>>0)},log1p:function(t){return(t=+t)>-1e-8&&1e-8>t?t-t*t/2:a(1+t)},log10:function(t){return a(t)/Math.LN10},log2:function(t){return a(t)/Math.LN2},sign:d,sinh:function(t){return c(t=+t)<1?(n(t)-n(-t))/2:(u(t-1)-u(-t-1))*(i/2)},tanh:function(t){var e=n(t=+t),o=n(-t);return e==r?1:o==r?-1:(e-o)/(u(t)+u(-t))},trunc:function(t){return(t>0?h:l)(t)}})},{"./$.def":12}],38:[function(t){"use strict";function e(t){var e,n;if(i(e=t.valueOf)&&!o(n=e.call(t)))return n;if(i(e=t.toString)&&!o(n=e.call(t)))return n;throw TypeError("Can't convert object to number")}function n(t){if(o(t)&&(t=e(t)),"string"==typeof t&&t.length>2&&48==t.charCodeAt(0)){var n=!1;switch(t.charCodeAt(1)){case 66:case 98:n=!0;case 79:case 111:return parseInt(t.slice(2),n?2:8)}}return+t}var r=t("./$"),o=r.isObject,i=r.isFunction,s="Number",c=r.g[s],u=c,a=c.prototype;!r.FW||c("0o1")&&c("0b1")||(c=function f(t){return this instanceof f?new u(n(t)):n(t)},r.each.call(r.DESC?r.getNames(u):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),function(t){r.has(u,t)&&!r.has(c,t)&&r.setDesc(c,t,r.getDesc(u,t))}),c.prototype=a,a.constructor=c,r.hide(r.g,s,c))},{"./$":16}],39:[function(t){function e(t){return!n.isObject(t)&&isFinite(t)&&i(t)===t}var n=t("./$"),r=t("./$.def"),o=Math.abs,i=Math.floor,s=9007199254740991;r(r.S,"Number",{EPSILON:Math.pow(2,-52),isFinite:function(t){return"number"==typeof t&&isFinite(t)},isInteger:e,isNaN:function(t){return t!=t},isSafeInteger:function(t){return e(t)&&o(t)<=s},MAX_SAFE_INTEGER:s,MIN_SAFE_INTEGER:-s,parseFloat:parseFloat,parseInt:parseInt})},{"./$":16,"./$.def":12}],40:[function(t){var e=t("./$.def");e(e.S,"Object",{assign:t("./$.assign")})},{"./$.assign":6,"./$.def":12}],41:[function(t){var e=t("./$.def");e(e.S,"Object",{is:function(t,e){return t===e?0!==t||1/t===1/e:t!=t&&e!=e}})},{"./$.def":12}],42:[function(t){var e=t("./$.def");e(e.S,"Object",{setPrototypeOf:t("./$.set-proto")})},{"./$.def":12,"./$.set-proto":20}],43:[function(t){function e(t,e){var s=(n.core.Object||{})[t]||Object[t],c=0,u={};u[t]=1==e?function(t){return o(t)?s(t):t}:2==e?function(t){return o(t)?s(t):!0}:3==e?function(t){return o(t)?s(t):!1}:4==e?function(t,e){return s(i(t),e)}:5==e?function(t){return s(Object(n.assertDefined(t)))}:function(t){return s(i(t))};try{s("z")}catch(a){c=1}r(r.S+r.F*c,"Object",u)}var n=t("./$"),r=t("./$.def"),o=n.isObject,i=n.toObject;e("freeze",1),e("seal",1),e("preventExtensions",1),e("isFrozen",2),e("isSealed",2),e("isExtensible",3),e("getOwnPropertyDescriptor",4),e("getPrototypeOf",5),e("keys"),e("getOwnPropertyNames")},{"./$":16,"./$.def":12}],44:[function(t){"use strict";var e=t("./$"),n=t("./$.cof"),r={};r[t("./$.wks")("toStringTag")]="z",e.FW&&"z"!=n(r)&&e.hide(Object.prototype,"toString",function(){return"[object "+n.classof(this)+"]"})},{"./$":16,"./$.cof":7,"./$.wks":26}],45:[function(t){"use strict";function e(t){var e=w(t)[a];return void 0!=e?e:t}var n,r=t("./$"),o=t("./$.ctx"),i=t("./$.cof"),s=t("./$.def"),c=t("./$.assert"),u=t("./$.iter"),a=t("./$.wks")("species"),f=t("./$.uid").safe("record"),l=u.forOf,h="Promise",d=r.g,p=d.process,g=p&&p.nextTick||t("./$.task").set,$=d[h],v=$,y=r.isFunction,m=r.isObject,b=c.fn,w=c.obj;y($)&&y($.resolve)&&$.resolve(n=new $(function(){}))==n||function(){function t(t){var e;return m(t)&&(e=t.then),y(e)?e:!1}function e(t){var n,r=t[f],o=r.c,i=0;if(r.h)return!0;for(;o.length>i;)if(n=o[i++],n.fail||e(n.P))return!0}function n(n,r){var o=n.c;(r||o.length)&&g(function(){var s=n.p,c=n.v,u=1==n.s,a=0;if(r&&!e(s))setTimeout(function(){e(s)||("process"==i(p)?p.emit("unhandledRejection",c,s):d.console&&y(console.error)&&console.error("Unhandled promise rejection",c))},1e3);else for(;o.length>a;)!function(e){var r,o,i=u?e.ok:e.fail;try{i?(u||(n.h=!0),r=i===!0?c:i(c),r===e.P?e.rej(TypeError(h+"-chain cycle")):(o=t(r))?o.call(r,e.res,e.rej):e.res(r)):e.rej(c)}catch(s){e.rej(s)}}(o[a++]);o.length=0})}function s(t){var e=this;e.d||(e.d=!0,e=e.r||e,e.v=t,e.s=2,n(e,!0))}function u(e){var r,i,c=this;if(!c.d){c.d=!0,c=c.r||c;try{(r=t(e))?(i={r:c,d:!1},r.call(e,o(u,i,1),o(s,i,1))):(c.v=e,c.s=1,n(c))}catch(a){s.call(i||{r:c,d:!1},a)}}}$=function(t){b(t);var e={p:c.inst(this,$,h),c:[],s:0,d:!1,v:void 0,h:!1};r.hide(this,f,e);try{t(o(u,e,1),o(s,e,1))}catch(n){s.call(e,n)}},r.mix($.prototype,{then:function(t,e){var r=w(w(this).constructor)[a],o={ok:y(t)?t:!0,fail:y(e)?e:!1},i=o.P=new(void 0!=r?r:$)(function(t,e){o.res=b(t),o.rej=b(e)}),s=this[f];return s.c.push(o),s.s&&n(s),i},"catch":function(t){return this.then(void 0,t)}})}(),s(s.G+s.W+s.F*($!=v),{Promise:$}),s(s.S,h,{reject:function(t){return new(e(this))(function(e,n){n(t)})},resolve:function(t){return m(t)&&f in t&&r.getProto(t)===this.prototype?t:new(e(this))(function(e){e(t)})}}),s(s.S+s.F*(u.fail(function(t){$.all(t)["catch"](function(){})})||u.DANGER_CLOSING),h,{all:function(t){var n=e(this),o=[];return new n(function(e,i){l(t,!1,o.push,o);var s=o.length,c=Array(s);s?r.each.call(o,function(t,r){n.resolve(t).then(function(t){c[r]=t,--s||e(c)},i)}):e(c)})},race:function(t){var n=e(this);return new n(function(e,r){l(t,!1,function(t){n.resolve(t).then(e,r)})})}}),i.set($,h),t("./$.species")($)},{"./$":16,"./$.assert":5,"./$.cof":7,"./$.ctx":11,"./$.def":12,"./$.iter":15,"./$.species":21,"./$.task":23,"./$.uid":24,"./$.wks":26}],46:[function(t){function e(t){var e,n=[];for(e in t)n.push(e);i.set(this,a,{o:t,a:n,i:0})}function n(t){return function(e){v(e);try{return t.apply(void 0,arguments),!0}catch(n){return!1}}}function r(t,e){var n,o=arguments.length<3?t:arguments[2],s=d(v(t),e);return s?i.has(s,"value")?s.value:void 0===s.get?void 0:s.get.call(o):h(n=g(t))?r(n,e,o):void 0}function o(t,e,n){var r,s,c=arguments.length<4?t:arguments[3],u=d(v(t),e);if(!u){if(h(s=g(t)))return o(s,e,n,c);u=i.desc(0)}return i.has(u,"value")?u.writable!==!1&&h(c)?(r=d(c,e)||i.desc(0),r.value=n,p(c,e,r),!0):!1:void 0===u.set?!1:(u.set.call(c,n),!0)}var i=t("./$"),s=t("./$.def"),c=t("./$.set-proto"),u=t("./$.iter"),a=t("./$.uid").safe("iter"),f=u.step,l=t("./$.assert"),h=i.isObject,d=i.getDesc,p=i.setDesc,g=i.getProto,$=Function.apply,v=l.obj,y=Object.isExtensible||i.it;u.create(e,"Object",function(){var t,e=this[a],n=e.a;do if(e.i>=n.length)return f(1);while(!((t=n[e.i++])in e.o));return f(0,t)});var m={apply:t("./$.ctx")(Function.call,$,3),construct:function(t,e){var n=l.fn(arguments.length<3?t:arguments[2]).prototype,r=i.create(h(n)?n:Object.prototype),o=$.call(t,r,e);return h(o)?o:r},defineProperty:n(p),deleteProperty:function(t,e){var n=d(v(t),e);return n&&!n.configurable?!1:delete t[e]},enumerate:function(t){return new e(v(t))},get:r,getOwnPropertyDescriptor:function(t,e){return d(v(t),e)},getPrototypeOf:function(t){return g(v(t))},has:function(t,e){return e in t},isExtensible:function(t){return!!y(v(t))},ownKeys:t("./$.own-keys"),preventExtensions:n(Object.preventExtensions||i.it),set:o};c&&(m.setPrototypeOf=function(t,e){return c(v(t),e),!0}),s(s.G,{Reflect:{}}),s(s.S,"Reflect",m)},{"./$":16,"./$.assert":5,"./$.ctx":11,"./$.def":12,"./$.iter":15,"./$.own-keys":18,"./$.set-proto":20,"./$.uid":24}],47:[function(t){var e=t("./$"),n=t("./$.cof"),r=e.g.RegExp,o=r,i=r.prototype;e.FW&&e.DESC&&(function(){try{return"/a/i"==r(/a/g,"i")}catch(t){}}()||(r=function(t,e){return new o("RegExp"==n(t)&&void 0!==e?t.source:t,e)},e.each.call(e.getNames(o),function(t){t in r||e.setDesc(r,t,{configurable:!0,get:function(){return o[t]},set:function(e){o[t]=e}})}),i.constructor=r,r.prototype=i,e.hide(e.g,"RegExp",r)),"g"!=/./g.flags&&e.setDesc(i,"flags",{configurable:!0,get:t("./$.replacer")(/^.*\/(\w*)$/,"$1")})),t("./$.species")(r)},{"./$":16,"./$.cof":7,"./$.replacer":19,"./$.species":21}],48:[function(t){"use strict";var e=t("./$.collection-strong");t("./$.collection")("Set",{add:function(t){return e.def(this,t=0===t?0:t,t)}},e)},{"./$.collection":10,"./$.collection-strong":8}],49:[function(t){var e=t("./$.def");e(e.P,"String",{codePointAt:t("./$.string-at")(!1)})},{"./$.def":12,"./$.string-at":22}],50:[function(t){"use strict";var e=t("./$"),n=t("./$.cof"),r=t("./$.def"),o=e.toLength;r(r.P,"String",{endsWith:function(t){if("RegExp"==n(t))throw TypeError();var r=String(e.assertDefined(this)),i=arguments[1],s=o(r.length),c=void 0===i?s:Math.min(o(i),s);return t+="",r.slice(c-t.length,c)===t}})},{"./$":16,"./$.cof":7,"./$.def":12}],51:[function(t){var e=t("./$.def"),n=t("./$").toIndex,r=String.fromCharCode;e(e.S,"String",{fromCodePoint:function(){for(var t,e=[],o=arguments.length,i=0;o>i;){if(t=+arguments[i++],n(t,1114111)!==t)throw RangeError(t+" is not a valid code point");e.push(65536>t?r(t):r(((t-=65536)>>10)+55296,t%1024+56320))}return e.join("")}})},{"./$":16,"./$.def":12}],52:[function(t){"use strict";var e=t("./$"),n=t("./$.cof"),r=t("./$.def");r(r.P,"String",{includes:function(t){if("RegExp"==n(t))throw TypeError();return!!~String(e.assertDefined(this)).indexOf(t,arguments[1])}})},{"./$":16,"./$.cof":7,"./$.def":12}],53:[function(t){var e=t("./$").set,n=t("./$.string-at")(!0),r=t("./$.uid").safe("iter"),o=t("./$.iter"),i=o.step;o.std(String,"String",function(t){e(this,r,{o:String(t),i:0})},function(){var t,e=this[r],o=e.o,s=e.i;return s>=o.length?i(1):(t=n.call(o,s),e.i+=t.length,i(0,t))})},{"./$":16,"./$.iter":15,"./$.string-at":22,"./$.uid":24}],54:[function(t){var e=t("./$"),n=t("./$.def");n(n.S,"String",{raw:function(t){for(var n=e.toObject(t.raw),r=e.toLength(n.length),o=arguments.length,i=[],s=0;r>s;)i.push(String(n[s++])),o>s&&i.push(String(arguments[s]));return i.join("")}})},{"./$":16,"./$.def":12}],55:[function(t){"use strict";var e=t("./$"),n=t("./$.def");n(n.P,"String",{repeat:function(t){var n=String(e.assertDefined(this)),r="",o=e.toInteger(t);if(0>o||1/0==o)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(n+=n))1&o&&(r+=n);return r}})},{"./$":16,"./$.def":12}],56:[function(t){"use strict";var e=t("./$"),n=t("./$.cof"),r=t("./$.def");r(r.P,"String",{startsWith:function(t){if("RegExp"==n(t))throw TypeError();var r=String(e.assertDefined(this)),o=e.toLength(Math.min(arguments[1],r.length));return t+="",r.slice(o,o+t.length)===t}})},{"./$":16,"./$.cof":7,"./$.def":12}],57:[function(t){"use strict";function e(t){var e=$[t]=n.set(n.create(l.prototype),p,t);return n.DESC&&d&&n.setDesc(Object.prototype,t,{configurable:!0,set:function(e){u(this,t,e)}}),e}var n=t("./$"),r=t("./$.cof").set,o=t("./$.uid"),i=t("./$.def"),s=t("./$.keyof"),c=n.has,u=n.hide,a=n.getNames,f=n.toObject,l=n.g.Symbol,h=l,d=!1,p=o.safe("tag"),g={},$={};n.isFunction(l)||(l=function(t){if(this instanceof l)throw TypeError("Symbol is not a constructor");return e(o(t))},u(l.prototype,"toString",function(){return this[p]})),i(i.G+i.W,{Symbol:l});var v={"for":function(t){return c(g,t+="")?g[t]:g[t]=l(t)},keyFor:function(t){return s(g,t)},pure:o.safe,set:n.set,useSetter:function(){d=!0},useSimple:function(){d=!1}};n.each.call("hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),function(n){var r=t("./$.wks")(n);v[n]=l===h?r:e(r)}),d=!0,i(i.S,"Symbol",v),i(i.S+i.F*(l!=h),"Object",{getOwnPropertyNames:function(t){for(var e,n=a(f(t)),r=[],o=0;n.length>o;)c($,e=n[o++])||r.push(e);return r},getOwnPropertySymbols:function(t){for(var e,n=a(f(t)),r=[],o=0;n.length>o;)c($,e=n[o++])&&r.push($[e]);return r}}),r(l,"Symbol"),r(Math,"Math",!0),r(n.g.JSON,"JSON",!0)},{"./$":16,"./$.cof":7,"./$.def":12,"./$.keyof":17,"./$.uid":24,"./$.wks":26}],58:[function(t){"use strict";var e=t("./$"),n=t("./$.collection-weak"),r=n.leakStore,o=n.ID,i=n.WEAK,s=e.has,c=e.isObject,u=Object.isFrozen||e.core.Object.isFrozen,a={},f=t("./$.collection")("WeakMap",{get:function(t){if(c(t)){if(u(t))return r(this).get(t);if(s(t,i))return t[i][this[o]]}},set:function(t,e){return n.def(this,t,e)}},n,!0,!0);e.FW&&7!=(new f).set((Object.freeze||Object)(a),7).get(a)&&e.each.call(["delete","has","get","set"],function(t){var e=f.prototype[t];
+f.prototype[t]=function(n,o){if(c(n)&&u(n)){var i=r(this)[t](n,o);return"set"==t?this:i}return e.call(this,n,o)}})},{"./$":16,"./$.collection":10,"./$.collection-weak":9}],59:[function(t){"use strict";var e=t("./$.collection-weak");t("./$.collection")("WeakSet",{add:function(t){return e.def(this,t,!0)}},e,!1,!0)},{"./$.collection":10,"./$.collection-weak":9}],60:[function(t,e){(function(t){!function(t){"use strict";function n(t,e,n,r){return new s(t,e,n||null,r||[])}function r(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(r){return{type:"throw",arg:r}}}function o(){}function i(){}function s(t,e,n,o){function i(e,o){if(u===m)throw new Error("Generator is already running");if(u===b)return l();for(;;){var i=c.delegate;if(i){var s=r(i.iterator[e],i.iterator,o);if("throw"===s.type){c.delegate=null,e="throw",o=s.arg;continue}e="next",o=h;var a=s.arg;if(!a.done)return u=y,a;c[i.resultName]=a.value,c.next=i.nextLoc,c.delegate=null}if("next"===e){if(u===v&&"undefined"!=typeof o)throw new TypeError("attempt to send "+JSON.stringify(o)+" to newborn generator");u===y?c.sent=o:delete c.sent}else if("throw"===e){if(u===v)throw u=b,o;c.dispatchException(o)&&(e="next",o=h)}else"return"===e&&c.abrupt("return",o);u=m;var s=r(t,n,c);if("normal"===s.type){u=c.done?b:y;var a={value:s.arg,done:c.done};if(s.arg!==w)return a;c.delegate&&"next"===e&&(o=h)}else"throw"===s.type&&(u=b,"next"===e?c.dispatchException(s.arg):o=s.arg)}}var s=e?Object.create(e.prototype):this,c=new a(o),u=v;return s.next=i.bind(s,"next"),s["throw"]=i.bind(s,"throw"),s["return"]=i.bind(s,"return"),s}function c(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function u(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function a(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(c,this),this.reset()}function f(t){if(t){var e=t[p];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,r=function o(){for(;++n<t.length;)if(d.call(t,n))return o.value=t[n],o.done=!1,o;return o.value=h,o.done=!0,o};return r.next=r}}return{next:l}}function l(){return{value:h,done:!0}}var h,d=Object.prototype.hasOwnProperty,p="function"==typeof Symbol&&Symbol.iterator||"@@iterator",g="object"==typeof e,$=t.regeneratorRuntime;if($)return void(g&&(e.exports=$));$=t.regeneratorRuntime=g?e.exports:{},$.wrap=n;var v="suspendedStart",y="suspendedYield",m="executing",b="completed",w={},x=i.prototype=s.prototype;o.prototype=x.constructor=i,i.constructor=o,o.displayName="GeneratorFunction",$.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return e?e===o||"GeneratorFunction"===(e.displayName||e.name):!1},$.mark=function(t){return t.__proto__=i,t.prototype=Object.create(x),t},$.async=function(t,e,o,i){return new Promise(function(s,c){function u(t){var e=r(this,null,t);if("throw"===e.type)return void c(e.arg);var n=e.arg;n.done?s(n.value):Promise.resolve(n.value).then(f,l)}var a=n(t,e,o,i),f=u.bind(a.next),l=u.bind(a["throw"]);f()})},x[p]=function(){return this},x.toString=function(){return"[object Generator]"},$.keys=function(t){var e=[];for(var n in t)e.push(n);return e.reverse(),function r(){for(;e.length;){var n=e.pop();if(n in t)return r.value=n,r.done=!1,r}return r.done=!0,r}},$.values=f,a.prototype={constructor:a,reset:function(){this.prev=0,this.next=0,this.sent=h,this.done=!1,this.delegate=null,this.tryEntries.forEach(u);for(var t,e=0;d.call(this,t="t"+e)||20>e;++e)this[t]=null},stop:function(){this.done=!0;var t=this.tryEntries[0],e=t.completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(t){function e(e,r){return i.type="throw",i.arg=t,n.next=e,!!r}if(this.done)throw t;for(var n=this,r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r],i=o.completion;if("root"===o.tryLoc)return e("end");if(o.tryLoc<=this.prev){var s=d.call(o,"catchLoc"),c=d.call(o,"finallyLoc");if(s&&c){if(this.prev<o.catchLoc)return e(o.catchLoc,!0);if(this.prev<o.finallyLoc)return e(o.finallyLoc)}else if(s){if(this.prev<o.catchLoc)return e(o.catchLoc,!0)}else{if(!c)throw new Error("try statement without catch or finally");if(this.prev<o.finallyLoc)return e(o.finallyLoc)}}}},abrupt:function(t,e){for(var n=this.tryEntries.length-1;n>=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&d.call(r,"finallyLoc")&&this.prev<r.finallyLoc){var o=r;break}}o&&("break"===t||"continue"===t)&&o.tryLoc<=e&&e<o.finallyLoc&&(o=null);var i=o?o.completion:{};return i.type=t,i.arg=e,o?this.next=o.finallyLoc:this.complete(i),w},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=t.arg,this.next="end"):"normal"===t.type&&e&&(this.next=e),w},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc)}},"catch":function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var o=r.arg;u(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:f(t),resultName:e,nextLoc:n},w}}}("object"==typeof t?t:"object"==typeof window?window:this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1]); \ No newline at end of file
diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html
index 574caf730..0e37a4660 100644
--- a/web/templates/admin_console.html
+++ b/web/templates/admin_console.html
@@ -4,6 +4,7 @@
<html>
{{template "head" . }}
<body>
+<script src="/static/js/Chart.min.js"></script>
<div id='error_bar'></div>
diff --git a/web/templates/channel.html b/web/templates/channel.html
index 4b8318d43..63fe38587 100644
--- a/web/templates/channel.html
+++ b/web/templates/channel.html
@@ -5,24 +5,7 @@
{{template "head" . }}
<body>
<div id="error_bar"></div>
- <div class="container-fluid">
- <div class="sidebar--right" id="sidebar-right"></div>
- <div class="sidebar--menu" id="sidebar-menu"></div>
- <div class="sidebar--left" id="sidebar-left"></div>
- <div class="inner__wrap channel__wrap">
- <div class="row header">
- <div id="navbar"></div>
- </div>
- <div class="row main">
- <div id="file_upload_overlay"></div>
- <div id="app-content" class="app__content">
- <div id="channel-header"></div>
- <div id="post-list"></div>
- <div class="post-create__container" id="post-create"></div>
- </div>
- </div>
- </div>
- </div>
+ <div id="channel_view" class="channel-view"></div>
<div id="channel_loader"></div>
<div id="post_mention_tab"></div>
<div id="reply_mention_tab"></div>
diff --git a/web/templates/head.html b/web/templates/head.html
index 041831ed7..24f9862c0 100644
--- a/web/templates/head.html
+++ b/web/templates/head.html
@@ -24,6 +24,7 @@
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/google-fonts.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">
@@ -36,6 +37,7 @@
<script src="/static/js/react-bootstrap-0.27.1.js"></script>
<script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
+ <script src="/static/js/babel-es6-polyfill.min.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
@@ -82,15 +84,14 @@
if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") {
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
analytics.load(window.mm_config.SegmentDeveloperKey);
- var user = window.UserStore.getCurrentUser(true);
- if (user) {
+ if (window.mm_user) {
analytics.identify(user.id, {
- name: user.nickname,
- email: user.email,
- createdAt: user.create_at,
- username: user.username,
- team_id: user.team_id,
- id: user.id
+ name: window.mm_user.nickname,
+ email: window.mm_user.email,
+ createdAt: window.mm_user.create_at,
+ username: window.mm_user.username,
+ team_id: window.mm_user.team_id,
+ id: window.mm_user.id
});
}
analytics.page();
diff --git a/web/web.go b/web/web.go
index 5f290ec99..51f6664b6 100644
--- a/web/web.go
+++ b/web/web.go
@@ -132,7 +132,7 @@ func watchAndParseTemplates() {
}
}
-var browsersNotSupported string = "MSIE/8;MSIE/9;Internet Explorer/8;Internet Explorer/9"
+var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7"
func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
ua := user_agent.New(r.UserAgent())
@@ -143,7 +143,7 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
version := strings.Split(browser, "/")
if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) {
- c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 10 or higher, FireFox 14 or higher", "")
+ c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 8 or higher", "")
return false
}
}
@@ -152,20 +152,6 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool {
}
-// func getTeamAndUser(c *api.Context) (*model.Team, *model.User) {
-// if tr := <-api.Srv.Store.Team().Get(c.Session.TeamId); tr.Err != nil {
-// c.Err = tr.Err
-// return nil, nil
-// } else {
-// if ur := <-api.Srv.Store.User().Get(c.Session.UserId); ur.Err != nil {
-// c.Err = ur.Err
-// return nil, nil
-// } else {
-// return tr.Data.(*model.Team), ur.Data.(*model.User)
-// }
-// }
-// }
-
func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !CheckBrowserCompatability(c, r) {
@@ -174,6 +160,22 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.UserId) == 0 {
page := NewHtmlTemplatePage("signup_team", "Signup")
+
+ if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teams := result.Data.([]*model.Team)
+ for _, team := range teams {
+ page.Props[team.Name] = team.DisplayName
+ }
+
+ if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation {
+ http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect)
+ return
+ }
+ }
+
page.Render(c, w)
} else {
teamChan := api.Srv.Store.Team().Get(c.Session.TeamId)
@@ -240,6 +242,11 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = team.Name
+
+ if team.AllowOpenInvite {
+ page.Props["InviteId"] = team.InviteId
+ }
+
page.Render(c, w)
}
@@ -285,7 +292,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
if len(id) > 0 {
props = make(map[string]string)
- if result := <-api.Srv.Store.Team().Get(id); result.Err != nil {
+ if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -969,20 +976,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
- var props map[string]string
+ var parsedRequest *model.IncomingWebhookRequest
if r.Header.Get("Content-Type") == "application/json" {
- props = model.MapFromJson(r.Body)
+ parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
} else {
- props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
+ parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
}
- text := props["text"]
+ text := parsedRequest.Text
if len(text) == 0 {
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
return
}
- channelName := props["channel"]
+ channelName := parsedRequest.ChannelName
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
@@ -1012,8 +1019,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
}
- overrideUsername := props["username"]
- overrideIconUrl := props["icon_url"]
+ overrideUsername := parsedRequest.Username
+ overrideIconUrl := parsedRequest.IconURL
if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)