summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/.eslintrc3
-rw-r--r--web/react/components/admin_console/admin_controller.jsx179
-rw-r--r--web/react/components/admin_console/admin_navbar_dropdown.jsx102
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx278
-rw-r--r--web/react/components/admin_console/admin_sidebar_header.jsx60
-rw-r--r--web/react/components/admin_console/email_settings.jsx549
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx277
-rw-r--r--web/react/components/admin_console/image_settings.jsx496
-rw-r--r--web/react/components/admin_console/log_settings.jsx295
-rw-r--r--web/react/components/admin_console/logs.jsx90
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx163
-rw-r--r--web/react/components/admin_console/rate_settings.jsx272
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx99
-rw-r--r--web/react/components/admin_console/service_settings.jsx296
-rw-r--r--web/react/components/admin_console/sql_settings.jsx283
-rw-r--r--web/react/components/admin_console/team_settings.jsx235
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/components/authorize.jsx72
-rw-r--r--web/react/components/change_url_modal.jsx177
-rw-r--r--web/react/components/channel_header.jsx19
-rw-r--r--web/react/components/channel_invite_modal.jsx2
-rw-r--r--web/react/components/channel_loader.jsx92
-rw-r--r--web/react/components/channel_members.jsx2
-rw-r--r--web/react/components/channel_notifications.jsx16
-rw-r--r--web/react/components/create_comment.jsx13
-rw-r--r--web/react/components/create_post.jsx154
-rw-r--r--web/react/components/delete_channel_modal.jsx3
-rw-r--r--web/react/components/email_verify.jsx20
-rw-r--r--web/react/components/error_bar.jsx85
-rw-r--r--web/react/components/file_attachment.jsx26
-rw-r--r--web/react/components/file_attachment_list.jsx16
-rw-r--r--web/react/components/file_upload.jsx12
-rw-r--r--web/react/components/find_team.jsx7
-rw-r--r--web/react/components/get_link_modal.jsx5
-rw-r--r--web/react/components/invite_member_modal.jsx99
-rw-r--r--web/react/components/login.jsx20
-rw-r--r--web/react/components/member_list_item.jsx3
-rw-r--r--web/react/components/message_wrapper.jsx10
-rw-r--r--web/react/components/more_channels.jsx21
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar.jsx7
-rw-r--r--web/react/components/navbar_dropdown.jsx80
-rw-r--r--web/react/components/new_channel.jsx211
-rw-r--r--web/react/components/new_channel_flow.jsx206
-rw-r--r--web/react/components/new_channel_modal.jsx199
-rw-r--r--web/react/components/password_reset_form.jsx3
-rw-r--r--web/react/components/password_reset_send_link.jsx7
-rw-r--r--web/react/components/popover_list_members.jsx4
-rw-r--r--web/react/components/post.jsx53
-rw-r--r--web/react/components/post_body.jsx39
-rw-r--r--web/react/components/post_info.jsx17
-rw-r--r--web/react/components/post_list.jsx243
-rw-r--r--web/react/components/post_list_container.jsx63
-rw-r--r--web/react/components/register_app_modal.jsx249
-rw-r--r--web/react/components/rhs_comment.jsx23
-rw-r--r--web/react/components/rhs_header_post.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx17
-rw-r--r--web/react/components/search_bar.jsx4
-rw-r--r--web/react/components/search_results_header.jsx1
-rw-r--r--web/react/components/search_results_item.jsx18
-rw-r--r--web/react/components/setting_item_max.jsx2
-rw-r--r--web/react/components/setting_item_min.jsx8
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/setting_upload.jsx6
-rw-r--r--web/react/components/settings_sidebar.jsx3
-rw-r--r--web/react/components/sidebar.jsx112
-rw-r--r--web/react/components/sidebar_header.jsx6
-rw-r--r--web/react/components/sidebar_right_menu.jsx11
-rw-r--r--web/react/components/signup_team.jsx56
-rw-r--r--web/react/components/signup_team_complete.jsx10
-rw-r--r--web/react/components/signup_user_complete.jsx48
-rw-r--r--web/react/components/team_export_tab.jsx94
-rw-r--r--web/react/components/team_feature_tab.jsx190
-rw-r--r--web/react/components/team_general_tab.jsx9
-rw-r--r--web/react/components/team_import_tab.jsx16
-rw-r--r--web/react/components/team_settings.jsx16
-rw-r--r--web/react/components/team_settings_modal.jsx12
-rw-r--r--web/react/components/team_signup_allowed_domains_page.jsx143
-rw-r--r--web/react/components/team_signup_choose_auth.jsx23
-rw-r--r--web/react/components/team_signup_display_name_page.jsx5
-rw-r--r--web/react/components/team_signup_password_page.jsx7
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx18
-rw-r--r--web/react/components/team_signup_url_page.jsx13
-rw-r--r--web/react/components/team_signup_username_page.jsx5
-rw-r--r--web/react/components/team_signup_welcome_page.jsx3
-rw-r--r--web/react/components/team_signup_with_email.jsx3
-rw-r--r--web/react/components/team_signup_with_sso.jsx7
-rw-r--r--web/react/components/textbox.jsx53
-rw-r--r--web/react/components/unread_channel_indicator.jsx35
-rw-r--r--web/react/components/user_profile.jsx3
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx111
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx179
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx174
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx58
-rw-r--r--web/react/components/user_settings/user_settings.jsx (renamed from web/react/components/user_settings.jsx)29
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx236
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx93
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx (renamed from web/react/components/user_settings_general.jsx)68
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx95
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx (renamed from web/react/components/user_settings_modal.jsx)16
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx (renamed from web/react/components/user_settings_notifications.jsx)25
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx (renamed from web/react/components/user_settings_security.jsx)21
-rw-r--r--web/react/components/user_settings_appearance.jsx180
-rw-r--r--web/react/components/view_image.jsx253
-rw-r--r--web/react/components/view_image_popover_bar.jsx66
-rw-r--r--web/react/package.json39
-rw-r--r--web/react/pages/admin_console.jsx25
-rw-r--r--web/react/pages/authorize.jsx21
-rw-r--r--web/react/pages/channel.jsx58
-rw-r--r--web/react/pages/home.jsx6
-rw-r--r--web/react/pages/login.jsx7
-rw-r--r--web/react/pages/password_reset.jsx12
-rw-r--r--web/react/pages/signup_team.jsx10
-rw-r--r--web/react/pages/signup_team_complete.jsx8
-rw-r--r--web/react/pages/signup_user_complete.jsx15
-rw-r--r--web/react/pages/verify.jsx9
-rw-r--r--web/react/stores/admin_store.jsx128
-rw-r--r--web/react/stores/browser_store.jsx7
-rw-r--r--web/react/stores/channel_store.jsx14
-rw-r--r--web/react/stores/config_store.jsx69
-rw-r--r--web/react/stores/error_store.jsx2
-rw-r--r--web/react/stores/post_store.jsx1
-rw-r--r--web/react/stores/socket_store.jsx42
-rw-r--r--web/react/stores/user_store.jsx16
-rw-r--r--web/react/utils/async_client.jsx104
-rw-r--r--web/react/utils/client.jsx280
-rw-r--r--web/react/utils/config.js48
-rw-r--r--web/react/utils/constants.jsx174
-rw-r--r--web/react/utils/emoticons.jsx158
-rw-r--r--web/react/utils/markdown.jsx62
-rw-r--r--web/react/utils/text_formatting.jsx295
-rw-r--r--web/react/utils/utils.jsx449
134 files changed, 8991 insertions, 2167 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc
index 53cc75913..c0d0bb200 100644
--- a/web/react/.eslintrc
+++ b/web/react/.eslintrc
@@ -18,7 +18,8 @@
"es6": true
},
"globals": {
- "React": false
+ "React": false,
+ "ReactBootstrap": false
},
"rules": {
"comma-dangle": [2, "never"],
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
new file mode 100644
index 000000000..92f0bbdce
--- /dev/null
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AdminSidebar = require('./admin_sidebar.jsx');
+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 EmailSettingsTab = require('./email_settings.jsx');
+var LogSettingsTab = require('./log_settings.jsx');
+var LogsTab = require('./logs.jsx');
+var FileSettingsTab = require('./image_settings.jsx');
+var PrivacySettingsTab = require('./privacy_settings.jsx');
+var RateSettingsTab = require('./rate_settings.jsx');
+var GitLabSettingsTab = require('./gitlab_settings.jsx');
+var SqlSettingsTab = require('./sql_settings.jsx');
+var TeamSettingsTab = require('./team_settings.jsx');
+var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
+
+export default class AdminController extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
+ this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
+
+ this.state = {
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addConfigChangeListener(this.onConfigListenerChange);
+ AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ }
+
+ onConfigListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ render() {
+ var tab = <LoadingScreen />;
+
+ if (this.state.config != null) {
+ if (this.state.selected === 'email_settings') {
+ tab = <EmailSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'log_settings') {
+ tab = <LogSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'logs') {
+ tab = <LogsTab />;
+ } else if (this.state.selected === 'image_settings') {
+ tab = <FileSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'privacy_settings') {
+ tab = <PrivacySettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'rate_settings') {
+ tab = <RateSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'gitlab_settings') {
+ tab = <GitLabSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'sql_settings') {
+ tab = <SqlSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_settings') {
+ tab = <TeamSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'service_settings') {
+ tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
+ }
+ }
+
+ return (
+ <div className='container-fluid'>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ />
+ <AdminSidebar
+ selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
+ selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
+ />
+ <div className='inner__wrap channel__wrap'>
+ <div className='row header'>
+ </div>
+ <div className='row main'>
+ <div
+ id='app-content'
+ className='app__content admin'
+ >
+ {tab}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx
new file mode 100644
index 000000000..a3ab81079
--- /dev/null
+++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx
@@ -0,0 +1,102 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../../utils/utils.jsx');
+var Client = require('../../utils/client.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
+
+var Constants = require('../../utils/constants.jsx');
+
+function getStateFromStores() {
+ return {currentTeam: TeamStore.getCurrent()};
+}
+
+export default class AdminNavbarDropdown extends React.Component {
+ constructor(props) {
+ super(props);
+ this.blockToggle = false;
+
+ this.handleLogoutClick = this.handleLogoutClick.bind(this);
+
+ this.state = getStateFromStores();
+ }
+
+ handleLogoutClick(e) {
+ e.preventDefault();
+ Client.logout();
+ }
+
+ componentDidMount() {
+ $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ this.blockToggle = true;
+ setTimeout(() => {
+ this.blockToggle = false;
+ }, 100);
+ });
+ }
+
+ componentWillUnmount() {
+ $(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
+ }
+
+ render() {
+ return (
+ <ul className='nav navbar-nav navbar-right'>
+ <li
+ ref='dropdown'
+ className='dropdown'
+ >
+ <a
+ href='#'
+ className='dropdown-toggle'
+ data-toggle='dropdown'
+ role='button'
+ aria-expanded='false'
+ >
+ <span
+ className='dropdown__icon'
+ dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}
+ />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ >
+ <li>
+ <a
+ href={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name}
+ >
+ {'Switch to ' + this.state.currentTeam.display_name}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ onClick={this.handleLogoutClick}
+ >
+ {'Logout'}
+ </a>
+ </li>
+ <li className='divider'></li>
+ <li>
+ <a
+ target='_blank'
+ href='/static/help/help.html'
+ >
+ {'Help'}
+ </a>
+ </li>
+ <li>
+ <a
+ target='_blank'
+ href='/static/help/report_problem.html'
+ >
+ {'Report a Problem'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+} \ No newline at end of file
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
new file mode 100644
index 000000000..cebb3ff20
--- /dev/null
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -0,0 +1,278 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
+
+export default class AdminSidebar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.isSelected = this.isSelected.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
+
+ this.state = {
+ showSelectModal: false
+ };
+ }
+
+ handleClick(name, teamId, e) {
+ e.preventDefault();
+ this.props.selectTab(name, teamId);
+ }
+
+ isSelected(name, teamId) {
+ if (this.props.selected === name) {
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
+ }
+
+ return '';
+ }
+
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
+ componentDidMount() {
+ }
+
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
+ render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
+ return (
+ <div className='sidebar--left sidebar--collapsable'>
+ <div>
+ <AdminSidebarHeader />
+ <ul className='nav nav-pills nav-stacked'>
+ <li>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
+ >
+ {'Service Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
+ >
+ {'Team Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
+ >
+ {'SQL Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
+ >
+ {'Email Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
+ >
+ {'File Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
+ >
+ {'Log Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
+ >
+ {'Rate Limit Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
+ >
+ {'Privacy Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
+ >
+ {'GitLab Settings'}
+ </a>
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ <li>
+ {teams}
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
+ >
+ {'Logs'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
+ </div>
+ );
+ }
+}
+
+AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
+ selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
+ selectTab: React.PropTypes.func
+}; \ No newline at end of file
diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx
new file mode 100644
index 000000000..81798da45
--- /dev/null
+++ b/web/react/components/admin_console/admin_sidebar_header.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AdminNavbarDropdown = require('./admin_navbar_dropdown.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+
+export default class SidebarHeader extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.toggleDropdown = this.toggleDropdown.bind(this);
+
+ this.state = {};
+ }
+
+ toggleDropdown(e) {
+ e.preventDefault();
+
+ if (this.refs.dropdown.blockToggle) {
+ this.refs.dropdown.blockToggle = false;
+ return;
+ }
+
+ $('.team__header').find('.dropdown-toggle').dropdown('toggle');
+ }
+
+ render() {
+ var me = UserStore.getCurrentUser();
+ var profilePicture = null;
+
+ if (!me) {
+ return null;
+ }
+
+ if (me.last_picture_update) {
+ profilePicture = (
+ <img
+ className='user__picture'
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
+ />
+ );
+ }
+
+ return (
+ <div className='team__header theme'>
+ <a
+ href='#'
+ onClick={this.toggleDropdown}
+ >
+ {profilePicture}
+ <div className='header__info'>
+ <div className='user__name'>{'@' + me.username}</div>
+ <div className='team__name'>{'System Console'}</div>
+ </div>
+ </a>
+ <AdminNavbarDropdown ref='dropdown' />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
new file mode 100644
index 000000000..32c832c6a
--- /dev/null
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -0,0 +1,549 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class EmailSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleTestConnection = this.handleTestConnection.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.buildConfig = this.buildConfig.bind(this);
+ this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
+ this.handleGenerateReset = this.handleGenerateReset.bind(this);
+
+ this.state = {
+ sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ saveNeeded: false,
+ serverError: null,
+ emailSuccess: null,
+ emailFail: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'sendEmailNotifications_true') {
+ s.sendEmailNotifications = true;
+ }
+
+ if (action === 'sendEmailNotifications_false') {
+ s.sendEmailNotifications = false;
+ }
+
+ this.setState(s);
+ }
+
+ buildConfig() {
+ var config = this.props.config;
+ config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim();
+ config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim();
+ config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim();
+ config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim();
+ config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim();
+ config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim();
+
+ config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim();
+ if (config.EmailSettings.InviteSalt === '') {
+ config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
+ }
+
+ config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim();
+ if (config.EmailSettings.PasswordResetSalt === '') {
+ config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
+ }
+
+ return config;
+ }
+
+ handleGenerateInvite(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleGenerateReset(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleTestConnection(e) {
+ e.preventDefault();
+ $('#connection-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.testEmail(
+ config,
+ () => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: true,
+ emailFail: null
+ });
+ $('#connection-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: err.message + ' - ' + err.detailed_error
+ });
+ $('#connection-button').button('reset');
+ }
+ );
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: false,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: err.message,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var emailSuccess = '';
+ if (this.state.emailSuccess) {
+ emailSuccess = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>{'No errors were reported while sending an email. Please check your inbox to make sure.'}
+ </div>
+ );
+ }
+
+ var emailFail = '';
+ if (this.state.emailFail) {
+ emailSuccess = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>{'Connection unsuccessful: ' + this.state.emailFail}
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Email Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='allowSignUpWithEmail'
+ >
+ {'Allow Sign Up With Email: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignUpWithEmail'
+ value='true'
+ ref='allowSignUpWithEmail'
+ defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignUpWithEmail'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='sendEmailNotifications'
+ >
+ {'Send Email Notifications: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='true'
+ ref='sendEmailNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='requireEmailVerification'
+ >
+ {'Require Email Verification: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='true'
+ ref='requireEmailVerification'
+ defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'true'}
+ </label>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='feedbackName'
+ >
+ {'Feedback Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='feedbackName'
+ ref='feedbackName'
+ placeholder='Ex: "Mattermost Notification", "System", "No-Reply"'
+ defaultValue={this.props.config.EmailSettings.FeedbackName}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'Display name on email account used when sending notification emails from Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='feedbackEmail'
+ >
+ {'Feedback Email:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='email'
+ className='form-control'
+ id='feedbackEmail'
+ ref='feedbackEmail'
+ placeholder='Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
+ defaultValue={this.props.config.EmailSettings.FeedbackEmail}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'Email address displayed on email account used when sending notification emails from Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPUsername'
+ >
+ {'SMTP Username:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPUsername'
+ ref='SMTPUsername'
+ placeholder='Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.EmailSettings.SMTPUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPPassword'
+ >
+ {'SMTP Password:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPPassword'
+ ref='SMTPPassword'
+ placeholder='Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.EmailSettings.SMTPPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPServer'
+ >
+ {'SMTP Server:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPServer'
+ ref='SMTPServer'
+ placeholder='Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
+ defaultValue={this.props.config.EmailSettings.SMTPServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'Location of SMTP email server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPPort'
+ >
+ {'SMTP Port:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPPort'
+ ref='SMTPPort'
+ placeholder='Ex: "25", "465"'
+ defaultValue={this.props.config.EmailSettings.SMTPPort}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'Port of SMTP email server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ConnectionSecurity'
+ >
+ {'Connection Security:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='ConnectionSecurity'
+ ref='ConnectionSecurity'
+ defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ <option value=''>{'None'}</option>
+ <option value='TLS'>{'TLS (Recommended)'}</option>
+ <option value='STARTTLS'>{'STARTTLS'}</option>
+ </select>
+ <div className='help-text'>
+ <table
+ className='table-bordered'
+ cellPadding='5'
+ >
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </table>
+ </div>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleTestConnection}
+ disabled={!this.state.sendEmailNotifications}
+ id='connection-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Testing...'}
+ >
+ {'Test Connection'}
+ </button>
+ {emailSuccess}
+ {emailFail}
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='InviteSalt'
+ >
+ {'Invite Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='InviteSalt'
+ ref='InviteSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.InviteSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateInvite}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PasswordResetSalt'
+ >
+ {'Password Reset Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PasswordResetSalt'
+ ref='PasswordResetSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateReset}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+EmailSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
new file mode 100644
index 000000000..1e10c5592
--- /dev/null
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -0,0 +1,277 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class GitLabSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ Allow: this.props.config.GitLabSettings.Allow,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'AllowTrue') {
+ s.Allow = true;
+ }
+
+ if (action === 'AllowFalse') {
+ s.Allow = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.GitLabSettings.Allow = React.findDOMNode(this.refs.Allow).checked;
+ config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
+ config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
+ config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+ config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
+ config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
+ config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).value.trim();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'GitLab Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Allow'
+ >
+ {'Enable Sign Up With GitLab: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='true'
+ ref='Allow'
+ defaultChecked={this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='false'
+ defaultChecked={!this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Secret'
+ >
+ {'Secret:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Secret'
+ ref='Secret'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Secret}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Id'
+ >
+ {'Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Id'
+ ref='Id'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Id}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Scope'
+ >
+ {'Scope:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Scope'
+ ref='Scope'
+ placeholder='Not currently used by GitLab. Please leave blank'
+ defaultValue={this.props.config.GitLabSettings.Scope}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AuthEndpoint'
+ >
+ {'Auth Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AuthEndpoint'
+ ref='AuthEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize).'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='TokenEndpoint'
+ >
+ {'Token Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='TokenEndpoint'
+ ref='TokenEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='UserApiEndpoint'
+ >
+ {'User API Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='UserApiEndpoint'
+ ref='UserApiEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+GitLabSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
new file mode 100644
index 000000000..e52f516e8
--- /dev/null
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -0,0 +1,496 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class FileSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null,
+ DriverName: this.props.config.FileSettings.DriverName
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'DriverName') {
+ s.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ }
+
+ this.setState(s);
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value;
+ config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
+ config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
+ config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value;
+ config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value;
+ config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked;
+
+ config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+
+ if (config.FileSettings.PublicLinkSalt === '') {
+ config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
+ }
+
+ var thumbnailWidth = 120;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
+ thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10);
+ }
+ config.FileSettings.ThumbnailWidth = thumbnailWidth;
+ React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
+
+ var thumbnailHeight = 100;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
+ thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10);
+ }
+ config.FileSettings.ThumbnailHeight = thumbnailHeight;
+ React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
+
+ var previewWidth = 1024;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) {
+ previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10);
+ }
+ config.FileSettings.PreviewWidth = previewWidth;
+ React.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
+
+ var previewHeight = 0;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) {
+ previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10);
+ }
+ config.FileSettings.PreviewHeight = previewHeight;
+ React.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
+
+ var profileWidth = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) {
+ profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10);
+ }
+ config.FileSettings.ProfileWidth = profileWidth;
+ React.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
+
+ var profileHeight = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) {
+ profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10);
+ }
+ config.FileSettings.ProfileHeight = profileHeight;
+ React.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var enableFile = false;
+ var enableS3 = false;
+
+ if (this.state.DriverName === 'local') {
+ enableFile = true;
+ }
+
+ if (this.state.DriverName === 'amazons3') {
+ enableS3 = true;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'File Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Store Files In:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='DriverName'
+ ref='DriverName'
+ defaultValue={this.props.config.FileSettings.DriverName}
+ onChange={this.handleChange.bind(this, 'DriverName')}
+ >
+ <option value=''>{'Disable File Storage'}</option>
+ <option value='local'>{'Local File System'}</option>
+ <option value='amazons3'>{'Amazon S3'}</option>
+ </select>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Directory'
+ >
+ {'Local Directory Location:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Directory'
+ ref='Directory'
+ placeholder='Ex "./data/"'
+ defaultValue={this.props.config.FileSettings.Directory}
+ onChange={this.handleChange}
+ disabled={!enableFile}
+ />
+ <p className='help-text'>{'Directory to which image files are written. If blank, will be set to ./data/.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3AccessKeyId'
+ >
+ {'Amazon S3 Access Key Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3AccessKeyId'
+ ref='AmazonS3AccessKeyId'
+ placeholder='Ex "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3SecretAccessKey'
+ >
+ {'Amazon S3 Secret Access Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3SecretAccessKey'
+ ref='AmazonS3SecretAccessKey'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Bucket'
+ >
+ {'Amazon S3 Bucket:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Bucket'
+ ref='AmazonS3Bucket'
+ placeholder='Ex "mattermost-media"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Name you selected for your S3 bucket in AWS.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Region'
+ >
+ {'Amazon S3 Region:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Region'
+ ref='AmazonS3Region'
+ placeholder='Ex "us-east-1"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Region}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'AWS region you selected for creating your S3 bucket.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailWidth'
+ >
+ {'Thumbnail Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailWidth'
+ ref='ThumbnailWidth'
+ placeholder='Ex "120"'
+ defaultValue={this.props.config.FileSettings.ThumbnailWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailHeight'
+ >
+ {'Thumbnail Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailHeight'
+ ref='ThumbnailHeight'
+ placeholder='Ex "100"'
+ defaultValue={this.props.config.FileSettings.ThumbnailHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewWidth'
+ >
+ {'Preview Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewWidth'
+ ref='PreviewWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.PreviewWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewHeight'
+ >
+ {'Preview Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewHeight'
+ ref='PreviewHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.PreviewHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileWidth'
+ >
+ {'Profile Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileWidth'
+ ref='ProfileWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.ProfileWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileHeight'
+ >
+ {'Profile Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileHeight'
+ ref='ProfileHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.ProfileHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePublicLink'
+ >
+ {'Share Public File Link: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='true'
+ ref='EnablePublicLink'
+ defaultChecked={this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='false'
+ defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Allow users to share public links to files and images.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PublicLinkSalt'
+ >
+ {'Public Link Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PublicLinkSalt'
+ ref='PublicLinkSalt'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.FileSettings.PublicLinkSalt}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+FileSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
new file mode 100644
index 000000000..1c39c60e8
--- /dev/null
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -0,0 +1,295 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class LogSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ consoleEnable: this.props.config.LogSettings.EnableConsole,
+ fileEnable: this.props.config.LogSettings.EnableFile,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'console_true') {
+ s.consoleEnable = true;
+ }
+
+ if (action === 'console_false') {
+ s.consoleEnable = false;
+ }
+
+ if (action === 'file_true') {
+ s.fileEnable = true;
+ }
+
+ if (action === 'file_false') {
+ s.fileEnable = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.LogSettings.EnableConsole = React.findDOMNode(this.refs.consoleEnable).checked;
+ config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
+ config.LogSettings.EnableFile = React.findDOMNode(this.refs.fileEnable).checked;
+ config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
+ config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
+ config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Log Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='consoleEnable'
+ >
+ {'Log To The Console: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='consoleEnable'
+ value='true'
+ ref='consoleEnable'
+ defaultChecked={this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='consoleEnable'
+ value='false'
+ defaultChecked={!this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='consoleLevel'
+ >
+ {'Console Log Level:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='consoleLevel'
+ ref='consoleLevel'
+ defaultValue={this.props.config.LogSettings.consoleLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.consoleEnable}
+ >
+ <option value='DEBUG'>{'DEBUG'}</option>
+ <option value='INFO'>{'INFO'}</option>
+ <option value='ERROR'>{'ERROR'}</option>
+ </select>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ {'Log To File: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='fileEnable'
+ ref='fileEnable'
+ value='true'
+ defaultChecked={this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='fileEnable'
+ value='false'
+ defaultChecked={!this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, log files are written to the log file specified in file location field below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='fileLevel'
+ >
+ {'File Log Level:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='fileLevel'
+ ref='fileLevel'
+ defaultValue={this.props.config.LogSettings.FileLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
+ >
+ <option value='DEBUG'>{'DEBUG'}</option>
+ <option value='INFO'>{'INFO'}</option>
+ <option value='ERROR'>{'ERROR'}</option>
+ </select>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='fileLocation'
+ >
+ {'File Location:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='fileLocation'
+ ref='fileLocation'
+ placeholder='Enter your file location'
+ defaultValue={this.props.config.LogSettings.FileLocation}
+ onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
+ />
+ <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='fileFormat'
+ >
+ {'File Format:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='fileFormat'
+ ref='fileFormat'
+ placeholder='Enter your file format'
+ defaultValue={this.props.config.LogSettings.FileFormat}
+ onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
+ />
+ <p className='help-text'>
+ {'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
+ <div className='help-text'>
+ <table
+ className='table-bordered'
+ cellPadding='5'
+ >
+ <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
+ <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
+ <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
+ <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
+ <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
+ <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ </table>
+ </div>
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+LogSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/logs.jsx b/web/react/components/admin_console/logs.jsx
new file mode 100644
index 000000000..0bb749bbd
--- /dev/null
+++ b/web/react/components/admin_console/logs.jsx
@@ -0,0 +1,90 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var AdminStore = require('../../stores/admin_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class Logs extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onLogListenerChange = this.onLogListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ logs: AdminStore.getLogs()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addLogChangeListener(this.onLogListenerChange);
+ AsyncClient.getLogs();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeLogChangeListener(this.onLogListenerChange);
+ }
+
+ onLogListenerChange() {
+ this.setState({
+ logs: AdminStore.getLogs()
+ });
+ }
+
+ reload() {
+ AdminStore.saveLogs(null);
+ this.setState({
+ logs: null
+ });
+
+ AsyncClient.getLogs();
+ }
+
+ render() {
+ var content = null;
+
+ if (this.state.logs === null) {
+ content = <LoadingScreen />;
+ } else {
+ content = [];
+
+ for (var i = 0; i < this.state.logs.length; i++) {
+ var style = {
+ whiteSpace: 'nowrap',
+ fontFamily: 'monospace'
+ };
+
+ if (this.state.logs[i].indexOf('[EROR]') > 0) {
+ style.color = 'red';
+ }
+
+ content.push(<br key={'br_' + i} />);
+ content.push(
+ <span
+ key={'log_' + i}
+ style={style}
+ >
+ {this.state.logs[i]}
+ </span>
+ );
+ }
+ }
+
+ return (
+ <div className='panel'>
+ <h3>{'Server Logs'}</h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ {'Reload'}
+ </button>
+ <div className='log__panel'>
+ {content}
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
new file mode 100644
index 000000000..affd8ae11
--- /dev/null
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -0,0 +1,163 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class PrivacySettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
+ config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Privacy Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowEmailAddress'
+ >
+ {'Show Email Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='true'
+ ref='ShowEmailAddress'
+ defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowFullName'
+ >
+ {'Show Full Name: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='true'
+ ref='ShowFullName'
+ defaultChecked={this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, hides full name of users from other users including team owner and team administrators.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+PrivacySettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
new file mode 100644
index 000000000..0081daca3
--- /dev/null
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -0,0 +1,272 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class RateSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
+ VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'EnableRateLimiterTrue') {
+ s.EnableRateLimiter = true;
+ }
+
+ if (action === 'EnableRateLimiterFalse') {
+ s.EnableRateLimiter = false;
+ }
+
+ if (action === 'VaryByRemoteAddrTrue') {
+ s.VaryByRemoteAddr = true;
+ }
+
+ if (action === 'VaryByRemoteAddrFalse') {
+ s.VaryByRemoteAddr = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.RateLimitSettings.EnableRateLimiter = React.findDOMNode(this.refs.EnableRateLimiter).checked;
+ config.RateLimitSettings.VaryByRemoteAddr = React.findDOMNode(this.refs.VaryByRemoteAddr).checked;
+ config.RateLimitSettings.VaryByHeader = React.findDOMNode(this.refs.VaryByHeader).value.trim();
+
+ var PerSec = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PerSec).value, 10))) {
+ PerSec = parseInt(React.findDOMNode(this.refs.PerSec).value, 10);
+ }
+ config.RateLimitSettings.PerSec = PerSec;
+ React.findDOMNode(this.refs.PerSec).value = PerSec;
+
+ var MemoryStoreSize = 10000;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
+ MemoryStoreSize = parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10);
+ }
+ config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
+ React.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'Rate Limit Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableRateLimiter'
+ >
+ {'Enable Rate Limiter: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='true'
+ ref='EnableRateLimiter'
+ defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, APIs are throttled at rates specified below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PerSec'
+ >
+ {'Number Of Queries Per Second:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PerSec'
+ ref='PerSec'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.RateLimitSettings.PerSec}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Throttles API at this number of requests per second.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MemoryStoreSize'
+ >
+ {'Memory Store Size:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MemoryStoreSize'
+ ref='MemoryStoreSize'
+ placeholder='Ex "10000"'
+ defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByRemoteAddr'
+ >
+ {'Vary By Remote Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='true'
+ ref='VaryByRemoteAddr'
+ defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, rate limit API access by IP address.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByHeader'
+ >
+ {'Vary By HTTP Header:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='VaryByHeader'
+ ref='VaryByHeader'
+ placeholder='Ex "X-Real-IP", "X-Forwarded-For"'
+ defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
+ />
+ <p className='help-text'>{'When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring Ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+RateSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
new file mode 100644
index 000000000..343f65131
--- /dev/null
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
+ render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
new file mode 100644
index 000000000..245ffa871
--- /dev/null
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -0,0 +1,296 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class ServiceSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.ServiceSettings.ListenAddress = React.findDOMNode(this.refs.ListenAddress).value.trim();
+ if (config.ServiceSettings.ListenAddress === '') {
+ config.ServiceSettings.ListenAddress = ':8065';
+ React.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
+ }
+
+ config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
+ config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
+ //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
+ config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
+
+ var MaximumLoginAttempts = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
+ MaximumLoginAttempts = parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
+ }
+ config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
+ React.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'Service Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ListenAddress'
+ >
+ {'Listen Address:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ListenAddress'
+ ref='ListenAddress'
+ placeholder='Ex ":8065"'
+ defaultValue={this.props.config.ServiceSettings.ListenAddress}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaximumLoginAttempts'
+ >
+ {'Maximum Login Attempts:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaximumLoginAttempts'
+ ref='MaximumLoginAttempts'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Login attempts allowed before user is locked out and required to reset password via email.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SegmentDeveloperKey'
+ >
+ {'Segment Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SegmentDeveloperKey'
+ ref='SegmentDeveloperKey'
+ placeholder='Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
+ defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'For users running a SaaS services, sign up for a key at Segment.com to track metrics.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='GoogleDeveloperKey'
+ >
+ {'Google Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='GoogleDeveloperKey'
+ ref='GoogleDeveloperKey'
+ placeholder='Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
+ defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '}<a href='https://www.youtube.com/watch?v=Im69kzhpR3I'>{'https://www.youtube.com/watch?v=Im69kzhpR3I'}</a>{'. Leaving field blank disables the automatic generation of YouTube video previews from links.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableIncomingWebhooks'
+ >
+ {'Enable Incoming Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='true'
+ ref='EnableIncomingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTesting'
+ >
+ {'Enable Testing: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='true'
+ ref='EnableTesting'
+ defaultChecked={this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+// <div className='form-group'>
+// <label
+// className='control-label col-sm-4'
+// htmlFor='EnableOAuthServiceProvider'
+// >
+// {'Enable OAuth Service Provider: '}
+// </label>
+// <div className='col-sm-8'>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='true'
+// ref='EnableOAuthServiceProvider'
+// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'true'}
+// </label>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='false'
+// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'false'}
+// </label>
+// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
+// </div>
+// </div>
+
+ServiceSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
new file mode 100644
index 000000000..430a7453b
--- /dev/null
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -0,0 +1,283 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class SqlSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.SqlSettings.Trace = React.findDOMNode(this.refs.Trace).checked;
+ config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
+
+ if (config.SqlSettings.AtRestEncryptKey === '') {
+ config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
+ }
+
+ var MaxOpenConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
+ MaxOpenConns = parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10);
+ }
+ config.SqlSettings.MaxOpenConns = MaxOpenConns;
+ React.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
+
+ var MaxIdleConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
+ MaxIdleConns = parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10);
+ }
+ config.SqlSettings.MaxIdleConns = MaxIdleConns;
+ React.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
+
+ var dataSourceReplicas = '';
+ this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'SQL Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Driver Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ {'Data Source:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ {'Data Source Replicas:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxIdleConns'
+ >
+ {'Maximum Idle Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxIdleConns'
+ ref='MaxIdleConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxIdleConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of idle connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxOpenConns'
+ >
+ {'Maximum Open Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxOpenConns'
+ ref='MaxOpenConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxOpenConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of open connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AtRestEncryptKey'
+ >
+ {'At Rest Encrypt Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AtRestEncryptKey'
+ ref='AtRestEncryptKey'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt available to encrypt and decrypt sensitive fields in database.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Trace'
+ >
+ {'Trace: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='true'
+ ref='Trace'
+ defaultChecked={this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='false'
+ defaultChecked={!this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'(Development Mode) When true, executing SQL statements are written to the log.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+SqlSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
new file mode 100644
index 000000000..0f6f819d3
--- /dev/null
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class TeamSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
+ config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
+ config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
+ config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
+
+ var MaxUsersPerTeam = 50;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
+ MaxUsersPerTeam = parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
+ }
+ config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
+ React.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>{'Team Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SiteName'
+ >
+ {'Site Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SiteName'
+ ref='SiteName'
+ placeholder='Ex "Mattermost"'
+ defaultValue={this.props.config.TeamSettings.SiteName}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Name of service shown in login screens and UI.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxUsersPerTeam'
+ >
+ {'Max Users Per Team:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxUsersPerTeam'
+ ref='MaxUsersPerTeam'
+ placeholder='Ex "25"'
+ defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum total number of users per team, including both active and inactive users.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamCreation'
+ >
+ {'Enable Team Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='true'
+ ref='EnableTeamCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableUserCreation'
+ >
+ {'Enable User Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='true'
+ ref='EnableUserCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictCreationToDomains'
+ >
+ {'Restrict Creation To Domains:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='RestrictCreationToDomains'
+ ref='RestrictCreationToDomains'
+ placeholder='Ex "corp.mattermost.com, mattermost.org"'
+ defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Teams can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
+ >
+ {'Save'}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+TeamSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ 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,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(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>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/authorize.jsx b/web/react/components/authorize.jsx
new file mode 100644
index 000000000..dd4479ad4
--- /dev/null
+++ b/web/react/components/authorize.jsx
@@ -0,0 +1,72 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../utils/client.jsx');
+
+export default class Authorize extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleAllow = this.handleAllow.bind(this);
+ this.handleDeny = this.handleDeny.bind(this);
+
+ this.state = {};
+ }
+ handleAllow() {
+ const responseType = this.props.responseType;
+ const clientId = this.props.clientId;
+ const redirectUri = this.props.redirectUri;
+ const state = this.props.state;
+ const scope = this.props.scope;
+
+ Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
+ (data) => {
+ if (data.redirect) {
+ window.location.replace(data.redirect);
+ }
+ },
+ () => {}
+ );
+ }
+ handleDeny() {
+ window.location.replace(this.props.redirectUri + '?error=access_denied');
+ }
+ render() {
+ return (
+ <div className='authorize-box'>
+ <div className='authorize-inner'>
+ <h3>{'An application would like to connect to your '}{this.props.teamName}{' account'}</h3>
+ <label>{'The app '}{this.props.appName}{' would like the ability to access and modify your basic information.'}</label>
+ <br/>
+ <br/>
+ <label>{'Allow '}{this.props.appName}{' access?'}</label>
+ <br/>
+ <button
+ type='submit'
+ className='btn authorize-btn'
+ onClick={this.handleDeny}
+ >
+ {'Deny'}
+ </button>
+ <button
+ type='submit'
+ className='btn btn-primary authorize-btn'
+ onClick={this.handleAllow}
+ >
+ {'Allow'}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
+
+Authorize.propTypes = {
+ appName: React.PropTypes.string,
+ teamName: React.PropTypes.string,
+ responseType: React.PropTypes.string,
+ clientId: React.PropTypes.string,
+ redirectUri: React.PropTypes.string,
+ state: React.PropTypes.string,
+ scope: React.PropTypes.string
+};
diff --git a/web/react/components/change_url_modal.jsx b/web/react/components/change_url_modal.jsx
new file mode 100644
index 000000000..3553e1107
--- /dev/null
+++ b/web/react/components/change_url_modal.jsx
@@ -0,0 +1,177 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Modal = ReactBootstrap.Modal;
+var Utils = require('../utils/utils.jsx');
+
+export default class ChangeUrlModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onURLChanged = this.onURLChanged.bind(this);
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ currentURL: props.currentURL,
+ urlError: '',
+ userEdit: false
+ };
+ }
+ componentWillReceiveProps(nextProps) {
+ // This check prevents the url being deleted when we re-render
+ // because of user status check
+ if (!this.state.userEdit) {
+ this.setState({
+ currentURL: nextProps.currentURL
+ });
+ }
+ }
+ componentDidUpdate(prevProps) {
+ if (this.props.show === true && prevProps.show === false) {
+ React.findDOMNode(this.refs.urlinput).select();
+ }
+ }
+ onURLChanged(e) {
+ const url = e.target.value.trim();
+ this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true});
+ }
+ getURLError(url) {
+ let error = []; //eslint-disable-line prefer-const
+ if (url.length < 2) {
+ error.push(<span key='error1'>{'Must be longer than two characters'}<br/></span>);
+ }
+ if (url.charAt(0) === '-' || url.charAt(0) === '_') {
+ error.push(<span key='error2'>{'Must start with a letter or number'}<br/></span>);
+ }
+ if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) {
+ error.push(<span key='error3'>{'Must end with a letter or number'}<br/></span>);
+ }
+ if (url.indexOf('__') > -1) {
+ error.push(<span key='error4'>{'Can not contain two underscores in a row.'}<br/></span>);
+ }
+
+ // In case of error we don't detect
+ if (error.length === 0) {
+ error.push(<span key='errorlast'>{'Invalid URL'}<br/></span>);
+ }
+ return error;
+ }
+ doSubmit(e) {
+ e.preventDefault();
+
+ const url = React.findDOMNode(this.refs.urlinput).value;
+ const cleanedURL = Utils.cleanUpUrlable(url);
+ if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
+ this.setState({urlError: this.getURLError(url)});
+ return;
+ }
+ this.setState({urlError: '', userEdit: false});
+ this.props.onModalSubmit(url);
+ }
+ doCancel() {
+ this.setState({urlError: '', userEdit: false});
+ this.props.onModalDismissed();
+ }
+ render() {
+ let urlClass = 'input-group input-group--limit';
+ let urlError = null;
+ let serverError = null;
+
+ if (this.state.urlError) {
+ urlClass += ' has-error';
+ urlError = (<p className='input__help error'>{this.state.urlError}</p>);
+ }
+
+ if (this.props.serverError) {
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
+ }
+
+ const fullTeamUrl = Utils.getTeamURLFromAddressBar();
+ const teamURL = Utils.getShortenedTeamURL();
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{this.props.title}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='modal-intro'>{this.props.description}</div>
+ <div className='form-group'>
+ <label className='col-sm-2 form__label control-label'>{this.props.urlLabel}</label>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title={fullTeamUrl}
+ className='input-group-addon'
+ >
+ {teamURL}
+ </span>
+ <input
+ type='text'
+ ref='urlinput'
+ className='form-control'
+ maxLength='22'
+ onChange={this.onURLChanged}
+ value={this.state.currentURL}
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {urlError}
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {this.props.submitButtonText}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ChangeUrlModal.defaultProps = {
+ show: false,
+ title: 'Change URL',
+ desciption: '',
+ urlLabel: 'URL',
+ submitButtonText: 'Save',
+ currentURL: '',
+ serverError: ''
+};
+
+ChangeUrlModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ title: React.PropTypes.string,
+ description: React.PropTypes.string,
+ urlLabel: React.PropTypes.string,
+ submitButtonText: React.PropTypes.string,
+ currentURL: React.PropTypes.string,
+ serverError: React.PropTypes.string,
+ onModalSubmit: React.PropTypes.func.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 87b9cab04..b81936b57 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
+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');
@@ -54,7 +55,7 @@ export default class ChannelHeader extends React.Component {
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
- $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover click', html: true, delay: {show: 500, hide: 500}});
+ $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
}
onSocketChange(msg) {
if (msg.action === 'new_user') {
@@ -64,9 +65,14 @@ export default class ChannelHeader extends React.Component {
handleLeave() {
Client.leaveChannel(this.state.channel.id,
function handleLeaveSuccess() {
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.LEAVE_CHANNEL,
+ id: this.state.channel.id
+ });
+
const townsquare = ChannelStore.getByName('town-square');
Utils.switchChannel(townsquare);
- },
+ }.bind(this),
function handleLeaveError(err) {
AsyncClient.dispatchError(err, 'handleLeave');
}
@@ -102,11 +108,10 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
- const description = Utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true});
const popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>);
let channelTitle = channel.display_name;
const currentId = UserStore.getCurrentId();
- const isAdmin = this.state.memberChannel.roles.indexOf('admin') > -1 || this.state.memberTeam.roles.indexOf('admin') > -1;
+ const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.memberTeam.roles);
const isDirect = (this.state.channel.type === 'D');
if (isDirect) {
@@ -321,9 +326,9 @@ export default class ChannelHeader extends React.Component {
data-toggle='popover'
data-content={popoverContent}
className='description'
- >
- {description}
- </div>
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
+ />
</div>
</th>
<th>
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 5feeb4e88..82fc51184 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -121,7 +121,7 @@ export default class ChannelInviteModal extends React.Component {
var currentMember = ChannelStore.getCurrentMember();
var isAdmin = false;
if (currentMember) {
- isAdmin = currentMember.roles.indexOf('admin') > -1 || UserStore.getCurrentUser().roles.indexOf('admin') > -1;
+ isAdmin = utils.isAdmin(currentMember.roles) || utils.isAdmin(UserStore.getCurrentUser().roles);
}
var content;
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 8e8ed3f73..39c86405c 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -12,11 +12,14 @@ var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var Utils = require('../utils/utils.jsx');
+var Constants = require('../utils/constants.jsx');
export default class ChannelLoader extends React.Component {
constructor(props) {
super(props);
+ this.intervalId = null;
+
this.onSocketChange = this.onSocketChange.bind(this);
this.state = {};
@@ -35,10 +38,12 @@ export default class ChannelLoader extends React.Component {
PostStore.clearPendingPosts();
/* Set up interval functions */
- setInterval(
+ this.intervalId = setInterval(
function pollStatuses() {
AsyncClient.getStatuses();
- }, 30000);
+ },
+ 30000
+ );
/* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
@@ -49,12 +54,12 @@ export default class ChannelLoader extends React.Component {
/* Set up tracking for whether the window is active */
window.isActive = true;
- $(window).focus(function windowFocus() {
+ $(window).on('focus', function windowFocus() {
AsyncClient.updateLastViewedAt();
window.isActive = true;
});
- $(window).blur(function windowBlur() {
+ $(window).on('blur', function windowBlur() {
window.isActive = false;
});
@@ -64,26 +69,69 @@ export default class ChannelLoader extends React.Component {
/* Update CSS classes to match user theme */
var user = UserStore.getCurrentUser();
- if (user.props && user.props.theme) {
- Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';');
- Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.mention', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';');
- Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}');
- Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';');
+ if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
+ Utils.applyTheme(user.theme_props);
+ } else {
+ Utils.applyTheme(Constants.THEMES.default);
}
- if (user.props.theme !== '#000000' && user.props.theme !== '#585858') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';');
- Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;');
- } else if (user.props.theme === '#000000') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';');
- $('.team__header').addClass('theme--black');
- } else if (user.props.theme === '#585858') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';');
- $('.team__header').addClass('theme--gray');
- }
+ /* Setup global mouse events */
+ $('body').on('click', function hidePopover(e) {
+ $('[data-toggle="popover"]').each(function eachPopover() {
+ if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
+ $(this).popover('hide');
+ }
+ });
+ });
+
+ $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
+ if (ev.type === 'mouseenter') {
+ $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
+ } else {
+ $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
+ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
+ }
+ });
+
+ $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
+ if (ev.type === 'mouseenter') {
+ $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
+ $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
+ } else {
+ $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
+ $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
+ }
+ });
+
+ /* Setup modal events */
+ $('.modal').on('show.bs.modal', function onShow() {
+ $('.modal-body').css('overflow-y', 'auto');
+ $('.modal-body').css('max-height', $(window).height() * 0.7);
+ });
+
+ /* Prevent backspace from navigating back a page */
+ $(window).on('keydown.preventBackspace', (e) => {
+ if (e.which === 8 && !$(e.target).is('input, textarea')) {
+ e.preventDefault();
+ }
+ });
+ }
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+
+ $(window).off('focus');
+ $(window).off('blur');
+
+ SocketStore.removeChangeListener(this.onSocketChange);
+
+ $('body').off('click.userpopover');
+ $('body').off('mouseenter mouseleave', '.post');
+ $('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
+
+ $('.modal').off('show.bs.modal');
+
+ $(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx
index 04fa2c7a2..1eda6a104 100644
--- a/web/react/components/channel_members.jsx
+++ b/web/react/components/channel_members.jsx
@@ -130,7 +130,7 @@ export default class ChannelMembers extends React.Component {
const currentMember = ChannelStore.getCurrentMember();
let isAdmin = false;
if (currentMember) {
- isAdmin = currentMember.roles.indexOf('admin') > -1 || UserStore.getCurrentUser().roles.indexOf('admin') > -1;
+ isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
var memberList = null;
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 83067240d..9eda68b38 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -163,10 +163,22 @@ export default class ChannelNotifications extends React.Component {
}.bind(this);
let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (<span>These settings will override the global notification settings</span>);
+ let extraInfo = (
+ <span>
+ These settings will override the global notification settings.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
if (curChannel && curChannel.display_name) {
- extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>);
+ extraInfo = (
+ <span>
+ These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
}
return (
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index c2fc0dcf3..5097b3aa5 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -28,6 +28,7 @@ export default class CreateComment extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -42,6 +43,12 @@ export default class CreateComment extends React.Component {
submitting: false
};
}
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
+ $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
+ }
handleSubmit(e) {
e.preventDefault();
@@ -178,6 +185,11 @@ export default class CreateComment extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -264,6 +276,7 @@ export default class CreateComment extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='comment'
channelId={this.props.channelId}
/>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 871b72a43..0cd14747d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -23,6 +23,7 @@ export default class CreatePost extends React.Component {
this.lastTime = 0;
+ this.getCurrentDraft = this.getCurrentDraft.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
@@ -30,31 +31,53 @@ export default class CreatePost extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
PostStore.clearDraftUploads();
- const draft = PostStore.getCurrentDraft();
- let previews = [];
- let messageText = '';
- let uploadsInProgress = [];
- if (draft && draft.previews && draft.message) {
- previews = draft.previews;
- messageText = draft.message;
- uploadsInProgress = draft.uploadsInProgress;
- }
+ const draft = this.getCurrentDraft();
this.state = {
channelId: ChannelStore.getCurrentId(),
- messageText: messageText,
- uploadsInProgress: uploadsInProgress,
- previews: previews,
+ messageText: draft.messageText,
+ uploadsInProgress: draft.uploadsInProgress,
+ previews: draft.previews,
submitting: false,
- initialText: messageText
+ initialText: draft.messageText
};
}
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.previews.length !== this.state.previews.length) {
+ this.resizePostHolder();
+ return;
+ }
+
+ if (prevState.uploadsInProgress !== this.state.uploadsInProgress) {
+ this.resizePostHolder();
+ return;
+ }
+ }
+ getCurrentDraft() {
+ const draft = PostStore.getCurrentDraft();
+ const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
+
+ if (draft) {
+ if (draft.message) {
+ safeDraft.messageText = draft.message;
+ }
+ if (draft.previews) {
+ safeDraft.previews = draft.previews;
+ }
+ if (draft.uploadsInProgress) {
+ safeDraft.uploadsInProgress = draft.uploadsInProgress;
+ }
+ }
+
+ return safeDraft;
+ }
handleSubmit(e) {
e.preventDefault();
@@ -62,7 +85,7 @@ export default class CreatePost extends React.Component {
return;
}
- let post = {};
+ const post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -82,20 +105,20 @@ export default class CreatePost extends React.Component {
this.state.channelId,
post.message,
false,
- function handleCommandSuccess(data) {
+ (data) => {
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
if (data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
- }.bind(this),
- function handleCommandError(err) {
- let state = {};
+ },
+ (err) => {
+ const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
} else {
post.channel_id = this.state.channelId;
@@ -116,10 +139,10 @@ export default class CreatePost extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
- function handlePostSuccess(data) {
+ (data) => {
AsyncClient.getPosts();
- let member = ChannelStore.getMember(channel.id);
+ const member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
@@ -129,8 +152,8 @@ export default class CreatePost extends React.Component {
post: data
});
},
- function handlePostError(err) {
- let state = {};
+ (err) => {
+ const state = {};
if (err.message === 'Invalid RootId parameter') {
if ($('#post_deleted').length > 0) {
@@ -144,7 +167,7 @@ export default class CreatePost extends React.Component {
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
}
}
@@ -162,9 +185,9 @@ export default class CreatePost extends React.Component {
}
}
handleUserInput(messageText) {
- this.setState({messageText: messageText});
+ this.setState({messageText});
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
@@ -174,7 +197,7 @@ export default class CreatePost extends React.Component {
$(window).trigger('resize');
}
handleUploadStart(clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeDraft(channelId, draft);
@@ -182,7 +205,7 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress});
}
handleFileUploadComplete(filenames, clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -200,7 +223,7 @@ export default class CreatePost extends React.Component {
}
handleUploadError(err, clientId) {
if (clientId !== -1) {
- let draft = PostStore.getDraft(this.state.channelId);
+ const draft = PostStore.getDraft(this.state.channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
@@ -214,9 +237,14 @@ export default class CreatePost extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
- let previews = this.state.previews;
- let uploadsInProgress = this.state.uploadsInProgress;
+ const previews = Object.assign([], this.state.previews);
+ const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
@@ -231,12 +259,12 @@ export default class CreatePost extends React.Component {
}
}
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCurrentDraft(draft);
- this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({previews, uploadsInProgress});
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
@@ -248,18 +276,9 @@ export default class CreatePost extends React.Component {
onChange() {
const channelId = ChannelStore.getCurrentId();
if (this.state.channelId !== channelId) {
- let draft = PostStore.getCurrentDraft();
-
- let previews = [];
- let messageText = '';
- let uploadsInProgress = [];
- if (draft && draft.previews && draft.message) {
- previews = draft.previews;
- messageText = draft.message;
- uploadsInProgress = draft.uploadsInProgress;
- }
+ const draft = this.getCurrentDraft();
- this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
getFileCount(channelId) {
@@ -310,25 +329,34 @@ export default class CreatePost extends React.Component {
>
<div className='post-create'>
<div className='post-create-body'>
- <Textbox
- onUserInput={this.handleUserInput}
- onKeyPress={this.postMsgKeyPress}
- onHeightChange={this.resizePostHolder}
- messageText={this.state.messageText}
- createMessage='Write a message...'
- channelId={this.state.channelId}
- id='post_textbox'
- ref='textbox'
- />
- <FileUpload
- ref='fileUpload'
- getFileCount={this.getFileCount}
- onUploadStart={this.handleUploadStart}
- onFileUpload={this.handleFileUploadComplete}
- onUploadError={this.handleUploadError}
- postType='post'
- channelId=''
- />
+ <div className='post-body__cell'>
+ <Textbox
+ onUserInput={this.handleUserInput}
+ onKeyPress={this.postMsgKeyPress}
+ onHeightChange={this.resizePostHolder}
+ messageText={this.state.messageText}
+ createMessage='Write a message...'
+ channelId={this.state.channelId}
+ id='post_textbox'
+ ref='textbox'
+ />
+ <FileUpload
+ ref='fileUpload'
+ getFileCount={this.getFileCount}
+ onUploadStart={this.handleUploadStart}
+ onFileUpload={this.handleFileUploadComplete}
+ onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
+ postType='post'
+ channelId=''
+ />
+ </div>
+ <a
+ className='send-button theme'
+ onClick={this.handleSubmit}
+ >
+ <i className='fa fa-paper-plane' />
+ </a>
</div>
<div className={postFooterClassName}>
{postError}
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 4efb9cb23..44c54db72 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -4,6 +4,7 @@
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -24,7 +25,7 @@ export default class DeleteChannelModal extends React.Component {
Client.deleteChannel(this.state.channelId,
function handleDeleteSuccess() {
AsyncClient.getChannels(true);
- window.location.href = '/';
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
function handleDeleteError(err) {
AsyncClient.dispatchError(err, 'handleDelete');
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
index 95948c8dd..8d3f15525 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-import {config} from '../utils/config.js';
-
export default class EmailVerify extends React.Component {
constructor(props) {
super(props);
@@ -12,17 +10,19 @@ export default class EmailVerify extends React.Component {
this.state = {};
}
handleResend() {
- window.location.href = window.location.href + '&resend=true';
+ const newAddress = window.location.href.replace('&resend_success=true', '');
+ window.location.href = newAddress + '&resend=true';
}
render() {
var title = '';
var body = '';
var resend = '';
+ var resendConfirm = '';
if (this.props.isVerified === 'true') {
- title = config.SiteName + ' Email Verified';
+ title = global.window.config.SiteName + ' Email Verified';
body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
} else {
- title = config.SiteName + ' Email Not Verified';
+ title = global.window.config.SiteName + ' Email Not Verified';
body = <p>Please verify your email address. Check your inbox for an email.</p>;
resend = (
<button
@@ -32,6 +32,9 @@ export default class EmailVerify extends React.Component {
Resend Email
</button>
);
+ if (this.props.resendSuccess) {
+ resendConfirm = <div><br /><p className='alert alert-success'><i className='fa fa-check'></i>{' Verification email sent.'}</p></div>;
+ }
}
return (
@@ -43,6 +46,7 @@ export default class EmailVerify extends React.Component {
<div className='panel-body'>
{body}
{resend}
+ {resendConfirm}
</div>
</div>
</div>
@@ -53,10 +57,12 @@ export default class EmailVerify extends React.Component {
EmailVerify.defaultProps = {
isVerified: 'false',
teamURL: '',
- userEmail: ''
+ userEmail: '',
+ resendSuccess: 'false'
};
EmailVerify.propTypes = {
isVerified: React.PropTypes.string,
teamURL: React.PropTypes.string,
- userEmail: React.PropTypes.string
+ userEmail: React.PropTypes.string,
+ resendSuccess: React.PropTypes.string
};
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index 87d94a41d..05726e860 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -2,10 +2,6 @@
// See License.txt for license information.
var ErrorStore = require('../stores/error_store.jsx');
-var utils = require('../utils/utils.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
export default class ErrorBar extends React.Component {
constructor() {
@@ -13,70 +9,79 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
+ this.prevTimer = null;
- this.state = this.getStateFromStores();
- if (this.state.message) {
- setTimeout(this.handleClose, 10000);
+ this.state = ErrorStore.getLastError();
+ if (this.state && this.state.message) {
+ this.prevTimer = setTimeout(this.handleClose, 10000);
}
}
- getStateFromStores() {
- var error = ErrorStore.getLastError();
- if (!error || error.message === 'There appears to be a problem with your internet connection') {
- return {message: null};
- }
- return {message: error.message};
- }
componentDidMount() {
ErrorStore.addChangeListener(this.onErrorChange);
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
- $(window).resize(function onResize() {
- if (this.state.message) {
+ $(window).resize(() => {
+ if (this.state && this.state.message) {
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
}
- }.bind(this));
+ });
}
+
componentWillUnmount() {
ErrorStore.removeChangeListener(this.onErrorChange);
}
+
onErrorChange() {
- var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- if (newState.message) {
- setTimeout(this.handleClose, 10000);
- }
+ var newState = ErrorStore.getLastError();
+
+ if (this.prevTimer != null) {
+ clearInterval(this.prevTimer);
+ this.prevTimer = null;
+ }
+ if (newState) {
this.setState(newState);
+ this.prevTimer = setTimeout(this.handleClose, 10000);
+ } else {
+ this.setState({message: null});
}
}
+
handleClose(e) {
if (e) {
e.preventDefault();
}
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
+ ErrorStore.storeLastError(null);
+ ErrorStore.emitChange();
$('body').css('padding-top', '0');
}
+
render() {
- if (this.state.message) {
- return (
- <div className='error-bar'>
- <span>{this.state.message}</span>
- <a
- href='#'
- className='error-bar__close'
- onClick={this.handleClose}
- >
- &times;
- </a>
- </div>
- );
+ if (!this.state) {
+ return <div/>;
+ }
+
+ if (!this.state.message) {
+ return <div/>;
+ }
+
+ if (this.state.connErrorCount < 7) {
+ return <div/>;
}
- return <div/>;
+ return (
+ <div className='error-bar'>
+ <span>{this.state.message}</span>
+ <a
+ href='#'
+ className='error-bar__close'
+ onClick={this.handleClose}
+ >
+ &times;
+ </a>
+ </div>
+ );
}
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 78693df98..888f24aa5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -97,6 +97,7 @@ export default class FileAttachment extends React.Component {
var filename = this.props.filename;
var fileInfo = utils.splitFileLocation(filename);
+ var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
var thumbnail;
@@ -142,22 +143,30 @@ export default class FileAttachment extends React.Component {
>
<a className='post-image__thumbnail'
href='#'
- onClick={this.props.handleImageClick}
- data-img-id={this.props.index}
- data-toggle='modal'
- data-target={'#' + this.props.modalId}
+ onClick={() => this.props.handleImageClick(this.props.index)}
>
{thumbnail}
</a>
<div className='post-image__details'>
- <div
+ <a
+ href={fileUrl}
+ download={filenameString}
data-toggle='tooltip'
title={filenameString}
className='post-image__name'
>
{trimmedFilename}
- </div>
+ </a>
<div>
+ <a
+ href={fileUrl}
+ download={filenameString}
+ className='post-image__download'
+ >
+ <span
+ className='fa fa-download'
+ />
+ </a>
<span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span>
<span className='post-image__size'>{fileSizeString}</span>
</div>
@@ -175,9 +184,6 @@ FileAttachment.propTypes = {
// the index of this attachment preview in the parent FileAttachmentList
index: React.PropTypes.number.isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
- // handler for when the thumbnail is clicked
+ // handler for when the thumbnail is clicked passed the index above
handleImageClick: React.PropTypes.func
};
diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx
index abe72089a..212d4a958 100644
--- a/web/react/components/file_attachment_list.jsx
+++ b/web/react/components/file_attachment_list.jsx
@@ -11,23 +11,21 @@ export default class FileAttachmentList extends React.Component {
this.handleImageClick = this.handleImageClick.bind(this);
- this.state = {startImgId: 0};
+ this.state = {showPreviewModal: false, startImgId: 0};
}
- handleImageClick(e) {
- this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)});
+ handleImageClick(indexClicked) {
+ this.setState({showPreviewModal: true, startImgId: indexClicked});
}
render() {
var filenames = this.props.filenames;
- var modalId = this.props.modalId;
var postFiles = [];
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
postFiles.push(
<FileAttachment
- key={i}
+ key={'file_attachment_' + i}
filename={filenames[i]}
index={i}
- modalId={modalId}
handleImageClick={this.handleImageClick}
/>
);
@@ -39,9 +37,10 @@ export default class FileAttachmentList extends React.Component {
{postFiles}
</div>
<ViewImageModal
+ show={this.state.showPreviewModal}
+ onModalDismissed={() => this.setState({showPreviewModal: false})}
channelId={this.props.channelId}
userId={this.props.userId}
- modalId={modalId}
startId={this.state.startImgId}
filenames={filenames}
/>
@@ -55,9 +54,6 @@ FileAttachmentList.propTypes = {
// a list of file pathes displayed by this
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
// the channel that this is part of
channelId: React.PropTypes.string,
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 534f0136e..3dc4e5de2 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -20,12 +20,11 @@ export default class FileUpload extends React.Component {
}
fileUploadSuccess(channelId, data) {
- var parsedData = $.parseJSON(data);
- this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
+ this.props.onFileUpload(data.filenames, data.client_ids, channelId);
var requests = this.state.requests;
- for (var j = 0; j < parsedData.client_ids.length; j++) {
- delete requests[parsedData.client_ids[j]];
+ for (var j = 0; j < data.client_ids.length; j++) {
+ delete requests[data.client_ids[j]];
}
this.setState({requests: requests});
}
@@ -53,7 +52,7 @@ export default class FileUpload extends React.Component {
}
// generate a unique id that can be used by other components to refer back to this upload
- var clientId = utils.generateId();
+ let clientId = utils.generateId();
// prepare data to be uploaded
var formData = new FormData();
@@ -111,7 +110,7 @@ export default class FileUpload extends React.Component {
if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
} else {
- this.props.onUploadError('Invalid file upload', -1);
+ this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
}
}
@@ -267,6 +266,7 @@ FileUpload.propTypes = {
getFileCount: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
+ onTextDrop: React.PropTypes.func,
channelId: React.PropTypes.string,
postType: React.PropTypes.string
};
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index 52988886c..eb2683a88 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -3,7 +3,6 @@
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
-import {strings} from '../utils/config.js';
export default class FindTeam extends React.Component {
constructor(props) {
@@ -51,8 +50,8 @@ export default class FindTeam extends React.Component {
if (this.state.sent) {
return (
<div>
- <h4>{'Find Your ' + utils.toTitleCase(strings.Team)}</h4>
- <p>{'An email was sent with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p>
+ <h4>{'Find Your team'}</h4>
+ <p>{'An email was sent with links to any teams to which you are a member.'}</p>
</div>
);
}
@@ -61,7 +60,7 @@ export default class FindTeam extends React.Component {
<div>
<h4>Find Your Team</h4>
<form onSubmit={this.handleSubmit}>
- <p>{'Get an email with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p>
+ <p>{'Get an email with links to any teams to which you are a member.'}</p>
<div className='form-group'>
<label className='control-label'>Email</label>
<div className={emailErrorClass}>
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 1f25ea0b7..5d8b13f00 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
-import {strings} from '../utils/config.js';
export default class GetLinkModal extends React.Component {
constructor(props) {
@@ -76,9 +75,9 @@ export default class GetLinkModal extends React.Component {
</div>
<div className='modal-body'>
<p>
- Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site.
+ Send teammates the link below for them to sign-up to this team site.
<br /><br />
- Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}.
+ Be careful not to share this link publicly, since anyone with the link can join your team.
</p>
<textarea
className='form-control no-resize'
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index c1cfa7800..395b98630 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -2,11 +2,9 @@
// See License.txt for license information.
var utils = require('../utils/utils.jsx');
-var ConfigStore = require('../stores/config_store.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var ConfirmModal = require('./confirm_modal.jsx');
-import {config} from '../utils/config.js';
export default class InviteMemberModal extends React.Component {
constructor(props) {
@@ -23,7 +21,7 @@ export default class InviteMemberModal extends React.Component {
emailErrors: {},
firstNameErrors: {},
lastNameErrors: {},
- emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false)
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
}
@@ -79,23 +77,9 @@ export default class InviteMemberModal extends React.Component {
emailErrors[index] = '';
}
- if (config.AllowInviteNames) {
- invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim();
- if (!invite.firstName && config.RequireInviteNames) {
- firstNameErrors[index] = 'This is a required field';
- valid = false;
- } else {
- firstNameErrors[index] = '';
- }
+ invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim();
- invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim();
- if (!invite.lastName && config.RequireInviteNames) {
- lastNameErrors[index] = 'This is a required field';
- valid = false;
- } else {
- lastNameErrors[index] = '';
- }
- }
+ invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim();
invites.push(invite);
}
@@ -143,10 +127,8 @@ export default class InviteMemberModal extends React.Component {
for (var i = 0; i < inviteIds.length; i++) {
var index = inviteIds[i];
React.findDOMNode(this.refs['email' + index]).value = '';
- if (config.AllowInviteNames) {
- React.findDOMNode(this.refs['first_name' + index]).value = '';
- React.findDOMNode(this.refs['last_name' + index]).value = '';
- }
+ React.findDOMNode(this.refs['first_name' + index]).value = '';
+ React.findDOMNode(this.refs['last_name' + index]).value = '';
}
this.setState({
@@ -210,44 +192,43 @@ export default class InviteMemberModal extends React.Component {
}
var nameFields = null;
- if (config.AllowInviteNames) {
- var firstNameClass = 'form-group';
- if (firstNameError) {
- firstNameClass += ' has-error';
- }
- var lastNameClass = 'form-group';
- if (lastNameError) {
- lastNameClass += ' has-error';
- }
- nameFields = (<div className='row--invite'>
- <div className='col-sm-6'>
- <div className={firstNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'first_name' + index}
- placeholder='First name'
- maxLength='64'
- disabled={!this.state.emailEnabled}
- />
- {firstNameError}
- </div>
+
+ var firstNameClass = 'form-group';
+ if (firstNameError) {
+ firstNameClass += ' has-error';
+ }
+ var lastNameClass = 'form-group';
+ if (lastNameError) {
+ lastNameClass += ' has-error';
+ }
+ nameFields = (<div className='row--invite'>
+ <div className='col-sm-6'>
+ <div className={firstNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'first_name' + index}
+ placeholder='First name'
+ maxLength='64'
+ disabled={!this.state.emailEnabled}
+ />
+ {firstNameError}
</div>
- <div className='col-sm-6'>
- <div className={lastNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'last_name' + index}
- placeholder='Last name'
- maxLength='64'
- disabled={!this.state.emailEnabled}
- />
- {lastNameError}
- </div>
+ </div>
+ <div className='col-sm-6'>
+ <div className={lastNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'last_name' + index}
+ placeholder='Last name'
+ maxLength='64'
+ disabled={!this.state.emailEnabled}
+ />
+ {lastNameError}
</div>
- </div>);
- }
+ </div>
+ </div>);
inviteSections[index] = (
<div key={'key' + index}>
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index b20c62833..8cc4f1483 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -5,8 +5,6 @@ const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const UserStore = require('../stores/user_store.jsx');
const BrowserStore = require('../stores/browser_store.jsx');
-const Constants = require('../utils/constants.jsx');
-import {config, strings} from '../utils/config.js';
export default class Login extends React.Component {
constructor(props) {
@@ -96,10 +94,8 @@ export default class Login extends React.Component {
focusEmail = true;
}
- const authServices = JSON.parse(this.props.authServices);
-
let loginMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -117,7 +113,7 @@ export default class Login extends React.Component {
}
let emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className={'form-group' + errorClass}>
@@ -177,7 +173,7 @@ export default class Login extends React.Component {
<div className='signup-team__container'>
<h5 className='margin--less'>Sign in to:</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {config.SiteName}</h2>
+ <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2>
<form onSubmit={this.handleSubmit}>
<div className={'form-group' + errorClass}>
{serverError}
@@ -185,11 +181,11 @@ export default class Login extends React.Component {
{loginMessage}
{emailSignup}
<div className='form-group margin--extra form-group--small'>
- <span><a href='/find_team'>{'Find other ' + strings.TeamPlural}</a></span>
+ <span><a href='/find_team'>{'Find other teams'}</a></span>
</div>
{forgotPassword}
<div className='margin--extra'>
- <span>{'Want to create your own ' + strings.Team + '? '}
+ <span>{'Want to create your own team? '}
<a
href='/'
className='signup-team-login'
@@ -206,11 +202,9 @@ export default class Login extends React.Component {
Login.defaultProps = {
teamName: '',
- teamDisplayName: '',
- authServices: ''
+ teamDisplayName: ''
};
Login.propTypes = {
teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- authServices: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 9a31a2e30..158ff65be 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class MemberListItem extends React.Component {
constructor(props) {
@@ -26,7 +27,7 @@ export default class MemberListItem extends React.Component {
render() {
var member = this.props.member;
var isAdmin = this.props.isAdmin;
- var isMemberAdmin = member.roles.indexOf('admin') > -1;
+ var isMemberAdmin = Utils.isAdmin(member.roles);
var timestamp = UserStore.getCurrentUser().update_at;
var invite;
diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx
index bce305853..5adf4f228 100644
--- a/web/react/components/message_wrapper.jsx
+++ b/web/react/components/message_wrapper.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
export default class MessageWrapper extends React.Component {
constructor(props) {
@@ -10,10 +10,7 @@ export default class MessageWrapper extends React.Component {
}
render() {
if (this.props.message) {
- var inner = Utils.textToJsx(this.props.message, this.props.options);
- return (
- <div>{inner}</div>
- );
+ return <div dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, this.props.options)}}/>;
}
return <div/>;
@@ -21,8 +18,7 @@ export default class MessageWrapper extends React.Component {
}
MessageWrapper.defaultProps = {
- message: null,
- options: null
+ message: ''
};
MessageWrapper.propTypes = {
message: React.PropTypes.string,
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index ba8be12b2..65cd40975 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -6,6 +6,7 @@ var client = require('../utils/client.jsx');
var asyncClient = require('../utils/async_client.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var LoadingScreen = require('./loading_screen.jsx');
+var NewChannelFlow = require('./new_channel_flow.jsx');
function getStateFromStores() {
return {
@@ -25,6 +26,7 @@ export default class MoreChannels extends React.Component {
var initState = getStateFromStores();
initState.channelType = '';
initState.joiningChannel = -1;
+ initState.showNewChannelModal = false;
this.state = initState;
}
componentDidMount() {
@@ -66,6 +68,7 @@ export default class MoreChannels extends React.Component {
}
handleNewChannel() {
$(React.findDOMNode(this.refs.modal)).modal('hide');
+ this.setState({showNewChannelModal: true});
}
render() {
var serverError;
@@ -148,20 +151,22 @@ export default class MoreChannels extends React.Component {
className='close'
data-dismiss='modal'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
+ <span aria-hidden='true'>{'×'}</span>
+ <span className='sr-only'>{'Close'}</span>
</button>
- <h4 className='modal-title'>More Channels</h4>
+ <h4 className='modal-title'>{'More Channels'}</h4>
<button
- data-toggle='modal'
- data-target='#new_channel'
- data-channeltype={this.state.channelType}
type='button'
className='btn btn-primary channel-create-btn'
onClick={this.handleNewChannel}
>
- Create New Channel
+ {'Create New Channel'}
</button>
+ <NewChannelFlow
+ show={this.state.showNewChannelModal}
+ channelType={this.state.channelType}
+ onModalDismissed={() => this.setState({showNewChannelModal: false})}
+ />
</div>
<div className='modal-body'>
{moreChannels}
@@ -173,7 +178,7 @@ export default class MoreChannels extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Close
+ {'Close'}
</button>
</div>
</div>
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index f27b09ecc..b7bce9b34 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component {
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Close</span>
</button>
- <h4 className='modal-title'>More Private Messages</h4>
+ <h4 className='modal-title'>More Direct Messages</h4>
</div>
<div className='modal-body'>
<ul className='nav nav-pills nav-stacked'>
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index 2258bf2b3..bdb50cd9e 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 Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -261,7 +262,7 @@ export default class Navbar extends React.Component {
return (
<div className='navbar-brand'>
<a
- href='/'
+ href={TeamStore.getCurrentTeamUrl() + '/channels/town-square'}
className='heading'
>
{channelTitle}
@@ -332,10 +333,10 @@ export default class Navbar extends React.Component {
popoverContent = React.renderToString(
<MessageWrapper
message={channel.description}
- options={{singleline: true, noMentionHighlight: true}}
+ options={{singleline: true, mentionHighlight: false}}
/>
);
- isAdmin = this.state.member.roles.indexOf('admin') > -1;
+ isAdmin = Utils.isAdmin(this.state.member.roles);
if (channel.type === 'O') {
channelTitle = channel.display_name;
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 99cdfa1ad..4c01d2c43 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -7,7 +7,6 @@ var UserStore = require('../stores/user_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
-import {config} from '../utils/config.js';
function getStateFromStores() {
return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()};
@@ -31,12 +30,12 @@ export default class NavbarDropdown extends React.Component {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
- $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', function resetDropdown() {
+ $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
- setTimeout(function blockTimeout() {
+ setTimeout(() => {
this.blockToggle = false;
- }.bind(this), 100);
- }.bind(this));
+ }, 100);
+ });
}
componentWillUnmount() {
UserStore.removeTeamsChangeListener(this.onListenerChange);
@@ -54,12 +53,16 @@ export default class NavbarDropdown extends React.Component {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
+ var sysAdminLink = '';
+ var adminDivider = '';
var currentUser = UserStore.getCurrentUser();
var isAdmin = false;
+ var isSystemAdmin = false;
var teamSettings = null;
if (currentUser != null) {
- isAdmin = currentUser.roles.indexOf('admin') > -1;
+ isAdmin = Utils.isAdmin(currentUser.roles);
+ isSystemAdmin = Utils.isInRole(currentUser.roles, 'system_admin');
inviteLink = (
<li>
@@ -68,7 +71,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#invite_member'
>
- Invite New Member
+ {'Invite New Member'}
</a>
</li>
);
@@ -83,7 +86,7 @@ export default class NavbarDropdown extends React.Component {
data-title='Team Invite'
data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
>
- Get Team Invite Link
+ {'Get Team Invite Link'}
</a>
</li>
);
@@ -98,19 +101,36 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
- Manage Team
+ {'Manage Team'}
+ </a>
+ </li>
+ );
+
+ adminDivider = (<li className='divider'></li>);
+
+ teamSettings = (
+ <li>
+ <a
+ href='#'
+ data-toggle='modal'
+ data-target='#team_settings'
+ >
+ {'Team Settings'}
+ </a>
+ </li>
+ );
+ }
+
+ if (isSystemAdmin) {
+ sysAdminLink = (
+ <li>
+ <a
+ href='/admin_console'
+ >
+ {'System Console'}
</a>
</li>
);
- teamSettings = (<li>
- <a
- href='#'
- data-toggle='modal'
- data-target='#team_settings'
- >
- Team Settings
- </a>
- </li>);
}
var teams = [];
@@ -124,9 +144,9 @@ export default class NavbarDropdown extends React.Component {
);
if (this.state.teams.length > 1 && this.state.currentTeam) {
var curTeamName = this.state.currentTeam.name;
- this.state.teams.forEach(function listTeams(teamName) {
+ this.state.teams.forEach((teamName) => {
if (teamName !== curTeamName) {
- teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>Switch to {teamName}</a></li>);
+ teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
}
});
}
@@ -136,7 +156,7 @@ export default class NavbarDropdown extends React.Component {
target='_blank'
href={Utils.getWindowLocationOrigin() + '/signup_team'}
>
- Create a New Team
+ {'Create a New Team'}
</a>
</li>);
@@ -168,37 +188,39 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#user_settings'
>
- Account Settings
+ {'Account Settings'}
</a>
</li>
- {teamSettings}
{inviteLink}
{teamLink}
- {manageLink}
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
- Logout
+ {'Logout'}
</a>
</li>
+ {adminDivider}
+ {teamSettings}
+ {manageLink}
+ {sysAdminLink}
{teams}
<li className='divider'></li>
<li>
<a
target='_blank'
- href={config.HelpLink}
+ href='/static/help/help.html'
>
- Help
+ {'Help'}
</a>
</li>
<li>
<a
target='_blank'
- href={config.ReportProblemLink}
+ href='/static/help/report_problem.html'
>
- Report a Problem
+ {'Report a Problem'}
</a>
</li>
</ul>
diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx
deleted file mode 100644
index 1a11fc652..000000000
--- a/web/react/components/new_channel.jsx
+++ /dev/null
@@ -1,211 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
-var asyncClient = require('../utils/async_client.jsx');
-var UserStore = require('../stores/user_store.jsx');
-
-export default class NewChannelModal extends React.Component {
- constructor() {
- super();
-
- this.handleSubmit = this.handleSubmit.bind(this);
- this.displayNameKeyUp = this.displayNameKeyUp.bind(this);
- this.handleClose = this.handleClose.bind(this);
-
- this.state = {channelType: ''};
- }
- handleSubmit(e) {
- e.preventDefault();
-
- var channel = {};
- var state = {serverError: ''};
-
- channel.display_name = React.findDOMNode(this.refs.display_name).value.trim();
- if (!channel.display_name) {
- state.displayNameError = 'This field is required';
- state.inValid = true;
- } else if (channel.display_name.length > 22) {
- state.displayNameError = 'This field must be less than 22 characters';
- state.inValid = true;
- } else {
- state.displayNameError = '';
- }
-
- channel.name = React.findDOMNode(this.refs.channel_name).value.trim();
- if (!channel.name) {
- state.nameError = 'This field is required';
- state.inValid = true;
- } else if (channel.name.length > 22) {
- state.nameError = 'This field must be less than 22 characters';
- state.inValid = true;
- } else {
- var cleanedName = utils.cleanUpUrlable(channel.name);
- if (cleanedName !== channel.name) {
- state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'";
- state.inValid = true;
- } else {
- state.nameError = '';
- }
- }
-
- this.setState(state);
-
- if (state.inValid) {
- return;
- }
-
- var cu = UserStore.getCurrentUser();
- channel.team_id = cu.team_id;
-
- channel.description = React.findDOMNode(this.refs.channel_desc).value.trim();
- channel.type = this.state.channelType;
-
- client.createChannel(channel,
- function success(data) {
- $(React.findDOMNode(this.refs.modal)).modal('hide');
-
- asyncClient.getChannel(data.id);
- utils.switchChannel(data);
-
- React.findDOMNode(this.refs.display_name).value = '';
- React.findDOMNode(this.refs.channel_name).value = '';
- React.findDOMNode(this.refs.channel_desc).value = '';
- }.bind(this),
- function error(err) {
- state.serverError = err.message;
- state.inValid = true;
- this.setState(state);
- }.bind(this)
- );
- }
- displayNameKeyUp() {
- var displayName = React.findDOMNode(this.refs.display_name).value.trim();
- var channelName = utils.cleanUpUrlable(displayName);
- React.findDOMNode(this.refs.channel_name).value = channelName;
- }
- componentDidMount() {
- var self = this;
- $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) {
- var button = e.relatedTarget;
- self.setState({channelType: $(button).attr('data-channeltype')});
- });
- $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
- }
- componentWillUnmount() {
- $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
- }
- handleClose() {
- $(React.findDOMNode(this)).find('.form-control').each(function clearForms() {
- this.value = '';
- });
-
- this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false});
- }
- render() {
- var displayNameError = null;
- var nameError = null;
- var serverError = null;
- var displayNameClass = 'form-group';
- var nameClass = 'form-group';
-
- if (this.state.displayNameError) {
- displayNameError = <label className='control-label'>{this.state.displayNameError}</label>;
- displayNameClass += ' has-error';
- }
- if (this.state.nameError) {
- nameError = <label className='control-label'>{this.state.nameError}</label>;
- nameClass += ' has-error';
- }
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var channelTerm = 'Channel';
- if (this.state.channelType === 'P') {
- channelTerm = 'Group';
- }
-
- return (
- <div
- className='modal fade'
- id='new_channel'
- ref='modal'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
- >
- <div className='modal-dialog'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- >
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Cancel</span>
- </button>
- <h4 className='modal-title'>New {channelTerm}</h4>
- </div>
- <form role='form'>
- <div className='modal-body'>
- <div className={displayNameClass}>
- <label className='control-label'>Display Name</label>
- <input
- onKeyUp={this.displayNameKeyUp}
- type='text'
- ref='display_name'
- className='form-control'
- placeholder='Enter display name'
- maxLength='22'
- />
- {displayNameError}
- </div>
- <div className={nameClass}>
- <label className='control-label'>Handle</label>
- <input
- type='text'
- className='form-control'
- ref='channel_name'
- placeholder="lowercase alphanumeric's only"
- maxLength='22'
- />
- {nameError}
- </div>
- <div className='form-group'>
- <label className='control-label'>Description</label>
- <textarea
- className='form-control no-resize'
- ref='channel_desc'
- rows='3'
- placeholder='Description'
- maxLength='1024'
- />
- </div>
- {serverError}
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Cancel
- </button>
- <button
- onClick={this.handleSubmit}
- type='submit'
- className='btn btn-primary'
- >
- Create New {channelTerm}
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx
new file mode 100644
index 000000000..df6a119d5
--- /dev/null
+++ b/web/react/components/new_channel_flow.jsx
@@ -0,0 +1,206 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../utils/utils.jsx');
+var AsyncClient = require('../utils/async_client.jsx');
+var Client = require('../utils/client.jsx');
+var UserStore = require('../stores/user_store.jsx');
+
+var NewChannelModal = require('./new_channel_modal.jsx');
+var ChangeURLModal = require('./change_url_modal.jsx');
+
+const SHOW_NEW_CHANNEL = 1;
+const SHOW_EDIT_URL = 2;
+const SHOW_EDIT_URL_THEN_COMPLETE = 3;
+
+export default class NewChannelFlow extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.typeSwitched = this.typeSwitched.bind(this);
+ this.urlChangeRequested = this.urlChangeRequested.bind(this);
+ this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
+ this.urlChangeDismissed = this.urlChangeDismissed.bind(this);
+ this.channelDataChanged = this.channelDataChanged.bind(this);
+
+ this.state = {
+ serverError: '',
+ channelType: 'O',
+ flowState: SHOW_NEW_CHANNEL,
+ channelDisplayName: '',
+ channelName: '',
+ channelDescription: '',
+ nameModified: false
+ };
+ }
+ componentWillReceiveProps(nextProps) {
+ // If we are being shown, grab channel type from props and clear
+ if (nextProps.show === true && this.props.show === false) {
+ this.setState({
+ serverError: '',
+ channelType: nextProps.channelType,
+ flowState: SHOW_NEW_CHANNEL,
+ channelDisplayName: '',
+ channelName: '',
+ channelDescription: '',
+ nameModified: false
+ });
+ }
+ }
+ doSubmit() {
+ var channel = {};
+
+ channel.display_name = this.state.channelDisplayName;
+ if (!channel.display_name) {
+ this.setState({serverError: 'Invalid Channel Name'});
+ return;
+ }
+
+ channel.name = this.state.channelName;
+ if (channel.name.length < 2) {
+ this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
+ return;
+ }
+
+ const cu = UserStore.getCurrentUser();
+ channel.team_id = cu.team_id;
+ channel.description = this.state.channelDescription;
+ channel.type = this.state.channelType;
+
+ Client.createChannel(channel,
+ (data) => {
+ this.props.onModalDismissed();
+ AsyncClient.getChannel(data.id);
+ Utils.switchChannel(data);
+ },
+ (err) => {
+ if (err.message === 'Name must be 2 or more lowercase alphanumeric characters') {
+ this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
+ }
+ if (err.message === 'A channel with that handle already exists') {
+ this.setState({serverError: 'A channel with that URL already exists'});
+ return;
+ }
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+ typeSwitched() {
+ if (this.state.channelType === 'P') {
+ this.setState({channelType: 'O'});
+ } else {
+ this.setState({channelType: 'P'});
+ }
+ }
+ urlChangeRequested() {
+ this.setState({flowState: SHOW_EDIT_URL});
+ }
+ urlChangeSubmitted(newURL) {
+ if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) {
+ this.setState({channelName: newURL, nameModified: true}, this.doSubmit);
+ } else {
+ this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true});
+ }
+ }
+ urlChangeDismissed() {
+ this.setState({flowState: SHOW_NEW_CHANNEL});
+ }
+ channelDataChanged(data) {
+ this.setState({
+ channelDisplayName: data.displayName,
+ channelDescription: data.description
+ });
+ if (!this.state.nameModified) {
+ this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
+ }
+ }
+ render() {
+ const channelData = {
+ name: this.state.channelName,
+ displayName: this.state.channelDisplayName,
+ description: this.state.channelDescription
+ };
+
+ let showChannelModal = false;
+ let showGroupModal = false;
+ let showChangeURLModal = false;
+
+ let changeURLTitle = '';
+ let changeURLSubmitButtonText = '';
+ let channelTerm = '';
+
+ // Only listen to flow state if we are being shown
+ if (this.props.show) {
+ switch (this.state.flowState) {
+ case SHOW_NEW_CHANNEL:
+ if (this.state.channelType === 'O') {
+ showChannelModal = true;
+ channelTerm = 'Channel';
+ } else {
+ showGroupModal = true;
+ channelTerm = 'Group';
+ }
+ break;
+ case SHOW_EDIT_URL:
+ showChangeURLModal = true;
+ changeURLTitle = 'Change ' + channelTerm + ' URL';
+ changeURLSubmitButtonText = 'Change ' + channelTerm + ' URL';
+ break;
+ case SHOW_EDIT_URL_THEN_COMPLETE:
+ showChangeURLModal = true;
+ changeURLTitle = 'Set ' + channelTerm + ' URL';
+ changeURLSubmitButtonText = 'Create ' + channelTerm;
+ break;
+ }
+ }
+ return (
+ <span>
+ <NewChannelModal
+ show={showChannelModal}
+ channelType={'O'}
+ channelData={channelData}
+ serverError={this.state.serverError}
+ onSubmitChannel={this.doSubmit}
+ onModalDismissed={this.props.onModalDismissed}
+ onTypeSwitched={this.typeSwitched}
+ onChangeURLPressed={this.urlChangeRequested}
+ onDataChanged={this.channelDataChanged}
+ />
+ <NewChannelModal
+ show={showGroupModal}
+ channelType={'P'}
+ channelData={channelData}
+ serverError={this.state.serverError}
+ onSubmitChannel={this.doSubmit}
+ onModalDismissed={this.props.onModalDismissed}
+ onTypeSwitched={this.typeSwitched}
+ onChangeURLPressed={this.urlChangeRequested}
+ onDataChanged={this.channelDataChanged}
+ />
+ <ChangeURLModal
+ show={showChangeURLModal}
+ title={changeURLTitle}
+ description={'Some characters are not allowed in URLs and may be removed.'}
+ urlLabel={channelTerm + ' URL'}
+ submitButtonText={changeURLSubmitButtonText}
+ currentURL={this.state.channelName}
+ serverError={this.state.serverError}
+ onModalSubmit={this.urlChangeSubmitted}
+ onModalDismissed={this.urlChangeDismissed}
+ />
+ </span>
+ );
+ }
+}
+
+NewChannelFlow.defaultProps = {
+ show: false,
+ channelType: 'O'
+};
+
+NewChannelFlow.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ channelType: React.PropTypes.string.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
new file mode 100644
index 000000000..c8ef59b4a
--- /dev/null
+++ b/web/react/components/new_channel_modal.jsx
@@ -0,0 +1,199 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Utils = require('../utils/utils.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class NewChannelModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+
+ this.state = {
+ displayNameError: ''
+ };
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.show === true && this.props.show === false) {
+ this.setState({
+ displayNameError: ''
+ });
+ }
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const displayName = React.findDOMNode(this.refs.display_name).value.trim();
+ if (displayName.length < 1) {
+ this.setState({displayNameError: 'This field is required'});
+ return;
+ }
+
+ this.props.onSubmitChannel();
+ }
+ handleChange() {
+ const newData = {
+ displayName: React.findDOMNode(this.refs.display_name).value,
+ description: React.findDOMNode(this.refs.channel_desc).value
+ };
+ this.props.onDataChanged(newData);
+ }
+ render() {
+ var displayNameError = null;
+ var serverError = null;
+ var displayNameClass = 'form-group';
+
+ if (this.state.displayNameError) {
+ displayNameError = <p className='input__help error'>{this.state.displayNameError}</p>;
+ displayNameClass += ' has-error';
+ }
+
+ if (this.props.serverError) {
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
+ }
+
+ var channelTerm = '';
+ var channelSwitchText = '';
+ switch (this.props.channelType) {
+ case 'P':
+ channelTerm = 'Group';
+ channelSwitchText = (
+ <div className='modal-intro'>
+ {'Create a new private group with restricted membership. '}
+ <a
+ href='#'
+ onClick={this.props.onTypeSwitched}
+ >
+ {'Create a public channel'}
+ </a>
+ </div>
+ );
+ break;
+ case 'O':
+ channelTerm = 'Channel';
+ channelSwitchText = (
+ <div className='modal-intro'>
+ {'Create a new public channel anyone can join. '}
+ <a
+ href='#'
+ onClick={this.props.onTypeSwitched}
+ >
+ {'Create a private group'}
+ </a>
+ </div>
+ );
+ break;
+ }
+
+ const prettyTeamURL = Utils.getShortenedTeamURL();
+
+ return (
+ <span>
+ <Modal
+ show={this.props.show}
+ bsSize='large'
+ onHide={this.props.onModalDismissed}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'New ' + channelTerm}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div>
+ {channelSwitchText}
+ </div>
+ <div className={displayNameClass}>
+ <label className='col-sm-3 form__label control-label'>{'Name'}</label>
+ <div className='col-sm-9'>
+ <input
+ onChange={this.handleChange}
+ type='text'
+ ref='display_name'
+ className='form-control'
+ placeholder='Ex: "Bugs", "Marketing", "办公室恋情"'
+ maxLength='22'
+ value={this.props.channelData.displayName}
+ autoFocus={true}
+ tabIndex='1'
+ />
+ {displayNameError}
+ <p className='input__help dark'>
+ {'URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
+ <a
+ href='#'
+ onClick={this.props.onChangeURLPressed}
+ >
+ {'Edit'}
+ </a>
+ {')'}
+ </p>
+ </div>
+ </div>
+ <div className='form-group less'>
+ <div className='col-sm-3'>
+ <label className='form__label control-label'>{'Description'}</label>
+ <label className='form__label light'>{'(optional)'}</label>
+ </div>
+ <div className='col-sm-9'>
+ <textarea
+ className='form-control no-resize'
+ ref='channel_desc'
+ rows='4'
+ placeholder='Description'
+ maxLength='1024'
+ value={this.props.channelData.description}
+ onChange={this.handleChange}
+ tabIndex='2'
+ />
+ <p className='input__help'>
+ {'Description helps others decide whether to join this channel.'}
+ </p>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onModalDismissed}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Create New ' + channelTerm}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
+ );
+ }
+}
+
+NewChannelModal.defaultProps = {
+ show: false,
+ channelType: 'O',
+ serverError: ''
+};
+NewChannelModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ channelType: React.PropTypes.string.isRequired,
+ channelData: React.PropTypes.object.isRequired,
+ serverError: React.PropTypes.string,
+ onSubmitChannel: React.PropTypes.func.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired,
+ onTypeSwitched: React.PropTypes.func.isRequired,
+ onChangeURLPressed: React.PropTypes.func.isRequired,
+ onDataChanged: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 1b579efbc..dae582627 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
var client = require('../utils/client.jsx');
-import {config} from '../utils/config.js';
export default class PasswordResetForm extends React.Component {
constructor(props) {
@@ -62,7 +61,7 @@ export default class PasswordResetForm extends React.Component {
<div className='signup-team__container'>
<h3>Password Reset</h3>
<form onSubmit={this.handlePasswordReset}>
- <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + config.SiteName + ' account.'}</p>
+ <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.config.SiteName + ' account.'}</p>
<div className={formClass}>
<input
type='password'
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 1e6cc3607..37d4a58cb 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+const Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
export default class PasswordResetSendLink extends React.Component {
@@ -15,8 +16,8 @@ export default class PasswordResetSendLink extends React.Component {
e.preventDefault();
var state = {};
- var email = React.findDOMNode(this.refs.email).value.trim();
- if (!email) {
+ var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ if (!email || !Utils.isEmail(email)) {
state.error = 'Please enter a valid email address.';
this.setState(state);
return;
@@ -67,7 +68,7 @@ export default class PasswordResetSendLink extends React.Component {
<p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p>
<div className={formClass}>
<input
- type='text'
+ type='email'
className='form-control'
name='email'
ref='email'
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index fb9522afb..a2ca8b00f 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -25,7 +25,7 @@ export default class PopoverListMembers extends React.Component {
$('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true});
$('body').on('click', function onClick(e) {
- if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
+ if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
$('#member_popover').popover('hide');
}
});
@@ -65,7 +65,7 @@ export default class PopoverListMembers extends React.Component {
>
{count}
<span
- className='glyphicon glyphicon-user'
+ className='fa fa-user'
aria-hidden='true'
/>
</div>
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 37de4ecc0..9127f00de 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -51,7 +51,7 @@ export default class Post extends React.Component {
var post = this.props.post;
client.createPost(post, post.channel_id,
- function success(data) {
+ (data) => {
AsyncClient.getPosts();
var channel = ChannelStore.get(post.channel_id);
@@ -65,11 +65,11 @@ export default class Post extends React.Component {
post: data
});
},
- function error() {
+ () => {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
this.forceUpdate();
- }.bind(this)
+ }
);
post.state = Constants.POST_LOADING;
@@ -81,31 +81,52 @@ export default class Post extends React.Component {
return true;
}
- return false;
- }
- render() {
- var post = this.props.post;
- var parentPost = this.props.parentPost;
- var posts = this.props.posts;
+ if (nextProps.sameRoot !== this.props.sameRoot) {
+ return true;
+ }
- var type = 'Post';
- if (post.root_id && post.root_id.length > 0) {
- type = 'Comment';
+ if (nextProps.sameUser !== this.props.sameUser) {
+ return true;
+ }
+
+ if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
+ return true;
}
- var commentCount = 0;
- var commentRootId;
+ return false;
+ }
+ getCommentCount(props) {
+ const post = props.post;
+ const parentPost = props.parentPost;
+ const posts = props.posts;
+
+ let commentCount = 0;
+ let commentRootId;
if (parentPost) {
commentRootId = post.root_id;
} else {
commentRootId = post.id;
}
- for (var postId in posts) {
+ for (let postId in posts) {
if (posts[postId].root_id === commentRootId) {
commentCount += 1;
}
}
+ return commentCount;
+ }
+ render() {
+ var post = this.props.post;
+ var parentPost = this.props.parentPost;
+ var posts = this.props.posts;
+
+ var type = 'Post';
+ if (post.root_id && post.root_id.length > 0) {
+ type = 'Comment';
+ }
+
+ const commentCount = this.getCommentCount(this.props);
+
var rootUser;
if (this.props.sameRoot) {
rootUser = 'same--root';
@@ -152,7 +173,7 @@ export default class Post extends React.Component {
return (
<div>
<div
- id={post.id}
+ id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
>
{profilePic}
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index d9b8f20ce..6e98e4aba 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -5,6 +5,7 @@ const FileAttachmentList = require('./file_attachment_list.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
+const TextFormatting = require('../utils/text_formatting.jsx');
const twemoji = require('twemoji');
export default class PostBody extends React.Component {
@@ -16,24 +17,43 @@ export default class PostBody extends React.Component {
const linkData = Utils.extractLinks(this.props.post.message);
this.state = {links: linkData.links, message: linkData.text};
}
+
+ getAllChildNodes(nodeIn) {
+ var textNodes = [];
+
+ function getTextNodes(node) {
+ textNodes.push(node);
+
+ for (var i = 0, len = node.childNodes.length; i < len; ++i) {
+ getTextNodes(node.childNodes[i]);
+ }
+ }
+
+ getTextNodes(nodeIn);
+ return textNodes;
+ }
+
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
}
+
componentDidMount() {
this.parseEmojis();
}
+
componentDidUpdate() {
this.parseEmojis();
}
+
componentWillReceiveProps(nextProps) {
const linkData = Utils.extractLinks(nextProps.post.message);
this.setState({links: linkData.links, message: linkData.text});
}
+
render() {
const post = this.props.post;
const filenames = this.props.post.filenames;
const parentPost = this.props.parentPost;
- const inner = Utils.textToJsx(this.state.message);
let comment = '';
let postClass = '';
@@ -75,7 +95,7 @@ export default class PostBody extends React.Component {
comment = (
<p className='post-link'>
<span>
- Commented on {name}{apostrophe} message:
+ {'Commented on '}{name}{apostrophe}{' message:'}
<a
className='theme'
onClick={this.props.handleCommentClick}
@@ -98,7 +118,7 @@ export default class PostBody extends React.Component {
href='#'
onClick={this.props.retryPost}
>
- Retry
+ {'Retry'}
</a>
);
} else if (post.state === Constants.POST_LOADING) {
@@ -121,7 +141,6 @@ export default class PostBody extends React.Component {
fileAttachmentHolder = (
<FileAttachmentList
filenames={filenames}
- modalId={`view_image_modal_${post.id}`}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -131,12 +150,18 @@ export default class PostBody extends React.Component {
return (
<div className='post-body'>
{comment}
- <p
+ <div
key={`${post.id}_message`}
+ id={`${post.id}_message`}
className={postClass}
>
- {loading}<span>{inner}</span>
- </p>
+ {loading}
+ <span
+ ref='message_span'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ />
+ </div>
{fileAttachmentHolder}
{embed}
</div>
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index c80b287a3..824e7ef39 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -5,6 +5,8 @@ var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -20,7 +22,7 @@ export default class PostInfo extends React.Component {
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = UserStore.getCurrentUser().roles.indexOf('admin') > -1;
+ var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
return '';
@@ -148,12 +150,19 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
+ let tooltip = <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'>
- <time className='post-profile-time'>
- {utils.displayDateTime(post.create_at)}
- </time>
+ <OverlayTrigger
+ placement='top'
+ overlay={tooltip}
+ >
+ <time className='post-profile-time'>
+ {utils.displayDateTime(post.create_at)}
+ </time>
+ </OverlayTrigger>
</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
index 6fa87ca4a..3e1e075bb 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -15,11 +15,9 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
-import {strings} from '../utils/config.js';
-
export default class PostList extends React.Component {
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.gotMorePosts = false;
this.scrolled = false;
@@ -27,6 +25,7 @@ export default class PostList extends React.Component {
this.seenNewMessages = false;
this.isUserScroll = true;
this.userHasSeenNew = false;
+ this.loadInProgress = false;
this.onChange = this.onChange.bind(this);
this.onTimeChange = this.onTimeChange.bind(this);
@@ -34,22 +33,19 @@ export default class PostList extends React.Component {
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.resize = this.resize.bind(this);
- this.state = this.getStateFromStores();
+ this.state = this.getStateFromStores(props.channelId);
this.state.numToDisplay = Constants.POST_CHUNK_SIZE;
this.state.isFirstLoadComplete = false;
}
- getStateFromStores() {
- var channel = ChannelStore.getCurrent();
-
- if (channel == null) {
- channel = {};
- }
-
- var postList = PostStore.getCurrentPosts();
+ getStateFromStores(id) {
+ var postList = PostStore.getPosts(id);
if (postList != null) {
- var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id);
+ var deletedPosts = PostStore.getUnseenDeletedPosts(id);
if (deletedPosts && Object.keys(deletedPosts).length > 0) {
for (var pid in deletedPosts) {
@@ -70,7 +66,7 @@ export default class PostList extends React.Component {
});
}
- var pendingPostList = PostStore.getPendingPosts(channel.id);
+ var pendingPostList = PostStore.getPendingPosts(id);
if (pendingPostList) {
postList.order = pendingPostList.order.concat(postList.order);
@@ -82,45 +78,45 @@ export default class PostList extends React.Component {
}
}
- var lastViewed = Number.MAX_VALUE;
-
- if (ChannelStore.getCurrentMember() != null) {
- lastViewed = ChannelStore.getCurrentMember().last_viewed_at;
- }
-
return {
- postList: postList,
- channel: channel,
- lastViewed: lastViewed
+ postList: 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);
- ChannelStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
- var postHolder = $('.post-list-holder-by-time');
-
- $('.modal').on('show.bs.modal', function onShow() {
- $('.modal-body').css('overflow-y', 'auto');
- $('.modal-body').css('max-height', $(window).height() * 0.7);
- });
-
- $(window).resize(function resize() {
- if ($('#create_post').length > 0) {
- var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
- postHolder.css('height', height + 'px');
- }
+ const postHolder = $(React.findDOMNode(this.refs.postlist));
+ $(window).resize(() => {
+ this.resize();
if (!this.scrolled) {
this.scrollToBottom();
}
- }.bind(this));
+ });
- postHolder.scroll(function scroll() {
- var position = postHolder.scrollTop() + postHolder.height() + 14;
- var bottom = postHolder[0].scrollHeight;
+ postHolder.on('scroll', () => {
+ const position = postHolder.scrollTop() + postHolder.height() + 14;
+ const bottom = postHolder[0].scrollHeight;
if (position >= bottom) {
this.scrolled = false;
@@ -132,45 +128,33 @@ export default class PostList extends React.Component {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
- }.bind(this));
-
- $('body').on('click.userpopover', function popOver(e) {
- if ($(e.target).attr('data-toggle') !== 'popover' &&
- $(e.target).parents('.popover.in').length === 0) {
- $('.user-popover').popover('hide');
- }
});
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
- $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
- if (ev.type === 'mouseenter') {
- $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
- $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
- } else {
- $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
- $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
- }
- });
-
- $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
- if (ev.type === 'mouseenter') {
- $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
- $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
- } else {
- $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
- $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
- }
- });
+ if (!this.state.isFirstLoadComplete) {
+ this.loadFirstPosts(this.props.channelId);
+ }
+ this.resize();
+ this.onChange();
this.scrollToBottom();
-
- if (this.state.channel.id != null) {
- this.loadFirstPosts(this.state.channel.id);
- }
+ }
+ deactivate() {
+ PostStore.removeChangeListener(this.onChange);
+ UserStore.removeStatusesChangeListener(this.onTimeChange);
+ SocketStore.removeChangeListener(this.onSocketChange);
+ $('body').off('click.userpopover');
+ $(window).off('resize');
+ var postHolder = $(React.findDOMNode(this.refs.postlist));
+ postHolder.off('scroll');
}
componentDidUpdate(prevProps, prevState) {
+ if (!this.props.isActive) {
+ return;
+ }
+
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
@@ -187,7 +171,7 @@ export default class PostList extends React.Component {
var firstPost = posts[order[0]] || {};
var isNewPost = oldOrder.indexOf(order[0]) === -1;
- if (this.state.channel.id !== prevState.channel.id) {
+ if (this.props.isActive && !prevProps.isActive) {
this.scrollToBottom();
} else if (oldOrder.length === 0) {
this.scrollToBottom();
@@ -201,39 +185,51 @@ export default class PostList extends React.Component {
} else if (isNewPost &&
userId === firstPost.user_id &&
!utils.isComment(firstPost)) {
- this.state.lastViewed = utils.getTimestamp();
this.scrollToBottom(true);
// the user clicked 'load more messages'
- } else if (this.gotMorePosts) {
- var lastPost = oldPosts[oldOrder[prevState.numToDisplay]];
- $('#' + lastPost.id)[0].scrollIntoView();
+ } 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 = $('.post-list-holder-by-time');
+ var postHolder = $(React.findDOMNode(this.refs.postlist));
this.prevScrollTop = postHolder.scrollTop();
}
- componentWillUnmount() {
- PostStore.removeChangeListener(this.onChange);
- ChannelStore.removeChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onTimeChange);
- SocketStore.removeChangeListener(this.onSocketChange);
- $('body').off('click.userpopover');
- $('.modal').off('show.bs.modal');
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.isActive === true && this.props.isActive === false) {
+ this.activate();
+ } else if (nextProps.isActive === false && this.props.isActive === true) {
+ this.deactivate();
+ }
+ }
+ resize() {
+ const postHolder = $(React.findDOMNode(this.refs.postlist));
+ if ($('#create_post').length > 0) {
+ const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
+ postHolder.css('height', height + 'px');
+ }
}
scrollTo(val) {
this.isUserScroll = false;
- var postHolder = $('.post-list-holder-by-time');
+ var postHolder = $(React.findDOMNode(this.refs.postlist));
postHolder[0].scrollTop = val;
}
scrollToBottom(force) {
this.isUserScroll = false;
- var postHolder = $('.post-list-holder-by-time');
- if ($('#new_message')[0] && !this.userHasSeenNew && !force) {
- $('#new_message')[0].scrollIntoView();
+ var postHolder = $(React.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;
@@ -241,34 +237,32 @@ export default class PostList extends React.Component {
}
}
loadFirstPosts(id) {
+ if (this.loadInProgress) {
+ return;
+ }
+
+ if (this.props.channelId == null) {
+ return;
+ }
+
+ this.loadInProgress = true;
Client.getPosts(
id,
PostStore.getLatestUpdate(id),
function success() {
+ this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
}.bind(this),
function fail() {
+ this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
}.bind(this)
);
}
onChange() {
- var newState = this.getStateFromStores();
-
- // Special case where the channel wasn't yet set in componentDidMount
- if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) {
- this.loadFirstPosts(newState.channel.id);
- }
-
- if (!utils.areStatesEqual(newState, this.state)) {
- if (this.state.channel.id !== newState.channel.id) {
- PostStore.clearUnseenDeletedPosts(this.state.channel.id);
- this.userHasSeenNew = false;
- newState.numToDisplay = Constants.POST_CHUNK_SIZE;
- } else {
- newState.lastViewed = this.state.lastViewed;
- }
+ var newState = this.getStateFromStores(this.props.channelId);
+ if (!utils.areStatesEqual(newState.postList, this.state.postList)) {
this.setState(newState);
}
}
@@ -332,8 +326,8 @@ export default class PostList extends React.Component {
<strong><UserProfile userId={teammate.id} /></strong>
</div>
<p className='channel-intro-text'>
- {'This is the start of your private message history with ' + teammateName + '.'}<br/>
- {'Private messages and files shared here are not shown to people outside this area.'}
+ {'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'
@@ -352,7 +346,7 @@ export default class PostList extends React.Component {
return (
<div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}</p>
+ <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>
);
}
@@ -374,7 +368,7 @@ export default class PostList extends React.Component {
<p className='channel-intro__content'>
Welcome to {channel.display_name}!
<br/><br/>
- This is the first channel {strings.Team}mates see when they
+ This is the first channel teammates see when they
<br/>
sign up - use it for posting updates everyone needs to know.
<br/><br/>
@@ -424,9 +418,9 @@ export default class PostList extends React.Component {
}
}
- var members = ChannelStore.getCurrentExtraInfo().members;
+ var members = ChannelStore.getExtraInfo(channel.id).members;
for (var i = 0; i < members.length; i++) {
- if (members[i].roles.indexOf('admin') > -1) {
+ if (utils.isAdmin(members[i].roles)) {
return members[i].username;
}
}
@@ -488,6 +482,11 @@ export default class PostList extends React.Component {
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) {
@@ -543,13 +542,13 @@ export default class PostList extends React.Component {
);
}
- if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) {
+ if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
renderedLastViewed = true;
// Temporary fix to solve ie10/11 rendering issue
let newSeparatorId = '';
if (!utils.isBrowserIE()) {
- newSeparatorId = 'new_message';
+ newSeparatorId = 'new_message_' + this.props.channelId;
}
postCtls.push(
<div
@@ -577,7 +576,7 @@ export default class PostList extends React.Component {
var posts = this.state.postList.posts;
var order = this.state.postList.order;
- var channelId = this.state.channel.id;
+ var channelId = this.props.channelId;
$(React.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
@@ -619,7 +618,7 @@ export default class PostList extends React.Component {
render() {
var order = [];
var posts;
- var channel = this.state.channel;
+ var channel = ChannelStore.get(this.props.channelId);
if (this.state.postList != null) {
posts = this.state.postList.posts;
@@ -628,7 +627,7 @@ export default class PostList extends React.Component {
var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
if (channel != null) {
- if (order.length > this.state.numToDisplay) {
+ if (order.length >= this.state.numToDisplay) {
moreMessages = (
<a
ref='loadmore'
@@ -655,10 +654,15 @@ export default class PostList extends React.Component {
/>);
}
+ var activeClass = '';
+ if (!this.props.isActive) {
+ activeClass = 'inactive';
+ }
+
return (
<div
ref='postlist'
- className='post-list-holder-by-time'
+ className={'post-list-holder-by-time ' + activeClass}
>
<div className='post-list__table'>
<div className='post-list__content'>
@@ -670,3 +674,12 @@ export default class PostList extends React.Component {
);
}
}
+
+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
new file mode 100644
index 000000000..e59d85d41
--- /dev/null
+++ b/web/react/components/post_list_container.jsx
@@ -0,0 +1,63 @@
+// Copyright (c) 2015 Spinpunch, 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/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
new file mode 100644
index 000000000..473ff3f91
--- /dev/null
+++ b/web/react/components/register_app_modal.jsx
@@ -0,0 +1,249 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../utils/client.jsx');
+
+export default class RegisterAppModal extends React.Component {
+ constructor() {
+ super();
+
+ this.register = this.register.bind(this);
+ this.onHide = this.onHide.bind(this);
+ this.save = this.save.bind(this);
+
+ this.state = {clientId: '', clientSecret: '', saved: false};
+ }
+ componentDidMount() {
+ $(React.findDOMNode(this)).on('hide.bs.modal', this.onHide);
+ }
+ register() {
+ var state = this.state;
+ state.serverError = null;
+
+ var app = {};
+
+ var name = this.refs.name.getDOMNode().value;
+ if (!name || name.length === 0) {
+ state.nameError = 'Application name must be filled in.';
+ this.setState(state);
+ return;
+ }
+ state.nameError = null;
+ app.name = name;
+
+ var homepage = this.refs.homepage.getDOMNode().value;
+ if (!homepage || homepage.length === 0) {
+ state.homepageError = 'Homepage must be filled in.';
+ this.setState(state);
+ return;
+ }
+ state.homepageError = null;
+ app.homepage = homepage;
+
+ var desc = this.refs.desc.getDOMNode().value;
+ app.description = desc;
+
+ var rawCallbacks = this.refs.callback.getDOMNode().value.trim();
+ if (!rawCallbacks || rawCallbacks.length === 0) {
+ state.callbackError = 'At least one callback URL must be filled in.';
+ this.setState(state);
+ return;
+ }
+ state.callbackError = null;
+ app.callback_urls = rawCallbacks.split('\n');
+
+ Client.registerOAuthApp(app,
+ (data) => {
+ state.clientId = data.id;
+ state.clientSecret = data.client_secret;
+ this.setState(state);
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+ onHide(e) {
+ if (!this.state.saved && this.state.clientId !== '') {
+ e.preventDefault();
+ return;
+ }
+
+ this.setState({clientId: '', clientSecret: '', saved: false});
+ }
+ save() {
+ this.setState({saved: this.refs.save.getDOMNode().checked});
+ }
+ render() {
+ var nameError;
+ if (this.state.nameError) {
+ nameError = <div className='form-group has-error'><label className='control-label'>{this.state.nameError}</label></div>;
+ }
+ var homepageError;
+ if (this.state.homepageError) {
+ homepageError = <div className='form-group has-error'><label className='control-label'>{this.state.homepageError}</label></div>;
+ }
+ var callbackError;
+ if (this.state.callbackError) {
+ callbackError = <div className='form-group has-error'><label className='control-label'>{this.state.callbackError}</label></div>;
+ }
+ var serverError;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ 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>
+ <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 {
+ var btnClass = ' disabled';
+ if (this.state.saved) {
+ btnClass = '';
+ }
+
+ body = (
+ <div className='form-group user-settings'>
+ <h3>{'Your Application Credentials'}</h3>
+ <br/>
+ <br/>
+ <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
+ <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
+ <br/>
+ <br/>
+ <br/>
+ <br/>
+ <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
+ <br/>
+ <br/>
+ <div className='checkbox'>
+ <label>
+ <input
+ ref='save'
+ type='checkbox'
+ checked={this.state.saved}
+ onClick={this.save}
+ >
+ {'I have saved both my Client Id and Client Secret somewhere safe'}
+ </input>
+ </label>
+ </div>
+ <a
+ className={'btn btn-sm btn-primary pull-right' + btnClass}
+ href='#'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ {'Close'}
+ </a>
+ </div>
+ );
+ }
+
+ return (
+ <div
+ className='modal fade'
+ ref='modal'
+ id='register_app'
+ role='dialog'
+ aria-hidden='true'
+ >
+ <div className='modal-dialog'>
+ <div className='modal-content'>
+ <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'
+ >
+ {'Developer Applications'}
+ </h4>
+ </div>
+ <div className='modal-body'>
+ {body}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index f1a90102c..5b4694eb1 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -12,6 +12,7 @@ var FileAttachmentList = require('./file_attachment_list.jsx');
var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var ActionTypes = Constants.ActionTypes;
+var TextFormatting = require('../utils/text_formatting.jsx');
var twemoji = require('twemoji');
export default class RhsComment extends React.Component {
@@ -84,7 +85,6 @@ export default class RhsComment extends React.Component {
type = 'Comment';
}
- var message = Utils.textToJsx(post.message);
var timestamp = UserStore.getCurrentUser().update_at;
var loading;
@@ -113,14 +113,7 @@ export default class RhsComment extends React.Component {
var ownerOptions;
if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
ownerOptions = (
- <div
- className='dropdown'
- onClick={
- function scroll() {
- $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);
- }
- }
- >
+ <div className='dropdown'>
<a
href='#'
className='dropdown-toggle theme'
@@ -170,7 +163,6 @@ export default class RhsComment extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_comment_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -193,7 +185,7 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
- <time className='post-right-comment-time'>
+ <time className='post-profile-time'>
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
@@ -202,7 +194,14 @@ export default class RhsComment extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p className={postClass}>{loading}{message}</p>
+ <p className={postClass}>
+ {loading}
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ </p>
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx
index 5156ec4d7..f55c4095e 100644
--- a/web/react/components/rhs_header_post.jsx
+++ b/web/react/components/rhs_header_post.jsx
@@ -65,6 +65,7 @@ export default class RhsHeaderPost extends React.Component {
aria-label='Close'
onClick={this.handleClose}
>
+ <i className='fa fa-sign-out'/>
</button>
</div>
);
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 83b57b955..13ab0c982 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -4,6 +4,7 @@
var ChannelStore = require('../stores/channel_store.jsx');
var UserProfile = require('./user_profile.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
var utils = require('../utils/utils.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var twemoji = require('twemoji');
@@ -35,7 +36,6 @@ export default class RhsRootPost extends React.Component {
}
render() {
var post = this.props.post;
- var message = utils.textToJsx(post.message);
var isOwner = UserStore.getCurrentId() === post.user_id;
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -53,7 +53,7 @@ export default class RhsRootPost extends React.Component {
var channelName;
if (channel) {
if (channel.type === 'D') {
- channelName = 'Private Message';
+ channelName = 'Direct Message';
} else {
channelName = channel.display_name;
}
@@ -111,7 +111,6 @@ export default class RhsRootPost extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -132,7 +131,11 @@ export default class RhsRootPost extends React.Component {
<div className='post__content'>
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
- <li className='post-header-col'><time className='post-right-root-time'>{utils.displayCommentDateTime(post.create_at)}</time></li>
+ <li className='post-header-col'>
+ <time className='post-profile-time'>
+ {utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
{ownerOptions}
@@ -140,7 +143,11 @@ export default class RhsRootPost extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p>{message}</p>
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 006d15459..77166fef9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -75,6 +75,9 @@ export default class SearchBar extends React.Component {
PostStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
}
+ handleMouseInput(e) {
+ e.preventDefault();
+ }
handleUserFocus(e) {
e.target.select();
$('.search-bar__container').addClass('focused');
@@ -140,6 +143,7 @@ export default class SearchBar extends React.Component {
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onChange={this.handleUserInput}
+ onMouseUp={this.handleMouseInput}
/>
{isSearching}
</form>
diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx
index 694f0c55d..4e8a3ef10 100644
--- a/web/react/components/search_results_header.jsx
+++ b/web/react/components/search_results_header.jsx
@@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component {
title='Close'
onClick={this.handleClose}
>
+ <i className='fa fa-sign-out'/>
</button>
</div>
);
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index aa56f1174..32b521560 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -10,6 +10,7 @@ var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Constants = require('../utils/constants.jsx');
+var TextFormatting = require('../utils/text_formatting.jsx');
var ActionTypes = Constants.ActionTypes;
export default class SearchResultsItem extends React.Component {
@@ -56,7 +57,6 @@ export default class SearchResultsItem extends React.Component {
}
render() {
- var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch});
var channelName = '';
var channel = ChannelStore.get(this.props.post.channel_id);
var timestamp = UserStore.getCurrentUser().update_at;
@@ -64,10 +64,15 @@ export default class SearchResultsItem extends React.Component {
if (channel) {
channelName = channel.display_name;
if (channel.type === 'D') {
- channelName = 'Private Message';
+ channelName = 'Direct Message';
}
}
+ const formattingOptions = {
+ searchTerm: this.props.term,
+ mentionHighlight: this.props.isMentionSearch
+ };
+
return (
<div
className='search-item-container post'
@@ -91,7 +96,12 @@ export default class SearchResultsItem extends React.Component {
</time>
</li>
</ul>
- <div className='search-item-snippet'><span>{message}</span></div>
+ <div className='search-item-snippet'>
+ <span
+ onClick={this.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ />
+ </div>
</div>
</div>
);
@@ -102,4 +112,4 @@ SearchResultsItem.propTypes = {
post: React.PropTypes.object,
isMentionSearch: React.PropTypes.bool,
term: React.PropTypes.string
-}; \ No newline at end of file
+};
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index b1bab1d48..1bffa7c79 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -26,7 +26,7 @@ export default class SettingItemMax extends React.Component {
href='#'
onClick={this.props.submit}
>
- Submit
+ Save
</a>
);
}
diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx
index 098729a4f..2c0fdf2f4 100644
--- a/web/react/components/setting_item_min.jsx
+++ b/web/react/components/setting_item_min.jsx
@@ -12,14 +12,18 @@ export default class SettingItemMin extends React.Component {
href='#'
onClick={this.props.updateSection}
>
- Edit
+ <i className='fa fa-pencil'/>
+ {'Edit'}
</a>
</li>
);
}
return (
- <ul className='section-min'>
+ <ul
+ className='section-min'
+ onClick={this.props.updateSection}
+ >
<li className='col-sm-10 section-title'>{this.props.title}</li>
{editButton}
<li className='col-sm-7 section-describe'>{this.props.describe}</li>
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index a53112651..ddad4fd53 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-import {config} from '../utils/config.js';
-
export default class SettingPicture extends React.Component {
constructor(props) {
super(props);
@@ -81,7 +79,7 @@ export default class SettingPicture extends React.Component {
>Save</a>
);
}
- var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + config.ProfileWidth + 'px in width and ' + config.ProfileHeight + 'px height.';
+ var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.config.ProfileWidth + 'px in width and ' + global.window.config.ProfileHeight + 'px height.';
var self = this;
return (
diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx
index 5979091c4..fad27b355 100644
--- a/web/react/components/setting_upload.jsx
+++ b/web/react/components/setting_upload.jsx
@@ -64,9 +64,9 @@ export default class SettingsUpload extends React.Component {
}
return (
<ul className='section-max'>
- <li className='col-xs-12 section-title'>{this.props.title}</li>
- <li className='col-xs-offset-3'>{this.props.helpText}</li>
- <li className='col-xs-offset-3 col-xs-8'>
+ <li className='col-sm-12 section-title'>{this.props.title}</li>
+ <li className='col-sm-offset-3 col-sm-9'>{this.props.helpText}</li>
+ <li className='col-sm-offset-3 col-sm-9'>
<ul className='setting-list'>
<li className='setting-list-item'>
<span className='btn btn-sm btn-primary btn-file sel-btn'>
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index e5cbd6e92..4c4675788 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -7,7 +7,8 @@ export default class SettingsSidebar extends React.Component {
this.handleClick = this.handleClick.bind(this);
}
- handleClick(tab) {
+ handleClick(tab, e) {
+ e.preventDefault();
this.props.updateTab(tab.name);
$('.settings-modal').addClass('display--content');
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 983260187..14664ed4d 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -12,6 +12,8 @@ var Utils = require('../utils/utils.jsx');
var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx');
var Constants = require('../utils/constants.jsx');
+var NewChannelFlow = require('./new_channel_flow.jsx');
+var UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -28,6 +30,7 @@ export default class Sidebar extends React.Component {
this.createChannelElement = this.createChannelElement.bind(this);
this.state = this.getStateFromStores();
+ this.state.modal = '';
this.state.loadingDMChannel = -1;
}
getStateFromStores() {
@@ -151,6 +154,16 @@ export default class Sidebar extends React.Component {
$(window).on('resize', this.onResize);
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areStatesEqual(nextProps, this.props)) {
+ return true;
+ }
+
+ if (!Utils.areStatesEqual(nextState, this.state)) {
+ return true;
+ }
+ return false;
+ }
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
@@ -272,15 +285,16 @@ export default class Sidebar extends React.Component {
this.updateUnreadIndicators();
}
updateUnreadIndicators() {
- var container = $(React.findDOMNode(this.refs.container));
+ const container = $(React.findDOMNode(this.refs.container));
+
+ var showTopUnread = false;
+ var showBottomUnread = false;
if (this.firstUnreadChannel) {
var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel]));
if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) {
- $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'initial');
- } else {
- $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'none');
+ showTopUnread = true;
}
}
@@ -288,11 +302,14 @@ export default class Sidebar extends React.Component {
var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel]));
if (lastUnreadElement.position().top > container.height()) {
- $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'initial');
- } else {
- $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'none');
+ showBottomUnread = true;
}
}
+
+ this.setState({
+ showTopUnread,
+ showBottomUnread
+ });
}
createChannelElement(channel, index) {
var members = this.state.members;
@@ -315,10 +332,12 @@ export default class Sidebar extends React.Component {
if (unread) {
titleClass = 'unread-title';
- if (!this.firstUnreadChannel) {
- this.firstUnreadChannel = channel.name;
+ if (channel.id !== activeId) {
+ if (!this.firstUnreadChannel) {
+ this.firstUnreadChannel = channel.name;
+ }
+ this.lastUnreadChannel = channel.name;
}
- this.lastUnreadChannel = channel.name;
}
var badge = null;
@@ -344,6 +363,11 @@ export default class Sidebar extends React.Component {
);
}
+ var badgeClass;
+ if (msgCount > 0) {
+ badgeClass = 'has-badge';
+ }
+
// set up status icon for direct message channels
var status = null;
if (channel.type === 'D') {
@@ -404,7 +428,7 @@ export default class Sidebar extends React.Component {
className={linkClass}
>
<a
- className={'sidebar-channel ' + titleClass}
+ className={'sidebar-channel ' + titleClass + ' ' + badgeClass}
href={href}
onClick={handleClick}
>
@@ -423,19 +447,13 @@ export default class Sidebar extends React.Component {
this.lastUnreadChannel = null;
// create elements for all 3 types of channels
- var channelItems = this.state.channels.filter(
- function filterPublicChannels(channel) {
- return channel.type === 'O';
- }
- ).map(this.createChannelElement);
+ const publicChannels = this.state.channels.filter((channel) => channel.type === 'O');
+ const publicChannelItems = publicChannels.map(this.createChannelElement);
- var privateChannelItems = this.state.channels.filter(
- function filterPrivateChannels(channel) {
- return channel.type === 'P';
- }
- ).map(this.createChannelElement);
+ const privateChannels = this.state.channels.filter((channel) => channel.type === 'P');
+ const privateChannelItems = privateChannels.map(this.createChannelElement);
- var directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
+ const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
// update the favicon to show if there are any notifications
var link = document.createElement('link');
@@ -471,28 +489,34 @@ export default class Sidebar extends React.Component {
);
}
+ let showChannelModal = false;
+ if (this.state.modal !== '') {
+ showChannelModal = true;
+ }
+
return (
<div>
+ <NewChannelFlow
+ show={showChannelModal}
+ channelType={this.state.modal}
+ onModalDismissed={() => this.setState({modal: ''})}
+ />
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
teamType={this.props.teamType}
/>
<SearchBox />
- <div
- ref='topUnreadIndicator'
- className='nav-pills__unread-indicator nav-pills__unread-indicator-top'
- style={{display: 'none'}}
- >
- Unread post(s) above
- </div>
- <div
- ref='bottomUnreadIndicator'
- className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom'
- style={{display: 'none'}}
- >
- Unread post(s) below
- </div>
+ <UnreadChannelIndicator
+ show={this.state.showTopUnread}
+ extraClass='nav-pills__unread-indicator-top'
+ text={'Unread post(s) above'}
+ />
+ <UnreadChannelIndicator
+ show={this.state.showBottomUnread}
+ extraClass='nav-pills__unread-indicator-bottom'
+ text={'Unread post(s) below'}
+ />
<div
ref='container'
@@ -506,15 +530,13 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
- data-toggle='modal'
- data-target='#new_channel'
- data-channeltype='O'
+ onClick={() => this.setState({modal: 'O'})}
>
- +
+ {'+'}
</a>
</h4>
</li>
- {channelItems}
+ {publicChannelItems}
<li>
<a
href='#'
@@ -535,18 +557,16 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
- data-toggle='modal'
- data-target='#new_channel'
- data-channeltype='P'
+ onClick={() => this.setState({modal: 'P'})}
>
- +
+ {'+'}
</a>
</h4>
</li>
{privateChannelItems}
</ul>
<ul className='nav nav-pills nav-stacked'>
- <li><h4>Private Messages</h4></li>
+ <li><h4>Direct Messages</h4></li>
{directMessageItems}
{directMessageMore}
</ul>
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 0056d7a2f..072c14e0a 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -3,7 +3,6 @@
var NavbarDropdown = require('./navbar_dropdown.jsx');
var UserStore = require('../stores/user_store.jsx');
-import {config} from '../utils/config.js';
export default class SidebarHeader extends React.Component {
constructor(props) {
@@ -13,7 +12,8 @@ export default class SidebarHeader extends React.Component {
this.state = {};
}
- toggleDropdown() {
+ toggleDropdown(e) {
+ e.preventDefault();
if (this.refs.dropdown.blockToggle) {
this.refs.dropdown.blockToggle = false;
return;
@@ -59,7 +59,7 @@ export default class SidebarHeader extends React.Component {
}
SidebarHeader.defaultProps = {
- teamDisplayName: config.SiteName,
+ teamDisplayName: global.window.config.SiteName,
teamType: ''
};
SidebarHeader.propTypes = {
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index bd10a6ef1..f1341d9d7 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -4,9 +4,12 @@
var UserStore = require('../stores/user_store.jsx');
var client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx');
-import {config} from '../utils/config.js';
export default class SidebarRightMenu extends React.Component {
+ componentDidMount() {
+ $('.sidebar--left .dropdown-menu').perfectScrollbar();
+ }
+
constructor(props) {
super(props);
@@ -27,7 +30,7 @@ export default class SidebarRightMenu extends React.Component {
var isAdmin = false;
if (currentUser != null) {
- isAdmin = currentUser.roles.indexOf('admin') > -1;
+ isAdmin = utils.isAdmin(currentUser.roles);
inviteLink = (
<li>
@@ -75,8 +78,8 @@ export default class SidebarRightMenu extends React.Component {
}
var siteName = '';
- if (config.SiteName != null) {
- siteName = config.SiteName;
+ if (global.window.config.SiteName != null) {
+ siteName = global.window.config.SiteName;
}
var teamDisplayName = siteName;
if (this.props.teamDisplayName) {
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index bf08e6508..4112138fa 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -12,38 +12,42 @@ export default class TeamSignUp extends React.Component {
this.updatePage = this.updatePage.bind(this);
- if (props.services.length === 1) {
- if (props.services[0] === Constants.EMAIL_SERVICE) {
- this.state = {page: 'email', service: ''};
- } else {
- this.state = {page: 'service', service: props.services[0]};
- }
- } else {
- this.state = {page: 'choose', service: ''};
+ var count = 0;
+
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
+ count = count + 1;
+ }
+
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ count = count + 1;
+ }
+
+ if (count > 1) {
+ this.state = {page: 'choose'};
+ } else if (global.window.config.EnableSignUpWithEmail === 'true') {
+ this.state = {page: 'email'};
+ } else if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ this.state = {page: 'gitlab'};
}
}
- updatePage(page, service) {
- this.setState({page: page, service: service});
+
+ updatePage(page) {
+ this.setState({page});
}
+
render() {
+ if (this.state.page === 'choose') {
+ return (
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ );
+ }
+
if (this.state.page === 'email') {
return <EmailSignUpPage />;
- } else if (this.state.page === 'service' && this.state.service !== '') {
- return <SSOSignupPage service={this.state.service} />;
+ } else if (this.state.page === 'gitlab') {
+ return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
}
-
- return (
- <ChoosePage
- services={this.props.services}
- updatePage={this.updatePage}
- />
- );
}
}
-
-TeamSignUp.defaultProps = {
- services: []
-};
-TeamSignUp.propTypes = {
- services: React.PropTypes.array
-};
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
index dc0d1d376..9c03c5c2f 100644
--- a/web/react/components/signup_team_complete.jsx
+++ b/web/react/components/signup_team_complete.jsx
@@ -4,7 +4,6 @@
var WelcomePage = require('./team_signup_welcome_page.jsx');
var TeamDisplayNamePage = require('./team_signup_display_name_page.jsx');
var TeamURLPage = require('./team_signup_url_page.jsx');
-var AllowedDomainsPage = require('./team_signup_allowed_domains_page.jsx');
var SendInivtesPage = require('./team_signup_send_invites_page.jsx');
var UsernamePage = require('./team_signup_username_page.jsx');
var PasswordPage = require('./team_signup_password_page.jsx');
@@ -70,15 +69,6 @@ export default class SignupTeamComplete extends React.Component {
);
}
- if (this.state.wizard === 'allowed_domains') {
- return (
- <AllowedDomainsPage
- state={this.state}
- updateParent={this.updateParent}
- />
- );
- }
-
if (this.state.wizard === 'send_invites') {
return (
<SendInivtesPage
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index f078f6169..495159efc 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -1,12 +1,10 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
+var Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
-var Constants = require('../utils/constants.jsx');
-import {config} from '../utils/config.js';
export default class SignupUserComplete extends React.Component {
constructor(props) {
@@ -32,13 +30,26 @@ export default class SignupUserComplete extends React.Component {
handleSubmit(e) {
e.preventDefault();
- this.state.user.username = React.findDOMNode(this.refs.name).value.trim();
+ const providedEmail = React.findDOMNode(this.refs.email).value.trim();
+ if (!providedEmail) {
+ this.setState({nameError: '', emailError: 'This field is required', passwordError: ''});
+ return;
+ }
+
+ if (!Utils.isEmail(providedEmail)) {
+ this.setState({nameError: '', emailError: 'Please enter a valid email address', passwordError: ''});
+ return;
+ }
+
+ this.state.user.email = providedEmail;
+
+ this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!this.state.user.username) {
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
return;
}
- var usernameError = utils.isValidUsername(this.state.user.username);
+ var usernameError = Utils.isValidUsername(this.state.user.username);
if (usernameError === 'Cannot use a reserved word as a username.') {
this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''});
return;
@@ -52,12 +63,6 @@ export default class SignupUserComplete extends React.Component {
return;
}
- this.state.user.email = React.findDOMNode(this.refs.email).value.trim();
- if (!this.state.user.email) {
- this.setState({nameError: '', emailError: 'This field is required', passwordError: ''});
- return;
- }
-
this.state.user.password = React.findDOMNode(this.refs.password).value.trim();
if (!this.state.user.password || this.state.user.password .length < 5) {
this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''});
@@ -79,7 +84,7 @@ export default class SignupUserComplete extends React.Component {
if (this.props.hash > 0) {
BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
}
- window.location.href = '/';
+ window.location.href = '/' + this.props.teamName + '/channels/town-square';
}.bind(this),
function emailLoginFailure(err) {
if (err.message === 'Login failed because email address has not been verified') {
@@ -136,7 +141,7 @@ export default class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
if (this.state.user.email) {
- yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {config.SiteName}.</span>;
+ yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
}
var emailContainerStyle = 'margin--extra';
@@ -162,11 +167,8 @@ export default class SignupUserComplete extends React.Component {
</div>
);
- // add options to log in using another service
- var authServices = JSON.parse(this.props.authServices);
-
var signupMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -179,7 +181,7 @@ export default class SignupUserComplete extends React.Component {
}
var emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className='inner__content'>
@@ -237,11 +239,6 @@ export default class SignupUserComplete extends React.Component {
);
}
- var termsDisclaimer = null;
- if (config.ShowTermsDuringSignup) {
- termsDisclaimer = <p>By creating an account and using Mattermost you are agreeing to our <a href={config.TermsLink}>Terms of Service</a>. If you do not agree, you cannot use this service.</p>;
- }
-
return (
<div>
<form>
@@ -251,12 +248,11 @@ export default class SignupUserComplete extends React.Component {
/>
<h5 className='margin--less'>Welcome to:</h5>
<h2 className='signup-team__name'>{this.props.teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {config.SiteName}</h2>
+ <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2>
<h4 className='color--light'>Let's create your account</h4>
{signupMessage}
{emailSignup}
{serverError}
- {termsDisclaimer}
</form>
</div>
);
@@ -269,7 +265,6 @@ SignupUserComplete.defaultProps = {
teamId: '',
email: '',
data: null,
- authServices: '',
teamDisplayName: ''
};
SignupUserComplete.propTypes = {
@@ -278,6 +273,5 @@ SignupUserComplete.propTypes = {
teamId: React.PropTypes.string,
email: React.PropTypes.string,
data: React.PropTypes.string,
- authServices: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/team_export_tab.jsx b/web/react/components/team_export_tab.jsx
new file mode 100644
index 000000000..2914904ad
--- /dev/null
+++ b/web/react/components/team_export_tab.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../utils/client.jsx');
+
+export default class TeamExportTab extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {status: 'request', link: '', err: ''};
+
+ this.onExportSuccess = this.onExportSuccess.bind(this);
+ this.onExportFailure = this.onExportFailure.bind(this);
+ this.doExport = this.doExport.bind(this);
+ }
+ onExportSuccess(data) {
+ this.setState({status: 'ready', link: data.link, err: ''});
+ }
+ onExportFailure(e) {
+ this.setState({status: 'failure', link: '', err: e.message});
+ }
+ doExport() {
+ if (this.state.status === 'in-progress') {
+ return;
+ }
+ this.setState({status: 'in-progress'});
+ Client.exportTeam(this.onExportSuccess, this.onExportFailure);
+ }
+ render() {
+ var messageSection = '';
+ switch (this.state.status) {
+ case 'request':
+ messageSection = '';
+ break;
+ case 'in-progress':
+ messageSection = (
+ <p className='confirm-import alert alert-warning'>
+ <i className='fa fa-spinner fa-pulse' />
+ {' Exporting...'}
+ </p>
+ );
+ break;
+ case 'ready':
+ messageSection = (
+ <p className='confirm-import alert alert-success'>
+ <i className='fa fa-check' />
+ {' Ready for '}
+ <a
+ href={this.state.link}
+ download={true}
+ >
+ {'download'}
+ </a>
+ </p>
+ );
+ break;
+ case 'failure':
+ messageSection = (
+ <p className='confirm-import alert alert-warning'>
+ <i className='fa fa-warning' />
+ {' Unable to export: ' + this.state.err}
+ </p>
+ );
+ break;
+ }
+
+ return (
+ <div
+ ref='wrapper'
+ className='user-settings'
+ >
+ <h3 className='tab-header'>{'Export'}</h3>
+ <div className='divider-dark first'/>
+ <ul className='section-max'>
+ <li className='col-xs-12 section-title'>{'Export your team'}</li>
+ <li className='col-xs-offset-3 col-xs-8'>
+ <ul className='setting-list'>
+ <li className='setting-list-item'>
+ <a
+ className='btn btn-sm btn-primary btn-file sel-btn'
+ href='#'
+ onClick={this.doExport}
+ >
+ {'Export'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <div className='divider-dark'/>
+ {messageSection}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx
deleted file mode 100644
index 3251746b8..000000000
--- a/web/react/components/team_feature_tab.jsx
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-
-export default class FeatureTab extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitValetFeature = this.submitValetFeature.bind(this);
- this.handleValetRadio = this.handleValetRadio.bind(this);
- this.onUpdateSection = this.onUpdateSection.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
-
- this.state = this.setupInitialState();
- }
- componentWillReceiveProps(newProps) {
- var team = newProps.team;
-
- var allowValet = 'false';
- if (team && team.allow_valet) {
- allowValet = 'true';
- }
-
- this.setState({allowValet: allowValet});
- }
- submitValetFeature() {
- var data = {};
- data.allow_valet = this.state.allowValet;
-
- Client.updateValetFeature(data,
- function success() {
- this.props.updateSection('');
- AsyncClient.getMyTeam();
- }.bind(this),
- function fail(err) {
- var state = this.setupInitialState();
- state.serverError = err;
- this.setState(state);
- }.bind(this)
- );
- }
- handleValetRadio(val) {
- this.setState({allowValet: val});
- React.findDOMNode(this.refs.wrapper).focus();
- }
- onUpdateSection(e) {
- e.preventDefault();
- if (this.props.activeSection === 'valet') {
- this.props.updateSection('');
- } else {
- this.props.updateSection('valet');
- }
- }
- setupInitialState() {
- var allowValet;
- var team = this.props.team;
-
- if (team && team.allow_valet) {
- allowValet = 'true';
- } else {
- allowValet = 'false';
- }
-
- return {allowValet: allowValet};
- }
- render() {
- var clientError = null;
- var serverError = null;
- if (this.state.clientError) {
- clientError = this.state.clientError;
- }
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
-
- var valetSection;
-
- if (this.props.activeSection === 'valet') {
- var valetActive = [false, false];
- if (this.state.allowValet === 'false') {
- valetActive[1] = true;
- } else {
- valetActive[0] = true;
- }
-
- let inputs = [];
-
- inputs.push(
- <div key='teamValetSetting'>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[0]}
- onChange={this.handleValetRadio.bind(this, 'true')}
- >
- On
- </input>
- </label>
- <br/>
- </div>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[1]}
- onChange={this.handleValetRadio.bind(this, 'false')}
- >
- Off
- </input>
- </label>
- <br/>
- </div>
- <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
- </div>
- );
-
- valetSection = (
- <SettingItemMax
- title='Valet (Preview - EXPERTS ONLY)'
- inputs={inputs}
- submit={this.submitValetFeature}
- server_error={serverError}
- client_error={clientError}
- updateSection={this.onUpdateSection}
- />
- );
- } else {
- var describe = '';
- if (this.state.allowValet === 'false') {
- describe = 'Off';
- } else {
- describe = 'On';
- }
-
- valetSection = (
- <SettingItemMin
- title='Valet (Preview - EXPERTS ONLY)'
- describe={describe}
- updateSection={this.onUpdateSection}
- />
- );
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <i className='modal-back'></i>Advanced Features
- </h4>
- </div>
- <div
- ref='wrapper'
- className='user-settings'
- >
- <h3 className='tab-header'>Advanced Features</h3>
- <div className='divider-dark first'/>
- {valetSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-FeatureTab.defaultProps = {
- team: {},
- activeSection: ''
-};
-FeatureTab.propTypes = {
- updateSection: React.PropTypes.func.isRequired,
- team: React.PropTypes.object.isRequired,
- activeSection: React.PropTypes.string.isRequired
-};
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 25139bb95..ca438df78 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -6,7 +6,6 @@ const SettingItemMax = require('./setting_item_max.jsx');
const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
-import {strings} from '../utils/config.js';
export default class GeneralTab extends React.Component {
constructor(props) {
@@ -30,7 +29,7 @@ export default class GeneralTab extends React.Component {
state.clientError = 'This field is required';
valid = false;
} else if (name === this.props.teamDisplayName) {
- state.clientError = 'Please choose a new name for your ' + strings.Team;
+ state.clientError = 'Please choose a new name for your team';
valid = false;
} else {
state.clientError = '';
@@ -99,7 +98,7 @@ export default class GeneralTab extends React.Component {
if (this.props.activeSection === 'name') {
let inputs = [];
- let teamNameLabel = Utils.toTitleCase(strings.Team) + ' Name';
+ let teamNameLabel = 'Team Name';
if (Utils.isMobile()) {
teamNameLabel = '';
}
@@ -123,7 +122,7 @@ export default class GeneralTab extends React.Component {
nameSection = (
<SettingItemMax
- title={`${Utils.toTitleCase(strings.Team)} Name`}
+ title={`Team Name`}
inputs={inputs}
submit={this.handleNameSubmit}
server_error={serverError}
@@ -136,7 +135,7 @@ export default class GeneralTab extends React.Component {
nameSection = (
<SettingItemMin
- title={`${Utils.toTitleCase(strings.Team)} Name`}
+ title={`Team Name`}
describe={describe}
updateSection={this.onUpdateSection}
/>
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 1ab348465..79f03510f 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -34,13 +34,11 @@ export default class TeamImportTab extends React.Component {
render() {
var uploadHelpText = (
<div>
- <br/>
- Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team's public channels.
- <br/><br/>
- The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import.
- <br/><br/>
+ <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
+ <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
+
var uploadSection = (
<SettingUpload
title='Import from Slack'
@@ -58,7 +56,7 @@ export default class TeamImportTab extends React.Component {
break;
case 'in-progress':
messageSection = (
- <p className='confirm-import alert alert-warning'><i className='fa fa-spinner fa-pulse'></i> Importing...</p>
+ <p className='confirm-import alert alert-warning'><i className='fa fa-spinner fa-pulse'></i>{' Importing...'}</p>
);
break;
case 'done':
@@ -99,18 +97,18 @@ export default class TeamImportTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
- ><i className='modal-back'></i>Import</h4>
+ ><i className='modal-back'></i>{'Import'}</h4>
</div>
<div
ref='wrapper'
className='user-settings'
>
- <h3 className='tab-header'>Import</h3>
+ <h3 className='tab-header'>{'Import'}</h3>
<div className='divider-dark first'/>
{uploadSection}
<div className='divider-dark'/>
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 53855fe1c..e91aa20bc 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -3,7 +3,7 @@
var TeamStore = require('../stores/team_store.jsx');
var ImportTab = require('./team_import_tab.jsx');
-var FeatureTab = require('./team_feature_tab.jsx');
+var ExportTab = require('./team_export_tab.jsx');
var GeneralTab = require('./team_general_tab.jsx');
var Utils = require('../utils/utils.jsx');
@@ -24,7 +24,7 @@ export default class TeamSettings extends React.Component {
onChange() {
var team = TeamStore.getCurrent();
if (!Utils.areStatesEqual(this.state.team, team)) {
- this.setState({team: team});
+ this.setState({team});
}
}
render() {
@@ -42,10 +42,10 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'feature':
+ case 'import':
result = (
<div>
- <FeatureTab
+ <ImportTab
team={this.state.team}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
@@ -53,14 +53,10 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'import':
+ case 'export':
result = (
<div>
- <ImportTab
- team={this.state.team}
- activeSection={this.props.activeSection}
- updateSection={this.props.updateSection}
- />
+ <ExportTab />
</div>
);
break;
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 668bf76cf..a96aadccf 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -20,8 +20,8 @@ export default class TeamSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function handleBackClick() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
- $('body').on('click', '.modal-header .close', function handleCloseClick() {
- setTimeout(function removeContent() {
+ $('body').on('click', '.modal-header .close', () => {
+ setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -33,10 +33,12 @@ export default class TeamSettingsModal extends React.Component {
this.setState({activeSection: section});
}
render() {
- let tabs = [];
+ const tabs = [];
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
- tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
+
+ // To enable export uncomment this line
+ //tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
return (
<div
@@ -62,7 +64,7 @@ export default class TeamSettingsModal extends React.Component {
className='modal-title'
ref='title'
>
- Team Settings
+ {'Team Settings'}
</h4>
</div>
<div className='modal-body'>
diff --git a/web/react/components/team_signup_allowed_domains_page.jsx b/web/react/components/team_signup_allowed_domains_page.jsx
deleted file mode 100644
index 721fa142a..000000000
--- a/web/react/components/team_signup_allowed_domains_page.jsx
+++ /dev/null
@@ -1,143 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var Client = require('../utils/client.jsx');
-import {strings} from '../utils/config.js';
-
-export default class TeamSignupAllowedDomainsPage extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitBack = this.submitBack.bind(this);
- this.submitNext = this.submitNext.bind(this);
-
- this.state = {};
- }
- submitBack(e) {
- e.preventDefault();
- this.props.state.wizard = 'team_url';
- this.props.updateParent(this.props.state);
- }
- submitNext(e) {
- e.preventDefault();
-
- if (React.findDOMNode(this.refs.open_network).checked) {
- this.props.state.wizard = 'send_invites';
- this.props.state.team.type = 'O';
- this.props.updateParent(this.props.state);
- return;
- }
-
- if (React.findDOMNode(this.refs.allow).checked) {
- var name = React.findDOMNode(this.refs.name).value.trim();
- var domainRegex = /^\w+\.\w+$/;
- if (!name) {
- this.setState({nameError: 'This field is required'});
- return;
- }
-
- if (!name.trim().match(domainRegex)) {
- this.setState({nameError: 'The domain doesn\'t appear valid'});
- return;
- }
-
- this.props.state.wizard = 'send_invites';
- this.props.state.team.allowed_domains = name;
- this.props.state.team.type = 'I';
- this.props.updateParent(this.props.state);
- } else {
- this.props.state.wizard = 'send_invites';
- this.props.state.team.type = 'I';
- this.props.updateParent(this.props.state);
- }
- }
- render() {
- Client.track('signup', 'signup_team_04_allow_domains');
-
- var nameError = null;
- var nameDivClass = 'form-group';
- if (this.state.nameError) {
- nameError = <label className='control-label'>{this.state.nameError}</label>;
- nameDivClass += ' has-error';
- }
-
- return (
- <div>
- <form>
- <img
- className='signup-team-logo'
- src='/static/images/logo.png'
- />
- <h2>Email Domain</h2>
- <p>
- <div className='checkbox'>
- <label>
- <input
- type='checkbox'
- ref='allow'
- defaultChecked={true}
- />
- {' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}
- </label>
- </div>
- </p>
- <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p>
- <h4>{'Your ' + strings.Team + '\'s domain for emails'}</h4>
- <div className={nameDivClass}>
- <div className='row'>
- <div className='col-sm-9'>
- <div className='input-group'>
- <span className='input-group-addon'>@</span>
- <input
- type='text'
- ref='name'
- className='form-control'
- placeholder=''
- maxLength='128'
- defaultValue={this.props.state.team.allowed_domains}
- autoFocus={true}
- onFocus={this.handleFocus}
- />
- </div>
- </div>
- </div>
- {nameError}
- </div>
- <p>To allow signups from multiple domains, separate each with a comma.</p>
- <p>
- <div className='checkbox'>
- <label>
- <input
- type='checkbox'
- ref='open_network'
- defaultChecked={this.props.state.team.type === 'O'}
- /> Allow anyone to signup to this domain without an invitation.</label>
- </div>
- </p>
- <button
- type='button'
- className='btn btn-default'
- onClick={this.submitBack}
- >
- <i className='glyphicon glyphicon-chevron-left'></i> Back
- </button>&nbsp;
- <button
- type='submit'
- className='btn-primary btn'
- onClick={this.submitNext}
- >
- Next<i className='glyphicon glyphicon-chevron-right'></i>
- </button>
- </form>
- </div>
- );
- }
-}
-
-TeamSignupAllowedDomainsPage.defaultProps = {
- state: {}
-};
-TeamSignupAllowedDomainsPage.propTypes = {
- state: React.PropTypes.object,
- updateParent: React.PropTypes.func
-};
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index acce6ab49..b8264b887 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -1,9 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Constants = require('../utils/constants.jsx');
-import {strings} from '../utils/config.js';
-
export default class ChooseAuthPage extends React.Component {
constructor(props) {
super(props);
@@ -11,7 +8,7 @@ export default class ChooseAuthPage extends React.Component {
}
render() {
var buttons = [];
- if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
buttons.push(
<a
className='btn btn-custom-login gitlab btn-full'
@@ -19,17 +16,17 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickGit(e) {
e.preventDefault();
- this.props.updatePage('service', Constants.GITLAB_SERVICE);
+ this.props.updatePage('gitlab');
}.bind(this)
}
>
<span className='icon' />
- <span>Create new {strings.Team} with GitLab Account</span>
+ <span>{'Create new team with GitLab Account'}</span>
</a>
);
}
- if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
buttons.push(
<a
className='btn btn-custom-login email btn-full'
@@ -37,35 +34,31 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickEmail(e) {
e.preventDefault();
- this.props.updatePage('email', '');
+ this.props.updatePage('email');
}.bind(this)
}
>
<span className='fa fa-envelope' />
- <span>Create new {strings.Team} with email address</span>
+ <span>{'Create new team with email address'}</span>
</a>
);
}
if (buttons.length === 0) {
- buttons = <span>No sign-up methods configured, please contact your system administrator.</span>;
+ buttons = <span>{'No sign-up methods configured, please contact your system administrator.'}</span>;
}
return (
<div>
{buttons}
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span>
+ <span><a href='/find_team'>{'Find my team'}</a></span>
</div>
</div>
);
}
}
-ChooseAuthPage.defaultProps = {
- services: []
-};
ChooseAuthPage.propTypes = {
- services: React.PropTypes.array,
updatePage: React.PropTypes.func
};
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 1849f8222..c0d0ed366 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -3,7 +3,6 @@
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
-import {strings} from '../utils/config.js';
export default class TeamSignupDisplayNamePage extends React.Component {
constructor(props) {
@@ -54,7 +53,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h2>{utils.toTitleCase(strings.Team) + ' Name'}</h2>
+ <h2>{'Team Name'}</h2>
<div className={nameDivClass}>
<div className='row'>
<div className='col-sm-9'>
@@ -73,7 +72,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
{nameError}
</div>
<div>
- {'Name your ' + strings.Team + ' in any language. Your ' + strings.Team + ' name shows in menus and headings.'}
+ {'Name your team in any language. Your team name shows in menus and headings.'}
</div>
<button
type='submit'
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index aa402846b..105e4817a 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -4,7 +4,6 @@
var Client = require('../utils/client.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var UserStore = require('../stores/user_store.jsx');
-import {strings, config} from '../utils/config.js';
export default class TeamSignupPasswordPage extends React.Component {
constructor(props) {
@@ -54,7 +53,7 @@ export default class TeamSignupPasswordPage extends React.Component {
props.state.wizard = 'finished';
props.updateParent(props.state, true);
- window.location.href = '/';
+ window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
}.bind(this),
function loginFail(err) {
if (err.message === 'Login failed because email address has not been verified') {
@@ -123,13 +122,13 @@ export default class TeamSignupPasswordPage extends React.Component {
type='submit'
className='btn btn-primary margin--extra'
id='finish-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'}
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating team...'}
onClick={this.submitNext}
>
Finish
</button>
</div>
- <p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
+ <p>By proceeding to create your account and use {global.window.config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.config.SiteName}.</p>
<div className='margin--extra'>
<a
href='#'
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index 11a9980d7..524bd5b50 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -2,10 +2,7 @@
// See License.txt for license information.
var EmailItem = require('./team_signup_email_item.jsx');
-var Utils = require('../utils/utils.jsx');
-var ConfigStore = require('../stores/config_store.jsx');
var Client = require('../utils/client.jsx');
-import {strings, config} from '../utils/config.js';
export default class TeamSignupSendInvitesPage extends React.Component {
constructor(props) {
@@ -16,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.submitSkip = this.submitSkip.bind(this);
this.keySubmit = this.keySubmit.bind(this);
this.state = {
- emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false)
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
if (!this.state.emailEnabled) {
@@ -26,12 +23,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
}
submitBack(e) {
e.preventDefault();
-
- if (config.AllowSignupDomainsWizard) {
- this.props.state.wizard = 'allowed_domains';
- } else {
- this.props.state.wizard = 'team_url';
- }
+ this.props.state.wizard = 'team_url';
this.props.updateParent(this.props.state);
}
@@ -138,7 +130,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
bottomContent = (
<p className='color--light'>
- {'if you prefer, you can invite ' + strings.Team + ' members later'}
+ {'if you prefer, you can invite team members later'}
<br />
{' and '}
<a
@@ -153,7 +145,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
} else {
content = (
<div className='form-group color--light'>
- {'Email is currently disabled for your ' + strings.Team + ', and emails cannot be sent. Contact your system administrator to enable email and email invitations.'}
+ {'Email is currently disabled for your team, and emails cannot be sent. Contact your system administrator to enable email and email invitations.'}
</div>
);
}
@@ -165,7 +157,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h2>{'Invite ' + Utils.toTitleCase(strings.Team) + ' Members'}</h2>
+ <h2>{'Invite Team Members'}</h2>
{content}
<div className='form-group'>
<button
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index ffe9d9fe8..1b722d611 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -4,7 +4,6 @@
const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const Constants = require('../utils/constants.jsx');
-import {strings, config} from '../utils/config.js';
export default class TeamSignupUrlPage extends React.Component {
constructor(props) {
@@ -51,12 +50,8 @@ export default class TeamSignupUrlPage extends React.Component {
Client.findTeamByName(name,
function success(data) {
if (!data) {
- if (config.AllowSignupDomainsWizard) {
- this.props.state.wizard = 'allowed_domains';
- } else {
- this.props.state.wizard = 'send_invites';
- this.props.state.team.type = 'O';
- }
+ this.props.state.wizard = 'send_invites';
+ this.props.state.team.type = 'O';
this.props.state.team.name = name;
this.props.updateParent(this.props.state);
@@ -97,7 +92,7 @@ export default class TeamSignupUrlPage extends React.Component {
className='signup-team-logo'
src='/static/images/logo.png'
/>
- <h2>{`${Utils.toTitleCase(strings.Team)} URL`}</h2>
+ <h2>{`Team URL`}</h2>
<div className={nameDivClass}>
<div className='row'>
<div className='col-sm-11'>
@@ -124,7 +119,7 @@ export default class TeamSignupUrlPage extends React.Component {
</div>
{nameError}
</div>
- <p>{`Choose the web address of your new ${strings.Team}:`}</p>
+ <p>{`Choose the web address of your new team:`}</p>
<ul className='color--light'>
<li>Short and memorable is best</li>
<li>Use lowercase letters, numbers and dashes</li>
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index b5c8b14df..0053b011d 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -3,7 +3,6 @@
var Utils = require('../utils/utils.jsx');
var Client = require('../utils/client.jsx');
-import {strings} from '../utils/config.js';
export default class TeamSignupUsernamePage extends React.Component {
constructor(props) {
@@ -22,7 +21,7 @@ export default class TeamSignupUsernamePage extends React.Component {
submitNext(e) {
e.preventDefault();
- var name = React.findDOMNode(this.refs.name).value.trim();
+ var name = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
var usernameError = Utils.isValidUsername(name);
if (usernameError === 'Cannot use a reserved word as a username.') {
@@ -55,7 +54,7 @@ export default class TeamSignupUsernamePage extends React.Component {
src='/static/images/logo.png'
/>
<h2 className='margin--less'>Your username</h2>
- <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5>
+ <h5 className='color--light'>{'Select a memorable username that makes it easy for teammates to identify you:'}</h5>
<div className='inner__content margin--extra'>
<div className={nameDivClass}>
<div className='row'>
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 43b7aea0e..626c6a17b 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -4,7 +4,6 @@
var Utils = require('../utils/utils.jsx');
var Client = require('../utils/client.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
-import {config} from '../utils/config.js';
export default class TeamSignupWelcomePage extends React.Component {
constructor(props) {
@@ -112,7 +111,7 @@ export default class TeamSignupWelcomePage extends React.Component {
src='/static/images/logo.png'
/>
<h3 className='sub-heading'>Welcome to:</h3>
- <h1 className='margin--top-none'>{config.SiteName}</h1>
+ <h1 className='margin--top-none'>{global.window.config.SiteName}</h1>
</p>
<p className='margin--less'>Let's set up your new team</p>
<p>
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index d75736bd3..4fb1c0d01 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -3,7 +3,6 @@
const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
-import {strings} from '../utils/config.js';
export default class EmailSignUpPage extends React.Component {
constructor() {
@@ -70,7 +69,7 @@ export default class EmailSignUpPage extends React.Component {
</button>
</div>
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{`Find my ${strings.Team}`}</a></span>
+ <span><a href='/find_team'>{`Find my team`}</a></span>
</div>
</form>
);
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 521c21733..a4972dd8d 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -4,7 +4,6 @@
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var Constants = require('../utils/constants.jsx');
-import {strings} from '../utils/config.js';
export default class SSOSignUpPage extends React.Component {
constructor(props) {
@@ -43,7 +42,7 @@ export default class SSOSignUpPage extends React.Component {
if (data.follow_link) {
window.location.href = data.follow_link;
} else {
- window.location.href = '/';
+ window.location.href = '/' + team.name + '/channels/town-square';
}
},
function fail(err) {
@@ -84,7 +83,7 @@ export default class SSOSignUpPage extends React.Component {
disabled={disabled}
>
<span className='icon'/>
- <span>Create {strings.Team} with GitLab Account</span>
+ <span>Create team with GitLab Account</span>
</a>
);
}
@@ -111,7 +110,7 @@ export default class SSOSignUpPage extends React.Component {
{serverError}
</div>
<div className='form-group margin--extra-2x'>
- <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span>
+ <span><a href='/find_team'>{'Find my team'}</a></span>
</div>
</form>
);
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index ea8126bec..5f5316013 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -5,7 +5,6 @@ const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const PostStore = require('../stores/post_store.jsx');
const CommandList = require('./command_list.jsx');
const ErrorStore = require('../stores/error_store.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
@@ -18,7 +17,6 @@ export default class Textbox extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
- this.onTimerInterrupt = this.onTimerInterrupt.bind(this);
this.updateMentionTab = this.updateMentionTab.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
@@ -35,8 +33,7 @@ export default class Textbox extends React.Component {
this.state = {
mentionText: '-1',
mentions: [],
- connection: '',
- timerInterrupt: null
+ connection: ''
};
this.caret = -1;
@@ -44,6 +41,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = false;
this.mentions = [];
}
+
getStateFromStores() {
const error = ErrorStore.getLastError();
@@ -53,6 +51,7 @@ export default class Textbox extends React.Component {
return {message: null};
}
+
componentDidMount() {
PostStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
@@ -60,46 +59,28 @@ export default class Textbox extends React.Component {
this.resize();
this.updateMentionTab(null);
}
+
componentWillUnmount() {
PostStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
+
onListenerChange(id, username) {
if (id === this.props.id) {
this.addMention(username);
}
}
- onRecievedError() {
- const errorState = this.getStateFromStores();
- if (this.state.timerInterrupt !== null) {
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
+ onRecievedError() {
+ const errorState = ErrorStore.getLastError();
- if (errorState.message === 'There appears to be a problem with your internet connection') {
+ if (errorState && errorState.connErrorCount > 0) {
this.setState({connection: 'bad-connection'});
- const timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000);
- this.setState({timerInterrupt: timerInterrupt});
} else {
this.setState({connection: ''});
}
}
- onTimerInterrupt() {
- // Since these should only happen when you have no connection and slightly briefly after any
- // performance hit should not matter
- if (this.state.connection === 'bad-connection') {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
-
- AsyncClient.updateLastViewedAt();
- }
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
componentDidUpdate() {
if (this.caret >= 0) {
Utils.setCaretPosition(React.findDOMNode(this.refs.message), this.caret);
@@ -111,6 +92,7 @@ export default class Textbox extends React.Component {
}
this.resize();
}
+
componentWillReceiveProps(nextProps) {
if (!this.addedMention) {
this.checkForNewMention(nextProps.messageText);
@@ -122,19 +104,22 @@ export default class Textbox extends React.Component {
this.addedMention = false;
this.refs.commands.getSuggestedCommands(nextProps.messageText);
}
+
updateMentionTab(mentionText) {
// using setTimeout so dispatch isn't called during an in progress dispatch
- setTimeout(function updateMentionTabAfterTimeout() {
+ setTimeout(() => {
AppDispatcher.handleViewAction({
type: ActionTypes.RECIEVED_MENTION_DATA,
id: this.props.id,
mention_text: mentionText
});
- }.bind(this), 1);
+ }, 1);
}
+
handleChange() {
this.props.onUserInput(React.findDOMNode(this.refs.message).value);
}
+
handleKeyPress(e) {
const text = React.findDOMNode(this.refs.message).value;
@@ -157,6 +142,7 @@ export default class Textbox extends React.Component {
this.props.onKeyPress(e);
}
+
handleKeyDown(e) {
if (Utils.getSelectedText(React.findDOMNode(this.refs.message)) !== '') {
this.doProcessMentions = true;
@@ -166,6 +152,7 @@ export default class Textbox extends React.Component {
this.handleBackspace(e);
}
}
+
handleBackspace() {
const text = React.findDOMNode(this.refs.message).value;
if (text.indexOf('/') === 0) {
@@ -185,6 +172,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = true;
}
}
+
checkForNewMention(text) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -211,6 +199,7 @@ export default class Textbox extends React.Component {
const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
this.updateMentionTab(name);
}
+
addMention(name) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -233,11 +222,13 @@ export default class Textbox extends React.Component {
this.props.onUserInput(`${prefix}@${name} ${suffix}`);
}
+
addCommand(cmd) {
const elm = React.findDOMNode(this.refs.message);
elm.value = cmd;
this.handleChange();
}
+
resize() {
const e = React.findDOMNode(this.refs.message);
const w = React.findDOMNode(this.refs.wrapper);
@@ -264,21 +255,25 @@ export default class Textbox extends React.Component {
this.props.onHeightChange();
}
}
+
handleFocus() {
const elm = React.findDOMNode(this.refs.message);
if (elm.title === elm.value) {
elm.value = '';
}
}
+
handleBlur() {
const elm = React.findDOMNode(this.refs.message);
if (elm.value === '') {
elm.value = elm.title;
}
}
+
handlePaste() {
this.doProcessMentions = true;
}
+
render() {
return (
<div
diff --git a/web/react/components/unread_channel_indicator.jsx b/web/react/components/unread_channel_indicator.jsx
new file mode 100644
index 000000000..12a67633e
--- /dev/null
+++ b/web/react/components/unread_channel_indicator.jsx
@@ -0,0 +1,35 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+// Indicator for the left sidebar which indicate if there's unread posts in a channel that is not shown
+// because it is either above or below the screen
+export default class UnreadChannelIndicator extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ let displayValue = 'none';
+ if (this.props.show) {
+ displayValue = 'initial';
+ }
+ return (
+ <div
+ className={'nav-pills__unread-indicator ' + this.props.extraClass}
+ style={{display: displayValue}}
+ >
+ {this.props.text}
+ </div>
+ );
+ }
+}
+
+UnreadChannelIndicator.defaultProps = {
+ show: false,
+ extraClass: '',
+ text: ''
+};
+UnreadChannelIndicator.propTypes = {
+ show: React.PropTypes.bool,
+ extraClass: React.PropTypes.string,
+ text: React.PropTypes.string
+};
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 739084053..c5d028d31 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -3,7 +3,6 @@
var Utils = require('../utils/utils.jsx');
var UserStore = require('../stores/user_store.jsx');
-import {config} from '../utils/config.js';
var id = 0;
@@ -58,7 +57,7 @@ export default class UserProfile extends React.Component {
}
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
- if (!config.ShowEmail) {
+ if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
} else {
dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
new file mode 100644
index 000000000..c680d75d1
--- /dev/null
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -0,0 +1,111 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Constants = require('../../utils/constants.jsx');
+
+export default class CustomThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onPickerChange = this.onPickerChange.bind(this);
+ this.onInputChange = this.onInputChange.bind(this);
+ this.pasteBoxChange = this.pasteBoxChange.bind(this);
+
+ this.state = {};
+ }
+ componentDidMount() {
+ $('.color-picker').colorpicker().on('changeColor', this.onPickerChange);
+ }
+ onPickerChange(e) {
+ const theme = this.props.theme;
+ theme[e.target.id] = e.color.toHex();
+ theme.type = 'custom';
+ this.props.updateTheme(theme);
+ }
+ onInputChange(e) {
+ const theme = this.props.theme;
+ theme[e.target.parentNode.id] = e.target.value;
+ theme.type = 'custom';
+ this.props.updateTheme(theme);
+ }
+ pasteBoxChange(e) {
+ const text = e.target.value;
+
+ if (text.length === 0) {
+ return;
+ }
+
+ const colors = text.split(',');
+
+ const theme = {type: 'custom'};
+ let index = 0;
+ Constants.THEME_ELEMENTS.forEach((element) => {
+ if (index < colors.length) {
+ theme[element.id] = colors[index];
+ }
+ index++;
+ });
+
+ this.props.updateTheme(theme);
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const elements = [];
+ let colors = '';
+ Constants.THEME_ELEMENTS.forEach((element, index) => {
+ elements.push(
+ <div
+ className='col-sm-4 form-group'
+ key={'custom-theme-key' + index}
+ >
+ <label className='custom-label'>{element.uiName}</label>
+ <div
+ className='input-group color-picker'
+ id={element.id}
+ >
+ <input
+ className='form-control'
+ type='text'
+ defaultValue={theme[element.id]}
+ onChange={this.onInputChange}
+ />
+ <span className='input-group-addon'><i></i></span>
+ </div>
+ </div>
+ );
+
+ colors += theme[element.id] + ',';
+ });
+
+ const pasteBox = (
+ <div className='col-sm-12'>
+ <label className='custom-label'>
+ {'Copy and paste to share theme colors:'}
+ </label>
+ <input
+ type='text'
+ className='form-control'
+ value={colors}
+ onChange={this.pasteBoxChange}
+ />
+ </div>
+ );
+
+ return (
+ <div>
+ <div className='row form-group'>
+ {elements}
+ </div>
+ <div className='row'>
+ {pasteBox}
+ </div>
+ </div>
+ );
+ }
+}
+
+CustomThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
new file mode 100644
index 000000000..48be83afe
--- /dev/null
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const Utils = require('../../utils/utils.jsx');
+const Client = require('../../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+
+const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
+const Constants = require('../../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+export default class ImportThemeModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateShow = this.updateShow.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+
+ this.state = {
+ inputError: '',
+ show: false
+ };
+ }
+ componentDidMount() {
+ UserStore.addImportModalListener(this.updateShow);
+ }
+ componentWillUnmount() {
+ UserStore.removeImportModalListener(this.updateShow);
+ }
+ updateShow(show) {
+ this.setState({show});
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const text = React.findDOMNode(this.refs.input).value;
+
+ if (!this.isInputValid(text)) {
+ this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
+ return;
+ }
+
+ const colors = text.split(',');
+ const theme = {type: 'custom'};
+
+ theme.sidebarBg = colors[0];
+ theme.sidebarText = colors[5];
+ theme.sidebarUnreadText = colors[5];
+ theme.sidebarTextHoverBg = colors[4];
+ theme.sidebarTextHoverColor = colors[5];
+ theme.sidebarTextActiveBg = colors[2];
+ theme.sidebarTextActiveColor = colors[3];
+ theme.sidebarHeaderBg = colors[1];
+ theme.sidebarHeaderTextColor = colors[5];
+ theme.onlineIndicator = colors[6];
+ theme.mentionBj = colors[7];
+ theme.mentionColor = '#ffffff';
+ theme.centerChannelBg = '#ffffff';
+ theme.centerChannelColor = '#333333';
+ theme.linkColor = '#2389d7';
+ theme.buttonBg = '#26a970';
+ theme.buttonColor = '#ffffff';
+
+ let user = UserStore.getCurrentUser();
+ user.theme_props = theme;
+
+ Client.updateUser(user,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ME,
+ me: data
+ });
+
+ this.setState({show: false});
+ Utils.applyTheme(theme);
+ $('#user_settings').modal('show');
+ },
+ (err) => {
+ var state = this.getStateFromStores();
+ state.serverError = err;
+ this.setState(state);
+ }
+ );
+ }
+ isInputValid(text) {
+ if (text.length === 0) {
+ return false;
+ }
+
+ if (text.indexOf(' ') !== -1) {
+ return false;
+ }
+
+ if (text.length > 0 && text.indexOf(',') === -1) {
+ return false;
+ }
+
+ if (text.length > 0) {
+ const colors = text.split(',');
+
+ if (colors.length !== 8) {
+ return false;
+ }
+
+ for (let i = 0; i < colors.length; i++) {
+ if (colors[i].length !== 7 && colors[i].length !== 4) {
+ return false;
+ }
+
+ if (colors[i].charAt(0) !== '#') {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ handleChange(e) {
+ if (this.isInputValid(e.target.value)) {
+ this.setState({inputError: null});
+ } else {
+ this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
+ }
+ }
+ render() {
+ return (
+ <span>
+ <Modal
+ show={this.state.show}
+ onHide={() => this.setState({show: false})}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Import Slack Theme'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <p>
+ {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
+ </p>
+ <div className='form-group less'>
+ <div className='col-sm-9'>
+ <input
+ ref='input'
+ type='text'
+ className='form-control'
+ onChange={this.handleChange}
+ />
+ {this.state.inputError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={() => this.setState({show: false})}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Submit'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
new file mode 100644
index 000000000..1bbfbd162
--- /dev/null
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -0,0 +1,174 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+var Constants = require('../../utils/constants.jsx');
+var ChannelStore = require('../../stores/channel_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+export default class ManageIncomingHooks extends React.Component {
+ constructor() {
+ super();
+
+ this.getHooks = this.getHooks.bind(this);
+ this.addNewHook = this.addNewHook.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+
+ this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false};
+ }
+ componentDidMount() {
+ this.getHooks();
+ }
+ addNewHook() {
+ let hook = {}; //eslint-disable-line prefer-const
+ hook.channel_id = this.state.channelId;
+
+ Client.addIncomingHook(
+ hook,
+ (data) => {
+ let hooks = this.state.hooks;
+ if (!hooks) {
+ hooks = [];
+ }
+ hooks.push(data);
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ removeHook(id) {
+ let data = {}; //eslint-disable-line prefer-const
+ data.id = id;
+
+ Client.deleteIncomingHook(
+ data,
+ () => {
+ let hooks = this.state.hooks; //eslint-disable-line prefer-const
+ let index = -1;
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ hooks.splice(index, 1);
+ }
+
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ getHooks() {
+ Client.listIncomingHooks(
+ (data) => {
+ let state = this.state; //eslint-disable-line prefer-const
+
+ if (data) {
+ state.hooks = data;
+ }
+
+ state.getHooksComplete = true;
+ this.setState(state);
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ updateChannelId(e) {
+ this.setState({channelId: e.target.value});
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const channels = ChannelStore.getAll();
+ let options = []; //eslint-disable-line prefer-const
+ channels.forEach((channel) => {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ });
+
+ let disableButton = '';
+ if (this.state.channelId === '') {
+ disableButton = ' disable';
+ }
+
+ let hooks = []; //eslint-disable-line prefer-const
+ this.state.hooks.forEach((hook) => {
+ const c = ChannelStore.get(hook.channel_id);
+ hooks.push(
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
+ </div>
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.name}
+ </div>
+ <div className='padding-top'>
+ <a
+ className={'text-danger'}
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
+ </div>
+ );
+ });
+
+ let displayHooks;
+ if (!this.state.getHooksComplete) {
+ displayHooks = <LoadingScreen/>;
+ } else if (hooks.length > 0) {
+ displayHooks = hooks;
+ } else {
+ displayHooks = <label>{': None'}</label>;
+ }
+
+ const existingHooks = (
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
+ {displayHooks}
+ </div>
+ );
+
+ return (
+ <div key='addIncomingHook'>
+ <label className='control-label'>{'Add a new incoming webhook'}</label>
+ <div className='padding-top'>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ {serverError}
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary' + disableButton}
+ href='#'
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingHooks}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
new file mode 100644
index 000000000..f8f916bd0
--- /dev/null
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -0,0 +1,58 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../../utils/utils.jsx');
+var Constants = require('../../utils/constants.jsx');
+
+export default class PremadeThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const premadeThemes = [];
+ for (const k in Constants.THEMES) {
+ if (Constants.THEMES.hasOwnProperty(k)) {
+ const premadeTheme = $.extend(true, {}, Constants.THEMES[k]);
+
+ let activeClass = '';
+ if (premadeTheme.type === theme.type) {
+ activeClass = 'active';
+ }
+
+ premadeThemes.push(
+ <div
+ className='col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
+ <div
+ className={activeClass}
+ onClick={() => this.props.updateTheme(premadeTheme)}
+ >
+ <label>
+ <img
+ className='img-responsive'
+ src={'/static/images/themes/' + premadeTheme.type.toLowerCase() + '.png'}
+ />
+ <div className='theme-label'>{Utils.toTitleCase(premadeTheme.type)}</div>
+ </label>
+ </div>
+ </div>
+ );
+ }
+ }
+
+ return (
+ <div className='row'>
+ {premadeThemes}
+ </div>
+ );
+ }
+}
+
+PremadeThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 282fb7681..0eab333c4 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -1,12 +1,14 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var utils = require('../../utils/utils.jsx');
var NotificationsTab = require('./user_settings_notifications.jsx');
var SecurityTab = require('./user_settings_security.jsx');
var GeneralTab = require('./user_settings_general.jsx');
var AppearanceTab = require('./user_settings_appearance.jsx');
+var DeveloperTab = require('./user_settings_developer.jsx');
+var IntegrationsTab = require('./user_settings_integrations.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -40,6 +42,7 @@ export default class UserSettings extends React.Component {
user={this.state.user}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
/>
</div>
);
@@ -75,6 +78,26 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'developer') {
+ return (
+ <div>
+ <DeveloperTab
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ />
+ </div>
+ );
+ } else if (this.props.activeTab === 'integrations') {
+ return (
+ <div>
+ <IntegrationsTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
@@ -86,4 +109,4 @@ UserSettings.propTypes = {
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func
-}; \ No newline at end of file
+};
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
new file mode 100644
index 000000000..4372069e7
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -0,0 +1,236 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var UserStore = require('../../stores/user_store.jsx');
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+const CustomThemeChooser = require('./custom_theme_chooser.jsx');
+const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
+const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
+const Constants = require('../../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+export default class UserSettingsAppearance extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.submitTheme = this.submitTheme.bind(this);
+ this.updateTheme = this.updateTheme.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleImportModal = this.handleImportModal.bind(this);
+
+ this.state = this.getStateFromStores();
+
+ this.originalTheme = this.state.theme;
+ }
+ componentDidMount() {
+ UserStore.addChangeListener(this.onChange);
+
+ if (this.props.activeSection === 'theme') {
+ $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ }
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+ componentDidUpdate() {
+ if (this.props.activeSection === 'theme') {
+ $('.color-btn').removeClass('active-border');
+ $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ }
+ }
+ componentWillUnmount() {
+ UserStore.removeChangeListener(this.onChange);
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+ getStateFromStores() {
+ const user = UserStore.getCurrentUser();
+ let theme = null;
+
+ if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
+ theme = user.theme_props;
+ } else {
+ theme = $.extend(true, {}, Constants.THEMES.default);
+ }
+
+ let type = 'premade';
+ if (theme.type === 'custom') {
+ type = 'custom';
+ }
+
+ return {theme, type};
+ }
+ onChange() {
+ const newState = this.getStateFromStores();
+
+ if (!Utils.areStatesEqual(this.state, newState)) {
+ this.setState(newState);
+ }
+ }
+ submitTheme(e) {
+ e.preventDefault();
+ var user = UserStore.getCurrentUser();
+ user.theme_props = this.state.theme;
+
+ Client.updateUser(user,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ME,
+ me: data
+ });
+
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ this.props.updateTab('general');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
+ $('#user_settings').modal('hide');
+ },
+ (err) => {
+ var state = this.getStateFromStores();
+ state.serverError = err;
+ this.setState(state);
+ }
+ );
+ }
+ updateTheme(theme) {
+ this.setState({theme});
+ Utils.applyTheme(theme);
+ }
+ updateType(type) {
+ this.setState({type});
+ }
+ handleClose() {
+ const state = this.getStateFromStores();
+ state.serverError = null;
+
+ Utils.applyTheme(state.theme);
+
+ this.setState(state);
+
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
+ $('#user_settings').modal('hide');
+ }
+ handleImportModal() {
+ $('#user_settings').modal('hide');
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
+ value: true
+ });
+ }
+ render() {
+ var serverError;
+ if (this.state.serverError) {
+ serverError = this.state.serverError;
+ }
+
+ const displayCustom = this.state.type === 'custom';
+
+ let custom;
+ let premade;
+ if (displayCustom) {
+ custom = (
+ <CustomThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateTheme}
+ />
+ );
+ } else {
+ premade = (
+ <PremadeThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateTheme}
+ />
+ );
+ }
+
+ const themeUI = (
+ <div className='section-max appearance-section'>
+ <div className='col-sm-12'>
+ <div className='radio'>
+ <label>
+ <input type='radio'
+ checked={!displayCustom}
+ onChange={this.updateType.bind(this, 'premade')}
+ >
+ {'Theme Colors'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ {premade}
+ <div className='radio'>
+ <label>
+ <input type='radio'
+ checked={displayCustom}
+ onChange={this.updateType.bind(this, 'custom')}
+ >
+ {'Custom Theme'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ {custom}
+ <hr />
+ {serverError}
+ <a
+ className='btn btn-sm btn-primary'
+ href='#'
+ onClick={this.submitTheme}
+ >
+ {'Submit'}
+ </a>
+ <a
+ className='btn btn-sm theme'
+ href='#'
+ onClick={this.handleClose}
+ >
+ {'Cancel'}
+ </a>
+ </div>
+ </div>
+ );
+
+ 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>{'Appearance Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Appearance Settings'}</h3>
+ <div className='divider-dark first'/>
+ {themeUI}
+ <div className='divider-dark'/>
+ </div>
+ <br/>
+ <a
+ className='theme'
+ onClick={this.handleImportModal}
+ >
+ {'Import from Slack'}
+ </a>
+ </div>
+ );
+ }
+}
+
+UserSettingsAppearance.defaultProps = {
+ activeSection: ''
+};
+UserSettingsAppearance.propTypes = {
+ activeSection: React.PropTypes.string,
+ updateTab: React.PropTypes.func
+};
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
new file mode 100644
index 000000000..d9fb43902
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -0,0 +1,93 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+
+export default class DeveloperTab extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+ register() {
+ $('#user_settings1').modal('hide');
+ $('#register_app').modal('show');
+ }
+ render() {
+ var appSection;
+ var self = this;
+ if (this.props.activeSection === 'app') {
+ var inputs = [];
+
+ inputs.push(
+ <div className='form-group'>
+ <div className='col-sm-7'>
+ <a
+ className='btn btn-sm btn-primary'
+ onClick={this.register}
+ >
+ {'Register New Application'}
+ </a>
+ </div>
+ </div>
+ );
+
+ appSection = (
+ <SettingItemMax
+ title='Applications (Preview)'
+ inputs={inputs}
+ updateSection={function updateSection(e) {
+ self.props.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ appSection = (
+ <SettingItemMin
+ title='Applications (Preview)'
+ describe='Open to register a new third-party application'
+ updateSection={function updateSection() {
+ self.props.updateSection('app');
+ }}
+ />
+ );
+ }
+
+ 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>{'Developer Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Developer Settings'}</h3>
+ <div className='divider-dark first'/>
+ {appSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+DeveloperTab.defaultProps = {
+ activeSection: ''
+};
+DeveloperTab.propTypes = {
+ activeSection: React.PropTypes.string,
+ updateSection: React.PropTypes.func
+};
diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index ead7ac1d5..c1d4c4ab5 100644
--- a/web/react/components/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -1,14 +1,13 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var ConfigStore = require('../stores/config_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var SettingPicture = require('./setting_picture.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var SettingPicture = require('../setting_picture.jsx');
+var client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var utils = require('../../utils/utils.jsx');
var assign = require('object-assign');
export default class UserSettingsGeneralTab extends React.Component {
@@ -40,7 +39,7 @@ export default class UserSettingsGeneralTab extends React.Component {
e.preventDefault();
var user = this.props.user;
- var username = this.state.username.trim();
+ var username = this.state.username.trim().toLowerCase();
var usernameError = utils.isValidUsername(username);
if (usernameError === 'Cannot use a reserved word as a username.') {
@@ -209,7 +208,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}
setupInitialState(props) {
var user = props.user;
- var emailEnabled = !ConfigStore.getSettingAsBoolean('ByPassEmail', false);
+ var emailEnabled = global.window.config.SendEmailNotifications === 'true';
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
}
@@ -238,7 +237,7 @@ export default class UserSettingsGeneralTab extends React.Component {
key='firstNameSetting'
className='form-group'
>
- <label className='col-sm-5 control-label'>First Name</label>
+ <label className='col-sm-5 control-label'>{'First Name'}</label>
<div className='col-sm-7'>
<input
className='form-control'
@@ -255,7 +254,7 @@ export default class UserSettingsGeneralTab extends React.Component {
key='lastNameSetting'
className='form-group'
>
- <label className='col-sm-5 control-label'>Last Name</label>
+ <label className='col-sm-5 control-label'>{'Last Name'}</label>
<div className='col-sm-7'>
<input
className='form-control'
@@ -267,6 +266,28 @@ export default class UserSettingsGeneralTab extends React.Component {
</div>
);
+ function notifClick(e) {
+ e.preventDefault();
+ this.updateSection('');
+ this.props.updateTab('notifications');
+ }
+
+ const notifLink = (
+ <a
+ href='#'
+ onClick={notifClick.bind(this)}
+ >
+ {'Notifications'}
+ </a>
+ );
+
+ const extraInfo = (
+ <span>
+ {'By default, you will receive mention notifications when someone types your first name. '}
+ {'Go to '} {notifLink} {'settings to change this default.'}
+ </span>
+ );
+
nameSection = (
<SettingItemMax
title='Full Name'
@@ -278,6 +299,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.updateSection('');
e.preventDefault();
}.bind(this)}
+ extraInfo={extraInfo}
/>
);
} else {
@@ -326,6 +348,13 @@ export default class UserSettingsGeneralTab extends React.Component {
</div>
);
+ const extraInfo = (
+ <span>
+ {'Use Nickname for a name you might be called that is different from your first name and user name.'}
+ {'This is most often used when two or more people have similar sounding names and usernames.'}
+ </span>
+ );
+
nicknameSection = (
<SettingItemMax
title='Nickname'
@@ -337,6 +366,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.updateSection('');
e.preventDefault();
}.bind(this)}
+ extraInfo={extraInfo}
/>
);
} else {
@@ -375,6 +405,8 @@ export default class UserSettingsGeneralTab extends React.Component {
</div>
);
+ const extraInfo = (<span>{'Pick something easy for teammates to recognize and recall.'}</span>);
+
usernameSection = (
<SettingItemMax
title='Username'
@@ -386,6 +418,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.updateSection('');
e.preventDefault();
}.bind(this)}
+ extraInfo={extraInfo}
/>
);
} else {
@@ -404,13 +437,13 @@ export default class UserSettingsGeneralTab extends React.Component {
let helpText = <div>Email is used for notifications, and requires verification if changed.</div>;
if (!this.state.emailEnabled) {
- helpText = <div className='text-danger'><br />Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.</div>;
+ helpText = <div className='setting-list__hint text-danger'>{'Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'}</div>;
}
inputs.push(
<div key='emailSetting'>
<div className='form-group'>
- <label className='col-sm-5 control-label'>Primary Email</label>
+ <label className='col-sm-5 control-label'>{'Primary Email'}</label>
<div className='col-sm-7'>
<input
className='form-control'
@@ -492,18 +525,18 @@ export default class UserSettingsGeneralTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>
- General Settings
+ {'General Settings'}
</h4>
</div>
<div 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'/>
@@ -524,5 +557,6 @@ export default class UserSettingsGeneralTab extends React.Component {
UserSettingsGeneralTab.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_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
new file mode 100644
index 000000000..cb45c5178
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -0,0 +1,95 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
+
+export default class UserSettingsIntegrationsTab extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+
+ this.state = {};
+ }
+ 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() {
+ let incomingHooksSection;
+ var inputs = [];
+
+ if (this.props.activeSection === 'incoming-hooks') {
+ inputs.push(
+ <ManageIncomingHooks />
+ );
+
+ incomingHooksSection = (
+ <SettingItemMax
+ title='Incoming Webhooks'
+ inputs={inputs}
+ updateSection={function clearSection(e) {
+ this.updateSection('');
+ e.preventDefault();
+ }.bind(this)}
+ />
+ );
+ } else {
+ incomingHooksSection = (
+ <SettingItemMin
+ title='Incoming Webhooks'
+ describe='Manage your incoming webhooks'
+ updateSection={function updateNameSection() {
+ this.updateSection('incoming-hooks');
+ }.bind(this)}
+ />
+ );
+ }
+
+ 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>
+ {'Integration Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Integration Settings'}</h3>
+ <div className='divider-dark first'/>
+ {incomingHooksSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+UserSettingsIntegrationsTab.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_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 7ec75e000..5113d2429 100644
--- a/web/react/components/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingsSidebar = require('./settings_sidebar.jsx');
+var SettingsSidebar = require('../settings_sidebar.jsx');
var UserSettings = require('./user_settings.jsx');
export default class UserSettingsModal extends React.Component {
@@ -17,8 +17,8 @@ export default class UserSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function changeDisplay() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
- $('body').on('click', '.modal-header .close', function closeModal() {
- setTimeout(function finishClose() {
+ $('body').on('click', '.modal-header .close', () => {
+ setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -35,6 +35,12 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'});
tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'});
tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'});
+ if (global.window.config.EnableOAuthServiceProvider === 'true') {
+ tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
+ }
+ if (global.window.config.EnableIncomingWebhooks === 'true') {
+ tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
+ }
return (
<div
@@ -54,13 +60,13 @@ export default class UserSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
- Account Settings
+ {'Account Settings'}
</h4>
</div>
<div className='modal-body'>
diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 33db1a332..ba14f019f 100644
--- a/web/react/components/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -1,14 +1,13 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var utils = require('../../utils/utils.jsx');
var assign = require('object-assign');
-import {config} from '../utils/config.js';
function getNotificationsStateFromStores() {
var user = UserStore.getCurrentUser();
@@ -242,7 +241,7 @@ export default class NotificationsTab extends React.Component {
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
>
- Only for mentions and private messages
+ Only for mentions and direct messages
</input>
</label>
<br/>
@@ -266,9 +265,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -278,7 +280,7 @@ export default class NotificationsTab extends React.Component {
} else {
let describe = '';
if (this.state.notifyLevel === 'mention') {
- describe = 'Only for mentions and private messages';
+ describe = 'Only for mentions and direct messages';
} else if (this.state.notifyLevel === 'none') {
describe = 'Never';
} else {
@@ -344,9 +346,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
+
soundSection = (
<SettingItemMax
title='Desktop notification sounds'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -415,7 +420,7 @@ export default class NotificationsTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + config.SiteName + ' for 5 minutes.'}</div>
+ <div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
</div>
);
diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index 6ccd09cb1..b59c08af0 100644
--- a/web/react/components/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var Constants = require('../utils/constants.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var Constants = require('../../utils/constants.jsx');
export default class SecurityTab extends React.Component {
constructor(props) {
@@ -251,6 +251,17 @@ export default class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-dark'/>
+ <ul
+ className='section-min'
+ >
+ <li className='col-sm-10 section-title'>{'Version ' + global.window.config.Version}</li>
+ <li className='col-sm-7 section-describe'>
+ <div className='text-nowrap'>{'Build Number: ' + global.window.config.BuildNumber}</div>
+ <div className='text-nowrap'>{'Build Date: ' + global.window.config.BuildDate}</div>
+ <div className='text-nowrap'>{'Build Hash: ' + global.window.config.BuildHash}</div>
+ </li>
+ </ul>
+ <div className='divider-dark'/>
<br></br>
<a
data-toggle='modal'
diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx
deleted file mode 100644
index 3afdd7349..000000000
--- a/web/react/components/user_settings_appearance.jsx
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var UserStore = require('../stores/user_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var Client = require('../utils/client.jsx');
-var Utils = require('../utils/utils.jsx');
-import {config} from '../utils/config.js';
-
-export default class UserSettingsAppearance extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitTheme = this.submitTheme.bind(this);
- this.updateTheme = this.updateTheme.bind(this);
- this.handleClose = this.handleClose.bind(this);
-
- this.state = this.getStateFromStores();
- }
- getStateFromStores() {
- var user = UserStore.getCurrentUser();
- var theme = '#2389d7';
- if (config.ThemeColors != null) {
- theme = config.ThemeColors[0];
- }
- if (user.props && user.props.theme) {
- theme = user.props.theme;
- }
-
- return {theme: theme.toLowerCase()};
- }
- submitTheme(e) {
- e.preventDefault();
- var user = UserStore.getCurrentUser();
- if (!user.props) {
- user.props = {};
- }
- user.props.theme = this.state.theme;
-
- Client.updateUser(user,
- function success() {
- this.props.updateSection('');
- window.location.reload();
- }.bind(this),
- function fail(err) {
- var state = this.getStateFromStores();
- state.serverError = err;
- this.setState(state);
- }.bind(this)
- );
- }
- updateTheme(e) {
- var hex = Utils.rgb2hex(e.target.style.backgroundColor);
- this.setState({theme: hex.toLowerCase()});
- }
- handleClose() {
- this.setState({serverError: null});
- this.props.updateTab('general');
- }
- componentDidMount() {
- if (this.props.activeSection === 'theme') {
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
- }
- $('#user_settings').on('hidden.bs.modal', this.handleClose);
- }
- componentDidUpdate() {
- if (this.props.activeSection === 'theme') {
- $('.color-btn').removeClass('active-border');
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
- }
- }
- componentWillUnmount() {
- $('#user_settings').off('hidden.bs.modal', this.handleClose);
- this.props.updateSection('');
- }
- render() {
- var serverError;
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
-
- var themeSection;
- var self = this;
-
- if (config.ThemeColors != null) {
- if (this.props.activeSection === 'theme') {
- var themeButtons = [];
-
- for (var i = 0; i < config.ThemeColors.length; i++) {
- themeButtons.push(
- <button
- key={config.ThemeColors[i] + 'key' + i}
- ref={config.ThemeColors[i]}
- type='button'
- className='btn btn-lg color-btn'
- style={{backgroundColor: config.ThemeColors[i]}}
- onClick={this.updateTheme}
- />
- );
- }
-
- var inputs = [];
-
- inputs.push(
- <li
- key='themeColorSetting'
- className='setting-list-item'
- >
- <div
- className='btn-group'
- data-toggle='buttons-radio'
- >
- {themeButtons}
- </div>
- </li>
- );
-
- themeSection = (
- <SettingItemMax
- title='Theme Color'
- inputs={inputs}
- submit={this.submitTheme}
- serverError={serverError}
- updateSection={function updateSection(e) {
- self.props.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- themeSection = (
- <SettingItemMin
- title='Theme Color'
- describe={this.state.theme}
- updateSection={function updateSection() {
- self.props.updateSection('theme');
- }}
- />
- );
- }
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <i className='modal-back'></i>Appearance Settings
- </h4>
- </div>
- <div className='user-settings'>
- <h3 className='tab-header'>Appearance Settings</h3>
- <div className='divider-dark first'/>
- {themeSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-UserSettingsAppearance.defaultProps = {
- activeSection: ''
-};
-UserSettingsAppearance.propTypes = {
- activeSection: React.PropTypes.string,
- updateSection: React.PropTypes.func,
- updateTab: React.PropTypes.func
-};
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index b0eaba5d6..a7fecb689 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -1,9 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var Utils = require('../utils/utils.jsx');
-import {config} from '../utils/config.js';
+const Client = require('../utils/client.jsx');
+const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
+const Modal = ReactBootstrap.Modal;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -17,6 +19,10 @@ export default class ViewImageModal extends React.Component {
this.handleKeyPress = this.handleKeyPress.bind(this);
this.getPublicLink = this.getPublicLink.bind(this);
this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
+ this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
var loaded = [];
var progress = [];
@@ -24,9 +30,20 @@ export default class ViewImageModal extends React.Component {
loaded.push(false);
progress.push(0);
}
- this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
+ this.state = {
+ imgId: this.props.startId,
+ imgHeight: '100%',
+ loaded: loaded,
+ progress: progress,
+ images: {},
+ fileSizes: {},
+ showFooter: false
+ };
}
- handleNext() {
+ handleNext(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) {
id = 0;
@@ -34,7 +51,10 @@ export default class ViewImageModal extends React.Component {
this.setState({imgId: id});
this.loadImage(id);
}
- handlePrev() {
+ handlePrev(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length - 1;
@@ -51,15 +71,27 @@ export default class ViewImageModal extends React.Component {
this.handlePrev();
}
}
- componentWillReceiveProps(nextProps) {
+ onModalShown(nextProps) {
this.setState({imgId: nextProps.startId});
+ this.loadImage(nextProps.startId);
+ }
+ onModalHidden() {
+ if (this.refs.video) {
+ var video = React.findDOMNode(this.refs.video);
+ video.pause();
+ video.currentTime = 0;
+ }
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.show === true && this.props.show === false) {
+ this.onModalShown(nextProps);
+ } else if (nextProps.show === false && this.props.show === true) {
+ this.onModalHidden();
+ }
}
loadImage(id) {
var imgHeight = $(window).height() - 100;
- if (this.state.loaded[id] || this.state.images[id]) {
- $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight);
- return;
- }
+ this.setState({imgHeight});
var filename = this.props.filenames[id];
@@ -69,76 +101,27 @@ export default class ViewImageModal extends React.Component {
if (fileType === 'image') {
var img = new Image();
img.load(this.getPreviewImagePath(filename),
- function load() {
- var progress = this.state.progress;
- progress[id] = img.completedPercentage;
- this.setState({progress: progress});
- }.bind(this));
- img.onload = (function onload(imgid) {
- return function onloadReturn() {
- var loaded = this.state.loaded;
- loaded[imgid] = true;
- this.setState({loaded: loaded});
- $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight);
- }.bind(this);
- }.bind(this)(id));
+ () => {
+ const progress = this.state.progress;
+ progress[id] = img.completedPercentage;
+ this.setState({progress});
+ });
+ img.onload = () => {
+ const loaded = this.state.loaded;
+ loaded[id] = true;
+ this.setState({loaded});
+ };
var images = this.state.images;
images[id] = img;
- this.setState({images: images});
+ this.setState({images});
} else {
// there's nothing to load for non-image files
var loaded = this.state.loaded;
loaded[id] = true;
- this.setState({loaded: loaded});
- }
- }
- componentDidUpdate() {
- if (this.state.loaded[this.state.imgId]) {
- if (this.refs.imageWrap) {
- $(React.findDOMNode(this.refs.imageWrap)).removeClass('default');
- }
+ this.setState({loaded});
}
}
componentDidMount() {
- $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() {
- this.setState({viewed: true});
- this.loadImage(this.state.imgId);
- }.bind(this));
-
- $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
- if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
- $('.image_modal').modal('hide');
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.imageWrap)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
-
- if (this.refs.previewArrowLeft) {
- $(React.findDOMNode(this.refs.previewArrowLeft)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
- if (this.refs.previewArrowRight) {
- $(React.findDOMNode(this.refs.previewArrowRight)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
$(window).on('keyup', this.handleKeyPress);
// keep track of whether or not this component is mounted so we can safely set the state asynchronously
@@ -182,6 +165,12 @@ export default class ViewImageModal extends React.Component {
// only images have proper previews, so just use a placeholder icon for non-images
return Utils.getPreviewImagePathForFileType(fileType);
}
+ onMouseEnterImage() {
+ this.setState({showFooter: true});
+ }
+ onMouseLeaveImage() {
+ this.setState({showFooter: false});
+ }
render() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>;
@@ -211,6 +200,25 @@ export default class ViewImageModal extends React.Component {
/>
</a>
);
+ } else if (fileType === 'video' || fileType === 'audio') {
+ let width = Constants.WEB_VIDEO_WIDTH;
+ let height = Constants.WEB_VIDEO_HEIGHT;
+ if (Utils.isMobile()) {
+ width = Constants.MOBILE_VIDEO_WIDTH;
+ height = Constants.MOBILE_VIDEO_HEIGHT;
+ }
+
+ content = (
+ <video
+ ref='video'
+ data-setup='{}'
+ controls='controls'
+ width={width}
+ height={height}
+ >
+ <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
+ </video>
+ );
} else {
// non-image files include a section providing details about the file
var infoString = 'File type ' + fileInfo.ext.toUpperCase();
@@ -282,23 +290,6 @@ export default class ViewImageModal extends React.Component {
bgClass = 'black-bg';
}
- var publicLink = '';
- if (config.AllowPublicLink) {
- publicLink = (
- <div>
- <a
- href='#'
- className='public-link text'
- data-title='Public Image'
- onClick={this.getPublicLink}
- >
- Get Public Link
- </a>
- <span className='text'> | </span>
- </div>
- );
- }
-
var leftArrow = '';
var rightArrow = '';
if (this.props.filenames.length > 1) {
@@ -325,65 +316,61 @@ export default class ViewImageModal extends React.Component {
);
}
+ let closeButtonClass = 'modal-close';
+ if (this.state.showFooter) {
+ closeButtonClass += ' modal-close--show';
+ }
+
return (
- <div
- className='modal fade image_modal'
- ref='modal'
- id={this.props.modalId}
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ className='image_modal'
+ dialogClassName='modal-image'
>
- <div className='modal-dialog modal-image'>
- <div className='modal-content image-content'>
+ <Modal.Body
+ modalClassName='image-body'
+ onClick={this.props.onModalDismissed}
+ >
+ <div
+ className={'image-wrapper ' + bgClass}
+ style={{maxHeight: this.state.imgHeight}}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
<div
- ref='imageBody'
- className='modal-body image-body'
- >
- <div
- ref='imageWrap'
- className={'image-wrapper default ' + bgClass}
- >
- <div
- className='modal-close'
- data-dismiss='modal'
- />
- {content}
- <div
- ref='imageFooter'
- className='modal-button-bar'
- >
- <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span>
- <div className='image-links'>
- {publicLink}
- <a
- href={fileUrl}
- download={name}
- className='text'
- >
- Download
- </a>
- </div>
- </div>
- </div>
- {leftArrow}
- {rightArrow}
- </div>
+ className={closeButtonClass}
+ onClick={this.props.onModalDismissed}
+ />
+ {content}
+ <ViewImagePopoverBar
+ show={this.state.showFooter}
+ fileId={this.state.imgId}
+ totalFiles={this.props.filenames.length}
+ filename={name}
+ fileURL={fileUrl}
+ onGetPublicLinkPressed={this.getPublicLink}
+ />
</div>
- </div>
- </div>
+ {leftArrow}
+ {rightArrow}
+ </Modal.Body>
+ </Modal>
);
}
}
ViewImageModal.defaultProps = {
+ show: false,
filenames: [],
- modalId: '',
channelId: '',
userId: '',
startId: 0
};
ViewImageModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired,
filenames: React.PropTypes.array,
modalId: React.PropTypes.string,
channelId: React.PropTypes.string,
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
new file mode 100644
index 000000000..68817d751
--- /dev/null
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ViewImagePopoverBar extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ var publicLink = '';
+ if (global.window.config.EnablePublicLink === 'true') {
+ publicLink = (
+ <div>
+ <a
+ href='#'
+ className='public-link text'
+ data-title='Public Image'
+ onClick={this.getPublicLink}
+ >
+ {'Get Public Link'}
+ </a>
+ <span className='text'>{' | '}</span>
+ </div>
+ );
+ }
+
+ var footerClass = 'modal-button-bar';
+ if (this.props.show) {
+ footerClass += ' footer--show';
+ }
+
+ return (
+ <div
+ ref='imageFooter'
+ className={footerClass}
+ >
+ <span className='pull-left text'>{'File ' + (this.props.fileId + 1) + ' of ' + this.props.totalFiles}</span>
+ <div className='image-links'>
+ {publicLink}
+ <a
+ href={this.props.fileURL}
+ download={this.props.filename}
+ className='text'
+ >
+ {'Download'}
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
+ViewImagePopoverBar.defaultProps = {
+ show: false,
+ imgId: 0,
+ totalFiles: 0,
+ filename: '',
+ fileURL: ''
+};
+
+ViewImagePopoverBar.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ fileId: React.PropTypes.number.isRequired,
+ totalFiles: React.PropTypes.number.isRequired,
+ filename: React.PropTypes.string.isRequired,
+ fileURL: React.PropTypes.string.isRequired,
+ onGetPublicLinkPressed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/package.json b/web/react/package.json
index da55dc2b8..a9eba6c6c 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -3,22 +3,24 @@
"version": "0.0.1",
"private": true,
"dependencies": {
- "autolinker": "^0.18.1",
- "flux": "^2.1.1",
- "keymirror": "^0.1.1",
- "object-assign": "^3.0.0",
- "react": "^0.13.3",
- "react-zeroclipboard-mixin": "^0.1.0",
- "twemoji": "^1.4.1"
+ "autolinker": "0.18.1",
+ "babel-runtime": "5.8.24",
+ "bootstrap-colorpicker": "2.2.0",
+ "flux": "2.1.1",
+ "keymirror": "0.1.1",
+ "marked": "0.3.5",
+ "object-assign": "3.0.0",
+ "react-zeroclipboard-mixin": "0.1.0",
+ "twemoji": "1.4.1"
},
"devDependencies": {
- "browserify": "^11.0.1",
- "envify": "^3.4.0",
- "babelify": "^6.1.3",
- "uglify-js": "^2.4.24",
- "watchify": "^3.3.1",
- "eslint": "^1.3.1",
- "eslint-plugin-react": "^3.3.1"
+ "browserify": "11.0.1",
+ "envify": "3.4.0",
+ "babelify": "6.1.3",
+ "uglify-js": "2.4.24",
+ "watchify": "3.3.1",
+ "eslint": "1.3.1",
+ "eslint-plugin-react": "3.3.1"
},
"scripts": {
"start": "watchify --extension=jsx -o ../static/js/bundle.js -v -d ./**/*.jsx",
@@ -27,7 +29,14 @@
},
"browserify": {
"transform": [
- "babelify",
+ [
+ "babelify",
+ {
+ "optional": [
+ "runtime"
+ ]
+ }
+ ],
"envify"
]
},
diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
new file mode 100644
index 000000000..689a6b3a2
--- /dev/null
+++ b/web/react/pages/admin_console.jsx
@@ -0,0 +1,25 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var ErrorBar = require('../components/error_bar.jsx');
+var SelectTeamModal = require('../components/admin_console/select_team_modal.jsx');
+var AdminController = require('../components/admin_console/admin_controller.jsx');
+
+export function setupAdminConsolePage() {
+ React.render(
+ <AdminController />,
+ document.getElementById('admin_controller')
+ );
+
+ React.render(
+ <SelectTeamModal />,
+ document.getElementById('select_team_modal')
+ );
+
+ React.render(
+ <ErrorBar/>,
+ document.getElementById('error_bar')
+ );
+}
+
+global.window.setup_admin_console_page = setupAdminConsolePage;
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
new file mode 100644
index 000000000..db42c8266
--- /dev/null
+++ b/web/react/pages/authorize.jsx
@@ -0,0 +1,21 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Authorize = require('../components/authorize.jsx');
+
+function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
+ React.render(
+ <Authorize
+ teamName={teamName}
+ appName={appName}
+ responseType={responseType}
+ clientId={clientId}
+ redirectUri={redirectUri}
+ scope={scope}
+ state={state}
+ />,
+ document.getElementById('authorize')
+ );
+}
+
+global.window.setup_authorize_page = setupAuthorizePage;
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 71a03cde0..74259194a 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -5,7 +5,7 @@ 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 PostList = require('../components/post_list.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');
@@ -17,16 +17,16 @@ var RenameChannelModal = require('../components/rename_channel_modal.jsx');
var EditPostModal = require('../components/edit_post_modal.jsx');
var DeletePostModal = require('../components/delete_post_modal.jsx');
var MoreChannelsModal = require('../components/more_channels.jsx');
-var NewChannelModal = require('../components/new_channel.jsx');
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
-var UserSettingsModal = require('../components/user_settings_modal.jsx');
+var UserSettingsModal = require('../components/user_settings/user_settings_modal.jsx');
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 DirectChannelModal = require('../components/more_direct_channels.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');
@@ -34,24 +34,22 @@ 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 AsyncClient = require('../utils/async_client.jsx');
+var RegisterAppModal = require('../components/register_app_modal.jsx');
+var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
-function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
- AsyncClient.getConfig();
-
+function setupChannelPage(props) {
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
- name: channelName,
- id: channelId
+ name: props.ChannelName,
+ id: props.ChannelId
});
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_TEAM,
- id: teamId
+ id: props.TeamId
});
// ChannelLoader must be rendered first
@@ -66,14 +64,14 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
);
React.render(
- <Navbar teamDisplayName={teamName} />,
+ <Navbar teamDisplayName={props.TeamDisplayName} />,
document.getElementById('navbar')
);
React.render(
<Sidebar
- teamDisplayName={teamName}
- teamType={teamType}
+ teamDisplayName={props.TeamDisplayName}
+ teamType={props.TeamType}
/>,
document.getElementById('sidebar-left')
);
@@ -89,17 +87,22 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
);
React.render(
- <TeamSettingsModal teamDisplayName={teamName} />,
+ <ImportThemeModal />,
+ document.getElementById('import_theme_modal')
+ );
+
+ React.render(
+ <TeamSettingsModal teamDisplayName={props.TeamDisplayName} />,
document.getElementById('team_settings_modal')
);
React.render(
- <TeamMembersModal teamDisplayName={teamName} />,
+ <TeamMembersModal teamDisplayName={props.TeamDisplayName} />,
document.getElementById('team_members_modal')
);
React.render(
- <MemberInviteModal teamType={teamType} />,
+ <MemberInviteModal teamType={props.TeamType} />,
document.getElementById('invite_member_modal')
);
@@ -154,12 +157,7 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
);
React.render(
- <NewChannelModal />,
- document.getElementById('new_channel_modal')
- );
-
- React.render(
- <PostList />,
+ <PostListContainer />,
document.getElementById('post-list')
);
@@ -190,8 +188,8 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
React.render(
<SidebarRightMenu
- teamDisplayName={teamName}
- teamType={teamType}
+ teamDisplayName={props.TeamDisplayName}
+ teamType={props.TeamType}
/>,
document.getElementById('sidebar-menu')
);
@@ -232,6 +230,16 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
/>,
document.getElementById('file_upload_overlay')
);
+
+ React.render(
+ <RegisterAppModal />,
+ document.getElementById('register_app_modal')
+ );
+
+ if (global.window.config.SendEmailNotifications === 'false') {
+ ErrorStore.storeLastError({message: 'Preview Mode: Email notifications have not been configured'});
+ ErrorStore.emitChange();
+ }
}
global.window.setup_channel_page = setupChannelPage;
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
index 18553542c..2299c306e 100644
--- a/web/react/pages/home.jsx
+++ b/web/react/pages/home.jsx
@@ -4,12 +4,12 @@
var ChannelStore = require('../stores/channel_store.jsx');
var Constants = require('../utils/constants.jsx');
-function setupHomePage(teamURL) {
+function setupHomePage(props) {
var last = ChannelStore.getLastVisitedName();
if (last == null || last.length === 0) {
- window.location = teamURL + '/channels/' + Constants.DEFAULT_CHANNEL;
+ window.location = props.TeamURL + '/channels/' + Constants.DEFAULT_CHANNEL;
} else {
- window.location = teamURL + '/channels/' + last;
+ window.location = props.TeamURL + '/channels/' + last;
}
}
diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx
index 424ae0e84..f78e0f37a 100644
--- a/web/react/pages/login.jsx
+++ b/web/react/pages/login.jsx
@@ -3,12 +3,11 @@
var Login = require('../components/login.jsx');
-function setupLoginPage(teamDisplayName, teamName, authServices) {
+function setupLoginPage(props) {
React.render(
<Login
- teamDisplayName={teamDisplayName}
- teamName={teamName}
- authServices={authServices}
+ teamDisplayName={props.TeamDisplayName}
+ teamName={props.TeamName}
/>,
document.getElementById('login')
);
diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx
index 2ca468bea..b7bfdcd5e 100644
--- a/web/react/pages/password_reset.jsx
+++ b/web/react/pages/password_reset.jsx
@@ -3,14 +3,14 @@
var PasswordReset = require('../components/password_reset.jsx');
-function setupPasswordResetPage(isReset, teamDisplayName, teamName, hash, data) {
+function setupPasswordResetPage(props) {
React.render(
<PasswordReset
- isReset={isReset}
- teamDisplayName={teamDisplayName}
- teamName={teamName}
- hash={hash}
- data={data}
+ isReset={props.IsReset}
+ teamDisplayName={props.TeamDisplayName}
+ teamName={props.TeamName}
+ hash={props.Hash}
+ data={props.Data}
/>,
document.getElementById('reset')
);
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
index e9e803aa4..d0e08f446 100644
--- a/web/react/pages/signup_team.jsx
+++ b/web/react/pages/signup_team.jsx
@@ -3,15 +3,9 @@
var SignupTeam = require('../components/signup_team.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-
-function setupSignupTeamPage(authServices) {
- AsyncClient.getConfig();
-
- var services = JSON.parse(authServices);
-
+function setupSignupTeamPage() {
React.render(
- <SignupTeam services={services} />,
+ <SignupTeam />,
document.getElementById('signup-team')
);
}
diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx
index 72f9992a8..ec77e6602 100644
--- a/web/react/pages/signup_team_complete.jsx
+++ b/web/react/pages/signup_team_complete.jsx
@@ -3,12 +3,12 @@
var SignupTeamComplete = require('../components/signup_team_complete.jsx');
-function setupSignupTeamCompletePage(email, data, hash) {
+function setupSignupTeamCompletePage(props) {
React.render(
<SignupTeamComplete
- email={email}
- hash={hash}
- data={data}
+ email={props.Email}
+ hash={props.Hash}
+ data={props.Data}
/>,
document.getElementById('signup-team-complete')
);
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
index eaf93a61c..cc7607187 100644
--- a/web/react/pages/signup_user_complete.jsx
+++ b/web/react/pages/signup_user_complete.jsx
@@ -3,16 +3,15 @@
var SignupUserComplete = require('../components/signup_user_complete.jsx');
-function setupSignupUserCompletePage(email, name, uiName, id, data, hash, authServices) {
+function setupSignupUserCompletePage(props) {
React.render(
<SignupUserComplete
- teamId={id}
- teamName={name}
- teamDisplayName={uiName}
- email={email}
- hash={hash}
- data={data}
- authServices={authServices}
+ teamId={props.TeamId}
+ teamName={props.TeamName}
+ teamDisplayName={props.TeamDisplayName}
+ email={props.Email}
+ hash={props.Hash}
+ data={props.Data}
/>,
document.getElementById('signup-user-complete')
);
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
index 7077b40b8..16a9846e5 100644
--- a/web/react/pages/verify.jsx
+++ b/web/react/pages/verify.jsx
@@ -3,12 +3,13 @@
var EmailVerify = require('../components/email_verify.jsx');
-global.window.setupVerifyPage = function setupVerifyPage(isVerified, teamURL, userEmail) {
+global.window.setupVerifyPage = function setupVerifyPage(props) {
React.render(
<EmailVerify
- isVerified={isVerified}
- teamURL={teamURL}
- userEmail={userEmail}
+ isVerified={props.IsVerified}
+ teamURL={props.TeamURL}
+ userEmail={props.UserEmail}
+ resendSuccess={props.ResendSuccess}
/>,
document.getElementById('verify')
);
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
new file mode 100644
index 000000000..7b2aeb631
--- /dev/null
+++ b/web/react/stores/admin_store.jsx
@@ -0,0 +1,128 @@
+// Copyright (c) 2015 Spinpunch, 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 LOG_CHANGE_EVENT = 'log_change';
+var CONFIG_CHANGE_EVENT = 'config_change';
+var ALL_TEAMS_EVENT = 'all_team_change';
+
+class AdminStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ this.logs = null;
+ this.config = null;
+ this.teams = null;
+
+ this.emitLogChange = this.emitLogChange.bind(this);
+ this.addLogChangeListener = this.addLogChangeListener.bind(this);
+ this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
+
+ this.emitConfigChange = this.emitConfigChange.bind(this);
+ this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
+ this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
+
+ this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
+ this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
+ this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
+ }
+
+ emitLogChange() {
+ this.emit(LOG_CHANGE_EVENT);
+ }
+
+ addLogChangeListener(callback) {
+ this.on(LOG_CHANGE_EVENT, callback);
+ }
+
+ removeLogChangeListener(callback) {
+ this.removeListener(LOG_CHANGE_EVENT, callback);
+ }
+
+ emitConfigChange() {
+ this.emit(CONFIG_CHANGE_EVENT);
+ }
+
+ addConfigChangeListener(callback) {
+ this.on(CONFIG_CHANGE_EVENT, callback);
+ }
+
+ removeConfigChangeListener(callback) {
+ this.removeListener(CONFIG_CHANGE_EVENT, callback);
+ }
+
+ emitAllTeamsChange() {
+ this.emit(ALL_TEAMS_EVENT);
+ }
+
+ addAllTeamsChangeListener(callback) {
+ this.on(ALL_TEAMS_EVENT, callback);
+ }
+
+ removeAllTeamsChangeListener(callback) {
+ this.removeListener(ALL_TEAMS_EVENT, callback);
+ }
+
+ getLogs() {
+ return this.logs;
+ }
+
+ saveLogs(logs) {
+ this.logs = logs;
+ }
+
+ getConfig() {
+ return this.config;
+ }
+
+ saveConfig(config) {
+ this.config = config;
+ }
+
+ getAllTeams() {
+ return this.teams;
+ }
+
+ saveAllTeams(teams) {
+ this.teams = teams;
+ }
+
+ getSelectedTeams() {
+ return BrowserStore.getItem('seleted_teams');
+ }
+
+ saveSelectedTeams(teams) {
+ BrowserStore.setItem('seleted_teams', teams);
+ }
+}
+
+var AdminStore = new AdminStoreClass();
+
+AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIEVED_LOGS:
+ AdminStore.saveLogs(action.logs);
+ AdminStore.emitLogChange();
+ break;
+ case ActionTypes.RECIEVED_CONFIG:
+ AdminStore.saveConfig(action.config);
+ AdminStore.emitConfigChange();
+ break;
+ case ActionTypes.RECIEVED_ALL_TEAMS:
+ AdminStore.saveAllTeams(action.teams);
+ AdminStore.emitAllTeamsChange();
+ break;
+ default:
+ }
+});
+
+export default AdminStore;
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index e1ca52746..d2dedb271 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -9,9 +9,6 @@ function getPrefix() {
return UserStore.getCurrentId() + '_';
}
-// Also change model/utils.go ETAG_ROOT_VERSION
-var BROWSER_STORE_VERSION = '.5';
-
class BrowserStoreClass {
constructor() {
this.getItem = this.getItem.bind(this);
@@ -25,9 +22,9 @@ class BrowserStoreClass {
this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this);
var currentVersion = localStorage.getItem('local_storage_version');
- if (currentVersion !== BROWSER_STORE_VERSION) {
+ if (currentVersion !== global.window.config.Version) {
this.clear();
- localStorage.setItem('local_storage_version', BROWSER_STORE_VERSION);
+ localStorage.setItem('local_storage_version', global.window.config.Version);
}
}
diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx
index bd655b767..b9ba37c27 100644
--- a/web/react/stores/channel_store.jsx
+++ b/web/react/stores/channel_store.jsx
@@ -10,6 +10,7 @@ var ActionTypes = Constants.ActionTypes;
var BrowserStore = require('../stores/browser_store.jsx');
var CHANGE_EVENT = 'change';
+var LEAVE_EVENT = 'leave';
var MORE_CHANGE_EVENT = 'change';
var EXTRA_INFO_EVENT = 'extra_info';
@@ -48,6 +49,15 @@ class ChannelStoreClass extends EventEmitter {
removeExtraInfoChangeListener(callback) {
this.removeListener(EXTRA_INFO_EVENT, callback);
}
+ emitLeave(id) {
+ this.emit(LEAVE_EVENT, id);
+ }
+ addLeaveListener(callback) {
+ this.on(LEAVE_EVENT, callback);
+ }
+ removeLeaveListener(callback) {
+ this.removeListener(LEAVE_EVENT, callback);
+ }
findFirstBy(field, value) {
var channels = this.pGetChannels();
for (var i = 0; i < channels.length; i++) {
@@ -272,6 +282,10 @@ ChannelStore.dispatchToken = AppDispatcher.register(function handleAction(payloa
ChannelStore.emitExtraInfoChange();
break;
+ case ActionTypes.LEAVE_CHANNEL:
+ ChannelStore.emitLeave(action.id);
+ break;
+
default:
break;
}
diff --git a/web/react/stores/config_store.jsx b/web/react/stores/config_store.jsx
deleted file mode 100644
index b397937be..000000000
--- a/web/react/stores/config_store.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (c) 2015 Spinpunch, 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';
-
-class ConfigStoreClass extends EventEmitter {
- constructor() {
- super();
-
- this.emitChange = this.emitChange.bind(this);
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
- this.getSetting = this.getSetting.bind(this);
- this.getSettingAsBoolean = this.getSettingAsBoolean.bind(this);
- this.updateStoredSettings = this.updateStoredSettings.bind(this);
- }
- emitChange() {
- this.emit(CHANGE_EVENT);
- }
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback);
- }
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
- getSetting(key, defaultValue) {
- return BrowserStore.getItem('config_' + key, defaultValue);
- }
- getSettingAsBoolean(key, defaultValue) {
- var value = this.getSetting(key, defaultValue);
-
- if (typeof value !== 'string') {
- return Boolean(value);
- }
-
- return value === 'true';
- }
- updateStoredSettings(settings) {
- for (let key in settings) {
- if (settings.hasOwnProperty(key)) {
- BrowserStore.setItem('config_' + key, settings[key]);
- }
- }
- }
-}
-
-var ConfigStore = new ConfigStoreClass();
-
-ConfigStore.dispatchToken = AppDispatcher.register(function registry(payload) {
- var action = payload.action;
-
- switch (action.type) {
- case ActionTypes.RECIEVED_CONFIG:
- ConfigStore.updateStoredSettings(action.settings);
- ConfigStore.emitChange();
- break;
- default:
- }
-});
-
-export default ConfigStore;
diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx
index 597c88cff..ece7d8522 100644
--- a/web/react/stores/error_store.jsx
+++ b/web/react/stores/error_store.jsx
@@ -48,7 +48,7 @@ class ErrorStoreClass extends EventEmitter {
var ErrorStore = new ErrorStoreClass();
-ErrorStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+ErrorStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
case ActionTypes.RECIEVED_ERROR:
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 5ffe65021..29ce47300 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -297,6 +297,7 @@ class PostStoreClass extends EventEmitter {
post.message = '(message deleted)';
post.state = Constants.POST_DELETED;
+ post.filenames = [];
posts[post.id] = post;
this.storeUnseenDeletedPosts(post.channel_id, posts);
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index ae74059d1..1d853f979 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -3,6 +3,7 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var UserStore = require('./user_store.jsx');
+var ErrorStore = require('./error_store.jsx');
var EventEmitter = require('events').EventEmitter;
var Constants = require('../utils/constants.jsx');
@@ -21,6 +22,7 @@ class SocketStoreClass extends EventEmitter {
this.addChangeListener = this.addChangeListener.bind(this);
this.removeChangeListener = this.removeChangeListener.bind(this);
this.sendMessage = this.sendMessage.bind(this);
+ this.failCount = 0;
this.initialize();
}
@@ -37,27 +39,43 @@ class SocketStoreClass extends EventEmitter {
protocol = 'wss://';
}
var connUrl = protocol + location.host + '/api/v1/websocket';
- console.log('connecting to ' + connUrl); //eslint-disable-line no-console
+ if (this.failCount === 0) {
+ console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
+ }
conn = new WebSocket(connUrl);
- conn.onclose = function closeConn(evt) {
- console.log('websocket closed'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
+ conn.onopen = () => {
+ if (this.failCount > 0) {
+ console.log('websocket re-established connection'); //eslint-disable-line no-console
+ }
+
+ this.failCount = 0;
+ ErrorStore.storeLastError(null);
+ ErrorStore.emitChange();
+ };
+
+ conn.onclose = () => {
conn = null;
setTimeout(
- function reconnect() {
+ () => {
this.initialize();
- }.bind(this),
+ },
3000
);
- }.bind(this);
+ };
+
+ conn.onerror = (evt) => {
+ if (this.failCount === 0) {
+ console.log('websocket error ' + evt); //eslint-disable-line no-console
+ }
+
+ this.failCount = this.failCount + 1;
- conn.onerror = function connError(evt) {
- console.log('websocket error'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
+ 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.emitChange();
};
- conn.onmessage = function connMessage(evt) {
+ conn.onmessage = (evt) => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_MSG,
msg: JSON.parse(evt.data)
@@ -86,7 +104,7 @@ class SocketStoreClass extends EventEmitter {
var SocketStore = new SocketStoreClass();
-SocketStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+SocketStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index f75c1d4c3..8842263fa 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -14,6 +14,7 @@ var CHANGE_EVENT_SESSIONS = 'change_sessions';
var CHANGE_EVENT_AUDITS = 'change_audits';
var CHANGE_EVENT_TEAMS = 'change_teams';
var CHANGE_EVENT_STATUSES = 'change_statuses';
+var TOGGLE_IMPORT_MODAL_EVENT = 'toggle_import_modal';
class UserStoreClass extends EventEmitter {
constructor() {
@@ -34,6 +35,9 @@ class UserStoreClass extends EventEmitter {
this.emitStatusesChange = this.emitStatusesChange.bind(this);
this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this);
this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this);
+ this.emitToggleImportModal = this.emitToggleImportModal.bind(this);
+ this.addImportModalListener = this.addImportModalListener.bind(this);
+ this.removeImportModalListener = this.removeImportModalListener.bind(this);
this.setCurrentId = this.setCurrentId.bind(this);
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrentUser = this.getCurrentUser.bind(this);
@@ -114,6 +118,15 @@ class UserStoreClass extends EventEmitter {
removeStatusesChangeListener(callback) {
this.removeListener(CHANGE_EVENT_STATUSES, callback);
}
+ emitToggleImportModal(value) {
+ this.emit(TOGGLE_IMPORT_MODAL_EVENT, value);
+ }
+ addImportModalListener(callback) {
+ this.on(TOGGLE_IMPORT_MODAL_EVENT, callback);
+ }
+ removeImportModalListener(callback) {
+ this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback);
+ }
setCurrentId(id) {
this.gCurrentId = id;
if (id == null) {
@@ -321,6 +334,9 @@ UserStore.dispatchToken = AppDispatcher.register(function registry(payload) {
UserStore.pSetStatuses(action.statuses);
UserStore.emitStatusesChange();
break;
+ case ActionTypes.TOGGLE_IMPORT_THEME_MODAL:
+ UserStore.emitToggleImportModal(action.value);
+ break;
default:
}
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 6ccef0506..ab2965000 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -319,6 +319,84 @@ export function getAudits() {
);
}
+export function getLogs() {
+ if (isCallInProgress('getLogs')) {
+ return;
+ }
+
+ callTracker.getLogs = utils.getTimestamp();
+ client.getLogs(
+ (data, textStatus, xhr) => {
+ callTracker.getLogs = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_LOGS,
+ logs: data
+ });
+ },
+ (err) => {
+ callTracker.getLogs = 0;
+ dispatchError(err, 'getLogs');
+ }
+ );
+}
+
+export function getConfig() {
+ if (isCallInProgress('getConfig')) {
+ return;
+ }
+
+ callTracker.getConfig = utils.getTimestamp();
+ client.getConfig(
+ (data, textStatus, xhr) => {
+ callTracker.getConfig = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_CONFIG,
+ config: data
+ });
+ },
+ (err) => {
+ callTracker.getConfig = 0;
+ dispatchError(err, 'getConfig');
+ }
+ );
+}
+
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
@@ -556,28 +634,4 @@ export function getMyTeam() {
dispatchError(err, 'getMyTeam');
}
);
-}
-
-export function getConfig() {
- if (isCallInProgress('getConfig')) {
- return;
- }
-
- callTracker.getConfig = utils.getTimestamp();
- client.getConfig(
- function getConfigSuccess(data, textStatus, xhr) {
- callTracker.getConfig = 0;
-
- if (data && xhr.status !== 304) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_CONFIG,
- settings: data
- });
- }
- },
- function getConfigFailure(err) {
- callTracker.getConfig = 0;
- dispatchError(err, 'getConfig');
- }
- );
-}
+} \ No newline at end of file
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 10f9c0b37..715e26197 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -4,18 +4,14 @@ var BrowserStore = require('../stores/browser_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
export function track(category, action, label, prop, val) {
- global.window.snowplow('trackStructEvent', category, action, label, prop, val);
global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
}
export function trackPage() {
- global.window.snowplow('trackPageView');
global.window.analytics.page();
}
function handleError(methodName, xhr, status, err) {
- var LTracker = global.window.LTracker || [];
-
var e = null;
try {
e = JSON.parse(xhr.responseText);
@@ -31,7 +27,7 @@ function handleError(methodName, xhr, status, err) {
msg = 'error in ' + methodName + ' status=' + status + ' statusCode=' + xhr.status + ' err=' + err;
if (xhr.status === 0) {
- e = {message: 'There appears to be a problem with your internet connection'};
+ e = {message: 'There appears to be a problem with your internet connection', connErrorCount: 1};
} else {
e = {message: 'We received an unexpected status code from the server (' + xhr.status + ')'};
}
@@ -39,7 +35,6 @@ function handleError(methodName, xhr, status, err) {
console.error(msg); //eslint-disable-line no-console
console.error(e); //eslint-disable-line no-console
- LTracker.push(msg);
track('api', 'api_weberror', methodName, 'message', msg);
@@ -62,7 +57,7 @@ export function createTeamFromSignup(teamSignup, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(teamSignup),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createTeamFromSignup', xhr, status, err);
error(e);
@@ -77,7 +72,7 @@ export function createTeamWithSSO(team, service, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(team),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createTeamWithSSO', xhr, status, err);
error(e);
@@ -92,7 +87,7 @@ export function createUser(user, data, emailHash, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(user),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createUser', xhr, status, err);
error(e);
@@ -109,7 +104,7 @@ export function updateUser(user, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(user),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateUser', xhr, status, err);
error(e);
@@ -126,7 +121,7 @@ export function updatePassword(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('newPassword', xhr, status, err);
error(e);
@@ -143,7 +138,7 @@ export function updateUserNotifyProps(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateUserNotifyProps', xhr, status, err);
error(e);
@@ -158,7 +153,7 @@ export function updateRoles(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateRoles', xhr, status, err);
error(e);
@@ -179,7 +174,7 @@ export function updateActive(userId, active, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateActive', xhr, status, err);
error(e);
@@ -196,7 +191,7 @@ export function sendPasswordReset(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('sendPasswordReset', xhr, status, err);
error(e);
@@ -213,7 +208,7 @@ export function resetPassword(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('resetPassword', xhr, status, err);
error(e);
@@ -257,7 +252,7 @@ export function revokeSession(altId, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({id: altId}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('revokeSession', xhr, status, err);
error(e);
@@ -272,7 +267,7 @@ export function getSessions(userId, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getSessions', xhr, status, err);
error(e);
@@ -286,7 +281,7 @@ export function getAudits(userId, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getAudits', xhr, status, err);
error(e);
@@ -294,6 +289,78 @@ export function getAudits(userId, success, error) {
});
}
+export function getLogs(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/logs',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getLogs', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getConfig(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function saveConfig(config, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/save_config',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(config),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('saveConfig', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function testEmail(config, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/test_email',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(config),
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('testEmail', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -327,7 +394,7 @@ export function inviteMembers(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('inviteMembers', xhr, status, err);
error(e);
@@ -344,7 +411,7 @@ export function updateTeamDisplayName(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateTeamDisplayName', xhr, status, err);
error(e);
@@ -361,7 +428,7 @@ export function signupTeam(email, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('singupTeam', xhr, status, err);
error(e);
@@ -378,7 +445,7 @@ export function createTeam(team, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(team),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createTeam', xhr, status, err);
error(e);
@@ -393,7 +460,7 @@ export function findTeamByName(teamName, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({name: teamName}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('findTeamByName', xhr, status, err);
error(e);
@@ -408,7 +475,7 @@ export function findTeamsSendEmail(email, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('findTeamsSendEmail', xhr, status, err);
error(e);
@@ -425,7 +492,7 @@ export function findTeams(email, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({email: email}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('findTeams', xhr, status, err);
error(e);
@@ -440,7 +507,7 @@ export function createChannel(channel, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(channel),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createChannel', xhr, status, err);
error(e);
@@ -457,7 +524,7 @@ export function createDirectChannel(channel, userId, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({user_id: userId}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createDirectChannel', xhr, status, err);
error(e);
@@ -474,7 +541,7 @@ export function updateChannel(channel, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(channel),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateChannel', xhr, status, err);
error(e);
@@ -491,7 +558,7 @@ export function updateChannelDesc(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateChannelDesc', xhr, status, err);
error(e);
@@ -508,7 +575,7 @@ export function updateNotifyLevel(data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateNotifyLevel', xhr, status, err);
error(e);
@@ -522,7 +589,7 @@ export function joinChannel(id, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('joinChannel', xhr, status, err);
error(e);
@@ -538,7 +605,7 @@ export function leaveChannel(id, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('leaveChannel', xhr, status, err);
error(e);
@@ -554,7 +621,7 @@ export function deleteChannel(id, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('deleteChannel', xhr, status, err);
error(e);
@@ -570,7 +637,7 @@ export function updateLastViewedAt(channelId, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updateLastViewedAt', xhr, status, err);
error(e);
@@ -584,7 +651,7 @@ export function getChannels(success, error) {
url: '/api/v1/channels/',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
ifModified: true,
error: function onError(xhr, status, err) {
var e = handleError('getChannels', xhr, status, err);
@@ -599,7 +666,7 @@ export function getChannel(id, success, error) {
url: '/api/v1/channels/' + id + '/',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getChannel', xhr, status, err);
error(e);
@@ -614,7 +681,7 @@ export function getMoreChannels(success, error) {
url: '/api/v1/channels/more',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
ifModified: true,
error: function onError(xhr, status, err) {
var e = handleError('getMoreChannels', xhr, status, err);
@@ -629,7 +696,7 @@ export function getChannelCounts(success, error) {
url: '/api/v1/channels/counts',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
ifModified: true,
error: function onError(xhr, status, err) {
var e = handleError('getChannelCounts', xhr, status, err);
@@ -643,7 +710,7 @@ export function getChannelExtraInfo(id, success, error) {
url: '/api/v1/channels/' + id + '/extra_info',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getChannelExtraInfo', xhr, status, err);
error(e);
@@ -658,7 +725,7 @@ export function executeCommand(channelId, command, suggest, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('executeCommand', xhr, status, err);
error(e);
@@ -673,7 +740,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete)
dataType: 'json',
type: 'GET',
ifModified: true,
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getPosts', xhr, status, err);
error(e);
@@ -688,7 +755,7 @@ export function getPosts(channelId, since, success, error, complete) {
dataType: 'json',
type: 'GET',
ifModified: true,
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getPosts', xhr, status, err);
error(e);
@@ -704,7 +771,7 @@ export function getPost(channelId, postId, success, error) {
dataType: 'json',
type: 'GET',
ifModified: false,
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getPost', xhr, status, err);
error(e);
@@ -718,7 +785,7 @@ export function search(terms, success, error) {
dataType: 'json',
type: 'GET',
data: {terms: terms},
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('search', xhr, status, err);
error(e);
@@ -734,7 +801,7 @@ export function deletePost(channelId, id, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('deletePost', xhr, status, err);
error(e);
@@ -751,7 +818,7 @@ export function createPost(post, channel, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(post),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('createPost', xhr, status, err);
error(e);
@@ -777,7 +844,7 @@ export function updatePost(post, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(post),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('updatePost', xhr, status, err);
error(e);
@@ -794,7 +861,7 @@ export function addChannelMember(id, data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('addChannelMember', xhr, status, err);
error(e);
@@ -811,7 +878,7 @@ export function removeChannelMember(id, data, success, error) {
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('removeChannelMember', xhr, status, err);
error(e);
@@ -828,7 +895,7 @@ export function getProfiles(success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success: success,
+ success,
ifModified: true,
error: function onError(xhr, status, err) {
var e = handleError('getProfiles', xhr, status, err);
@@ -837,6 +904,21 @@ export function getProfiles(success, error) {
});
}
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function uploadFile(formData, success, error) {
var request = $.ajax({
url: '/api/v1/files/upload',
@@ -845,7 +927,7 @@ export function uploadFile(formData, success, error) {
cache: false,
contentType: false,
processData: false,
- success: success,
+ success,
error: function onError(xhr, status, err) {
if (err !== 'abort') {
var e = handleError('uploadFile', xhr, status, err);
@@ -865,7 +947,7 @@ export function getFileInfo(filename, success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getFileInfo', xhr, status, err);
error(e);
@@ -879,7 +961,7 @@ export function getPublicLink(data, success, error) {
dataType: 'json',
type: 'POST',
data: JSON.stringify(data),
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getPublicLink', xhr, status, err);
error(e);
@@ -895,7 +977,7 @@ export function uploadProfileImage(imageData, success, error) {
cache: false,
contentType: false,
processData: false,
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('uploadProfileImage', xhr, status, err);
error(e);
@@ -911,7 +993,7 @@ export function importSlack(fileData, success, error) {
cache: false,
contentType: false,
processData: false,
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('importTeam', xhr, status, err);
error(e);
@@ -919,13 +1001,26 @@ export function importSlack(fileData, success, error) {
});
}
+export function exportTeam(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/export_team',
+ type: 'GET',
+ dataType: 'json',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('exportTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getStatuses(success, error) {
$.ajax({
url: '/api/v1/users/status',
dataType: 'json',
contentType: 'application/json',
type: 'GET',
- success: success,
+ success,
error: function onError(xhr, status, err) {
var e = handleError('getStatuses', xhr, status, err);
error(e);
@@ -938,7 +1033,7 @@ export function getMyTeam(success, error) {
url: '/api/v1/teams/me',
dataType: 'json',
type: 'GET',
- success: success,
+ success,
ifModified: true,
error: function onError(xhr, status, err) {
var e = handleError('getMyTeam', xhr, status, err);
@@ -947,32 +1042,77 @@ export function getMyTeam(success, error) {
});
}
-export function updateValetFeature(data, success, error) {
+export function registerOAuthApp(app, success, error) {
$.ajax({
- url: '/api/v1/teams/update_valet_feature',
+ url: '/api/v1/oauth/register',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify(data),
+ data: JSON.stringify(app),
success: success,
- error: function onError(xhr, status, err) {
- var e = handleError('updateValetFeature', xhr, status, err);
+ error: (xhr, status, err) => {
+ const e = handleError('registerApp', xhr, status, err);
error(e);
}
});
- track('api', 'api_teams_update_valet_feature');
+ module.exports.track('api', 'api_apps_register');
}
-export function getConfig(success, error) {
+export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) {
$.ajax({
- url: '/api/v1/config/get_all',
+ url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state,
dataType: 'json',
+ contentType: 'application/json',
type: 'GET',
- ifModified: true,
- success: success,
- error: function onError(xhr, status, err) {
- var e = handleError('getConfig', xhr, status, err);
+ success,
+ error: (xhr, status, err) => {
+ const e = handleError('allowOAuth2', xhr, status, err);
+ error(e);
+ }
+ });
+
+ module.exports.track('api', 'api_users_allow_oauth2');
+}
+
+export function addIncomingHook(hook, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(hook),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addIncomingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteIncomingHook(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteIncomingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listIncomingHooks(success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/incoming/list',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listIncomingHooks', xhr, status, err);
error(e);
}
});
diff --git a/web/react/utils/config.js b/web/react/utils/config.js
deleted file mode 100644
index c7d1aa2bc..000000000
--- a/web/react/utils/config.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-export var config = {
-
- // Loggly configs
- LogglyWriteKey: '',
- LogglyConsoleErrors: true,
-
- // Segment configs
- SegmentWriteKey: '',
-
- // Feature switches
- AllowPublicLink: true,
- AllowInviteNames: true,
- RequireInviteNames: false,
- AllowSignupDomainsWizard: false,
-
- // Google Developer Key (for Youtube API links)
- // Leave blank to disable
- GoogleDeveloperKey: '',
-
- // Privacy switches
- ShowEmail: true,
-
- // Links
- TermsLink: '/static/help/configure_links.html',
- PrivacyLink: '/static/help/configure_links.html',
- AboutLink: '/static/help/configure_links.html',
- HelpLink: '/static/help/configure_links.html',
- ReportProblemLink: '/static/help/configure_links.html',
- HomeLink: '',
-
- // Toggle whether or not users are shown a message about agreeing to the Terms of Service during the signup process
- ShowTermsDuringSignup: false,
-
- ThemeColors: ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']
-};
-
-// Flavor strings
-export var strings = {
- Team: 'team',
- TeamPlural: 'teams',
- Company: 'company',
- CompanyPlural: 'companies'
-};
-
-global.window.config = config;
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 18b7ff59c..75e80bc7e 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -9,6 +9,7 @@ module.exports = {
CLICK_CHANNEL: null,
CREATE_CHANNEL: null,
+ LEAVE_CHANNEL: null,
RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null,
RECIEVED_MORE_CHANNELS: null,
@@ -33,7 +34,11 @@ module.exports = {
CLICK_TEAM: null,
RECIEVED_TEAM: null,
- RECIEVED_CONFIG: null
+ RECIEVED_CONFIG: null,
+ RECIEVED_LOGS: null,
+ RECIEVED_ALL_TEAMS: null,
+
+ TOGGLE_IMPORT_THEME_MODAL: null
}),
PayloadSources: keyMirror({
@@ -67,6 +72,10 @@ module.exports = {
MAX_FILE_SIZE: 50000000, // 50 MB
THUMBNAIL_WIDTH: 128,
THUMBNAIL_HEIGHT: 100,
+ WEB_VIDEO_WIDTH: 640,
+ WEB_VIDEO_HEIGHT: 480,
+ MOBILE_VIDEO_WIDTH: 480,
+ MOBILE_VIDEO_HEIGHT: 360,
DEFAULT_CHANNEL: 'town-square',
OFFTOPIC_CHANNEL: 'off-topic',
GITLAB_SERVICE: 'gitlab',
@@ -107,5 +116,166 @@ module.exports = {
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>"
+ 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>",
+ THEMES: {
+ default: {
+ type: 'Mattermost',
+ sidebarBg: '#fafafa',
+ sidebarText: '#999999',
+ sidebarUnreadText: '#333333',
+ sidebarTextHoverBg: '#e6f2fa',
+ sidebarTextHoverColor: '#999999',
+ sidebarTextActiveBg: '#e1e1e1',
+ sidebarTextActiveColor: '#111111',
+ sidebarHeaderBg: '#2389d7',
+ sidebarHeaderTextColor: '#ffffff',
+ onlineIndicator: '#7DBE00',
+ mentionBj: '#2389d7',
+ mentionColor: '#ffffff',
+ centerChannelBg: '#ffffff',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2389d7',
+ buttonBg: '#2389d7',
+ buttonColor: '#FFFFFF'
+ },
+ organization: {
+ type: 'Organization',
+ sidebarBg: '#2071a7',
+ sidebarText: '#bfcde8',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#136197',
+ sidebarTextHoverColor: '#bfcde8',
+ sidebarTextActiveBg: '#136197',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#2f81b7',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#7DBE00',
+ mentionBj: '#136197',
+ mentionColor: '#bfcde8',
+ centerChannelBg: '#f2f4f8',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2f81b7',
+ buttonBg: '#1dacfc',
+ buttonColor: '#FFFFFF'
+ },
+ mattermostDark: {
+ type: 'Mattermost Dark',
+ sidebarBg: '#1B2C3E',
+ sidebarText: '#bbbbbb',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#4A5664',
+ sidebarTextHoverColor: '#bbbbbb',
+ sidebarTextActiveBg: '#39769C',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1B2C3E',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#55C5B2',
+ mentionBj: '#B74A4A',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#2F3E4E',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#5de5da',
+ linkColor: '#A4FFEB',
+ buttonBg: '#1dacfc',
+ buttonColor: '#FFFFFF'
+ },
+ windows10: {
+ type: 'Windows Dark',
+ sidebarBg: '#171717',
+ sidebarText: '#eee',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#302e30',
+ sidebarTextHoverColor: '#fff',
+ sidebarTextActiveBg: '#484748',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1f1f1f',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#0177e7',
+ mentionBj: '#0177e7',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#1F1F1F',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#CC992D',
+ linkColor: '#0177e7',
+ buttonBg: '#0177e7',
+ buttonColor: '#FFFFFF'
+ }
+ },
+ THEME_ELEMENTS: [
+ {
+ id: 'sidebarBg',
+ uiName: 'Sidebar BG'
+ },
+ {
+ id: 'sidebarText',
+ uiName: 'Sidebar Text'
+ },
+ {
+ id: 'sidebarHeaderBg',
+ uiName: 'Sidebar Header BG'
+ },
+ {
+ id: 'sidebarHeaderTextColor',
+ uiName: 'Sidebar Header Text'
+ },
+ {
+ id: 'sidebarUnreadText',
+ uiName: 'Sidebar Unread Text'
+ },
+ {
+ id: 'sidebarTextHoverBg',
+ uiName: 'Sidebar Text Hover BG'
+ },
+ {
+ id: 'sidebarTextHoverColor',
+ uiName: 'Sidebar Text Hover Color'
+ },
+ {
+ id: 'sidebarTextActiveBg',
+ uiName: 'Sidebar Text Active BG'
+ },
+ {
+ id: 'sidebarTextActiveColor',
+ uiName: 'Sidebar Text Active Color'
+ },
+ {
+ id: 'onlineIndicator',
+ uiName: 'Online Indicator'
+ },
+ {
+ id: 'mentionBj',
+ uiName: 'Mention Jewel BG'
+ },
+ {
+ id: 'mentionColor',
+ uiName: 'Mention Jewel Text'
+ },
+ {
+ id: 'centerChannelBg',
+ uiName: 'Center Channel BG'
+ },
+ {
+ id: 'centerChannelColor',
+ uiName: 'Center Channel Text'
+ },
+ {
+ id: 'newMessageSeparator',
+ uiName: 'New message separator'
+ },
+ {
+ id: 'linkColor',
+ uiName: 'Link Color'
+ },
+ {
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+
+ {
+ id: 'buttonColor',
+ uiName: 'Button Text'
+ }
+ ]
};
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
new file mode 100644
index 000000000..a7c837199
--- /dev/null
+++ b/web/react/utils/emoticons.jsx
@@ -0,0 +1,158 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const emoticonPatterns = {
+ smile: /:-?\)/g, // :)
+ open_mouth: /:o/gi, // :o
+ scream: /:-o/gi, // :-o
+ smirk: /:-?]/g, // :]
+ grinning: /:-?d/gi, // :D
+ stuck_out_tongue_closed_eyes: /x-d/gi, // x-d
+ stuck_out_tongue_winking_eye: /:-?p/gi, // :p
+ rage: /:-?[\[@]/g, // :@
+ frowning: /:-?\(/g, // :(
+ sob: /:['’]-?\(|:&#x27;\(/g, // :`(
+ kissing_heart: /:-?\*/g, // :*
+ pensive: /:-?\//g, // :/
+ confounded: /:-?s/gi, // :s
+ flushed: /:-?\|/g, // :|
+ relaxed: /:-?\$/g, // :$
+ mask: /:-x/gi, // :-x
+ heart: /<3|&lt;3/g, // <3
+ broken_heart: /<\/3|&lt;&#x2F;3/g, // </3
+ thumbsup: /:\+1:/g, // :+1:
+ thumbsdown: /:\-1:/g // :-1:
+};
+
+function initializeEmoticonMap() {
+ const emoticonNames =
+ ('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' +
+ 'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' +
+ 'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' +
+ 'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' +
+ 'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' +
+ 'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
+ 'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
+ 'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
+ 'black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,' +
+ 'blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,' +
+ 'bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,' +
+ 'bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
+ 'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
+ 'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
+ 'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
+ 'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' +
+ 'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' +
+ 'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' +
+ 'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' +
+ 'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' +
+ 'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' +
+ 'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' +
+ 'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' +
+ 'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' +
+ 'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' +
+ 'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' +
+ 'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' +
+ 'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' +
+ 'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' +
+ 'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' +
+ 'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' +
+ 'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' +
+ 'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' +
+ 'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' +
+ 'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' +
+ 'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' +
+ 'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' +
+ 'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' +
+ 'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' +
+ 'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' +
+ 'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' +
+ 'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' +
+ 'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' +
+ 'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' +
+ 'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' +
+ 'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' +
+ 'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' +
+ 'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' +
+ 'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' +
+ 'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' +
+ 'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' +
+ 'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' +
+ 'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' +
+ 'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' +
+ 'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' +
+ 'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' +
+ 'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' +
+ 'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' +
+ 'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' +
+ 'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' +
+ 'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' +
+ 'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' +
+ 'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' +
+ 'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
+ 'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
+ 'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
+ 'ski,skull,sleeping,sleepy,slot_machine,small_blue_diamond,small_orange_diamond,small_red_triangle,' +
+ 'small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,smirk,smirk_cat,smoking,snail,snake,' +
+ 'snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,spaghetti,sparkle,sparkler,' +
+ 'sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,star2,stars,station,' +
+ 'statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,stuck_out_tongue_closed_eyes,' +
+ 'stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,sunrise_over_mountains,surfer,' +
+ 'sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,symbols,syringe,tada,' +
+ 'tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,thought_balloon,' +
+ 'three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,tophat,' +
+ 'tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
+ 'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
+ 'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
+ 'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
+ 'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' +
+ 'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' +
+ 'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' +
+ 'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' +
+ 'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(',');
+
+ // use a map to help make lookups faster instead of having to use indexOf on an array
+ const out = new Map();
+
+ for (let i = 0; i < emoticonNames.length; i++) {
+ out[emoticonNames[i]] = true;
+ }
+
+ return out;
+}
+
+const emoticonMap = initializeEmoticonMap();
+
+export function handleEmoticons(text, tokens) {
+ let output = text;
+
+ function replaceEmoticonWithToken(match, name) {
+ if (emoticonMap[name]) {
+ const index = tokens.size;
+ const alias = `MM_EMOTICON${index}`;
+
+ tokens.set(alias, {
+ value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
+ originalText: match
+ });
+
+ return alias;
+ }
+
+ return match;
+ }
+
+ output = output.replace(/:([a-zA-Z0-9_-]+):/g, replaceEmoticonWithToken);
+
+ $.each(emoticonPatterns, (name, pattern) => {
+ // this might look a bit funny, but since the name isn't contained in the actual match
+ // like with the named emoticons, we need to add it in manually
+ output = output.replace(pattern, (match) => replaceEmoticonWithToken(match, name));
+ });
+
+ return output;
+}
+
+function getImagePathForEmoticon(name) {
+ return `/static/images/emoji/${name}.png`;
+}
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
new file mode 100644
index 000000000..7e88f8644
--- /dev/null
+++ b/web/react/utils/markdown.jsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const TextFormatting = require('./text_formatting.jsx');
+
+const marked = require('marked');
+
+export class MattermostMarkdownRenderer extends marked.Renderer {
+ constructor(options, formattingOptions = {}) {
+ super(options);
+
+ this.heading = this.heading.bind(this);
+ this.text = this.text.bind(this);
+
+ this.formattingOptions = formattingOptions;
+ }
+
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
+ heading(text, level, raw) {
+ const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
+ return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
+ }
+
+ link(href, title, text) {
+ let outHref = href;
+
+ if (outHref.lastIndexOf('http', 0) !== 0) {
+ outHref = `http://${outHref}`;
+ }
+
+ let output = '<a class="theme markdown__link" href="' + outHref + '"';
+ if (title) {
+ output += ' title="' + title + '"';
+ }
+ output += ' target="_blank">' + text + '</a>';
+
+ return output;
+ }
+
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ 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);
+ }
+}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
new file mode 100644
index 000000000..56bf49c3f
--- /dev/null
+++ b/web/react/utils/text_formatting.jsx
@@ -0,0 +1,295 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Autolinker = require('autolinker');
+const Constants = require('./constants.jsx');
+const Emoticons = require('./emoticons.jsx');
+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:
+// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
+// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
+// - singleline - Specifies whether or not to remove newlines. Defaults to false.
+// - emoticons - Enables emoticon parsing. Defaults to true.
+// - markdown - Enables markdown parsing. Defaults to true.
+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
+ });
+ } else {
+ output = sanitizeHtml(text);
+ output = doFormatText(output, options);
+ }
+
+ // replace newlines with spaces if necessary
+ if (options.singleline) {
+ output = replaceNewlines(output);
+ }
+
+ return output;
+}
+
+// Performs most of the actual formatting work for formatText. Not intended to be called normally.
+export function doFormatText(text, options) {
+ let output = text;
+
+ const tokens = new Map();
+
+ // replace important words and phrases with tokens
+ output = autolinkUrls(output, tokens);
+ output = autolinkAtMentions(output, tokens);
+ output = autolinkHashtags(output, tokens);
+
+ if (!('emoticons' in options) || options.emoticon) {
+ output = Emoticons.handleEmoticons(output, tokens);
+ }
+
+ if (options.searchTerm) {
+ output = highlightSearchTerm(output, tokens, options.searchTerm);
+ }
+
+ if (!('mentionHighlight' in options) || options.mentionHighlight) {
+ output = highlightCurrentMentions(output, tokens);
+ }
+
+ // reinsert tokens with formatted versions of the important words and phrases
+ output = replaceTokens(output, tokens);
+
+ return output;
+}
+
+export function sanitizeHtml(text) {
+ let output = text;
+
+ // normal string.replace only does a single occurrance so use a regex instead
+ output = output.replace(/&/g, '&amp;');
+ output = output.replace(/</g, '&lt;');
+ output = output.replace(/>/g, '&gt;');
+ output = output.replace(/'/g, '&apos;');
+ output = output.replace(/"/g, '&quot;');
+
+ return output;
+}
+
+function autolinkUrls(text, tokens) {
+ function replaceUrlWithToken(autolinker, match) {
+ const linkText = match.getMatchedText();
+ let url = linkText;
+
+ if (url.lastIndexOf('http', 0) !== 0) {
+ url = `http://${linkText}`;
+ }
+
+ const index = tokens.size;
+ const alias = `MM_LINK${index}`;
+
+ tokens.set(alias, {
+ value: `<a class='theme' target='_blank' href='${url}'>${linkText}</a>`,
+ originalText: linkText
+ });
+
+ return alias;
+ }
+
+ // we can't just use a static autolinker because we need to set replaceFn
+ const autolinker = new Autolinker({
+ urls: true,
+ email: true,
+ phone: false,
+ twitter: false,
+ hashtag: false,
+ replaceFn: replaceUrlWithToken
+ });
+
+ return autolinker.link(text);
+}
+
+function autolinkAtMentions(text, tokens) {
+ let output = text;
+
+ 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
+ });
+
+ return prefix + alias;
+ }
+
+ return fullMatch;
+ }
+
+ output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken);
+
+ return output;
+}
+
+function highlightCurrentMentions(text, tokens) {
+ let output = text;
+
+ const mentionKeys = UserStore.getCurrentMentionKeys();
+
+ // look for any existing tokens which are self mentions and should be highlighted
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (mentionKeys.indexOf(token.originalText) !== -1) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_SELFMENTION${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='mention-highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for self mentions in the text
+ function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
+ const index = tokens.size;
+ const alias = `MM_SELFMENTION${index}`;
+
+ tokens.set(alias, {
+ value: `<span class='mention-highlight'>${mention}</span>`,
+ originalText: mention
+ });
+
+ return prefix + alias;
+ }
+
+ for (const mention of UserStore.getCurrentMentionKeys()) {
+ output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), replaceCurrentMentionWithToken);
+ }
+
+ return output;
+}
+
+function autolinkHashtags(text, tokens) {
+ let output = text;
+
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText.lastIndexOf('#', 0) === 0) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_HASHTAG${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ // look for hashtags in the text
+ function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
+ const index = tokens.size;
+ const alias = `MM_HASHTAG${index}`;
+
+ tokens.set(alias, {
+ value: `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`,
+ originalText: hashtag
+ });
+
+ return prefix + alias;
+ }
+
+ return output.replace(/(^|\W)(#[a-zA-Z][a-zA-Z0-9.\-_]*)\b/g, replaceHashtagWithToken);
+}
+
+function highlightSearchTerm(text, tokens, searchTerm) {
+ let output = text;
+
+ var newTokens = new Map();
+ for (const [alias, token] of tokens) {
+ if (token.originalText === searchTerm) {
+ const index = tokens.size + newTokens.size;
+ const newAlias = `MM_SEARCHTERM${index}`;
+
+ newTokens.set(newAlias, {
+ value: `<span class='search-highlight'>${alias}</span>`,
+ originalText: token.originalText
+ });
+
+ output = output.replace(alias, newAlias);
+ }
+ }
+
+ // the new tokens are stashed in a separate map since we can't add objects to a map during iteration
+ for (const newToken of newTokens) {
+ tokens.set(newToken[0], newToken[1]);
+ }
+
+ function replaceSearchTermWithToken(fullMatch, prefix, word) {
+ const index = tokens.size;
+ const alias = `MM_SEARCHTERM${index}`;
+
+ tokens.set(alias, {
+ value: `<span class='search-highlight'>${word}</span>`,
+ originalText: word
+ });
+
+ return prefix + alias;
+ }
+
+ return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken);
+}
+
+function replaceTokens(text, tokens) {
+ let output = text;
+
+ // iterate backwards through the map so that we do replacement in the opposite order that we added tokens
+ const aliases = [...tokens.keys()];
+ for (let i = aliases.length - 1; i >= 0; i--) {
+ const alias = aliases[i];
+ const token = tokens.get(alias);
+ output = output.replace(alias, token.value);
+ }
+
+ return output;
+}
+
+function replaceNewlines(text) {
+ return text.replace(/\n/g, ' ');
+}
+
+// A click handler that can be used with the results of TextFormatting.formatText to add default functionality
+// to clicked hashtags and @mentions.
+export function handleClick(e) {
+ const mentionAttribute = e.target.getAttributeNode('data-mention');
+ const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
+
+ if (mentionAttribute) {
+ Utils.searchForTerm(mentionAttribute.value);
+ } else if (hashtagAttribute) {
+ Utils.searchForTerm(hashtagAttribute.value);
+ }
+}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 71cd1d344..61dcae6d8 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -4,12 +4,12 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var AsyncClient = require('./async_client.jsx');
var client = require('./client.jsx');
var Autolinker = require('autolinker');
-import {config} from '../utils/config.js';
export function isEmail(email) {
var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
@@ -55,6 +55,29 @@ export function isTestDomain() {
return false;
}
+export function isInRole(roles, inRole) {
+ var parts = roles.split(' ');
+ for (var i = 0; i < parts.length; i++) {
+ if (parts[i] === inRole) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function isAdmin(roles) {
+ if (isInRole(roles, 'admin')) {
+ return true;
+ }
+
+ if (isInRole(roles, 'system_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
export function getDomainWithOutSub() {
var parts = window.location.host.split('.');
@@ -91,7 +114,7 @@ export function notifyMe(title, body, channel) {
if (channel) {
switchChannel(channel);
} else {
- window.location.href = '/';
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
}
};
setTimeout(function closeNotificationOnTimeout() {
@@ -292,15 +315,14 @@ function getYoutubeEmbed(link) {
$('.video-type.' + youtubeId).html('Youtube - ');
$('.video-uploader.' + youtubeId).html(metadata.channelTitle);
$('.video-title.' + youtubeId).find('a').html(metadata.title);
- $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time')[0].scrollHeight);
}
- if (config.GoogleDeveloperKey) {
+ if (global.window.config.GoogleDeveloperKey) {
$.ajax({
async: true,
url: 'https://www.googleapis.com/youtube/v3/videos',
type: 'GET',
- data: {part: 'snippet', id: youtubeId, key: config.GoogleDeveloperKey},
+ data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
success: success
});
}
@@ -434,205 +456,6 @@ export function searchForTerm(term) {
});
}
-var puncStartRegex = /^((?![@#])\W)+/g;
-var puncEndRegex = /(\W)+$/g;
-
-export function textToJsx(textin, options) {
- var text = textin;
- if (options && options.singleline) {
- var repRegex = new RegExp('\n', 'g'); //eslint-disable-line no-control-regex
- text = text.replace(repRegex, ' ');
- }
-
- var searchTerm = '';
- if (options && options.searchTerm) {
- searchTerm = options.searchTerm.toLowerCase();
- }
-
- var mentionClass = 'mention-highlight';
- if (options && options.noMentionHighlight) {
- mentionClass = '';
- }
-
- var inner = [];
-
- // Function specific regex
- var hashRegex = /^href="#[^']+"|(^#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g;
-
- var implicitKeywords = UserStore.getCurrentMentionKeys();
-
- var lines = text.split('\n');
- for (let i = 0; i < lines.length; i++) {
- var line = lines[i];
- var words = line.split(' ');
- var highlightSearchClass = '';
- for (let z = 0; z < words.length; z++) {
- var word = words[z];
- var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim();
- var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here
- var explicitMention = mentionRegex.exec(trimWord);
-
- if (searchTerm !== '') {
- let searchWords = searchTerm.split(' ');
- for (let idx in searchWords) {
- if ({}.hasOwnProperty.call(searchWords, idx)) {
- let searchWord = searchWords[idx];
- if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- break;
- } else if (searchWord.charAt(searchWord.length - 1) === '*') {
- let searchWordPrefix = searchWord.slice(0, -1);
- if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) {
- highlightSearchClass = ' search-highlight';
- break;
- }
- }
- }
- }
- }
-
- if (explicitMention &&
- (UserStore.getProfileByUsername(explicitMention[1]) ||
- Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1)) {
- let name = explicitMention[1];
-
- // do both a non-case sensitive and case senstive check
- let mClass = '';
- if (implicitKeywords.indexOf('@' + name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@' + name) !== -1) {
- mClass = mentionClass;
- }
-
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
-
- if (searchTerm === name) {
- highlightSearchClass = ' search-highlight';
- }
-
- inner.push(
- <span key={name + i + z + '_span'}>
- {prefix}
- <a
- className={mClass + highlightSearchClass + ' mention-link'}
- key={name + i + z + '_link'}
- href='#'
- onClick={() => searchForTerm(name)} //eslint-disable-line no-loop-func
- >
- @{name}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (testUrlMatch(word).length) {
- let match = testUrlMatch(word)[0];
- let link = match.link;
-
- let prefix = word.substring(0, word.indexOf(match.text));
- let suffix = word.substring(word.indexOf(match.text) + match.text.length);
-
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- key={word + i + z + '_link'}
- className={'theme' + highlightSearchClass}
- target='_blank'
- href={link}
- >
- {match.text}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (trimWord.match(hashRegex)) {
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
- let mClass = '';
- if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
- mClass = mentionClass;
- }
-
- if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- }
-
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- key={word + i + z + '_hash'}
- className={'theme ' + mClass + highlightSearchClass}
- href='#'
- onClick={searchForTerm.bind(this, trimWord)} //eslint-disable-line no-loop-func
- >
- {trimWord}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) {
- let suffix = word.match(puncEndRegex);
- let prefix = word.match(puncStartRegex);
-
- if (trimWord.charAt(0) === '@') {
- if (searchTerm === trimWord.substring(1).toLowerCase()) {
- highlightSearchClass = ' search-highlight';
- }
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <a
- className={mentionClass + highlightSearchClass}
- key={name + i + z + '_link'}
- href='#'
- >
- {trimWord}
- </a>
- {suffix}
- {' '}
- </span>
- );
- } else {
- inner.push(
- <span key={word + i + z + '_span'}>
- {prefix}
- <span className={mentionClass + highlightSearchClass}>
- {replaceHtmlEntities(trimWord)}
- </span>
- {suffix}
- {' '}
- </span>
- );
- }
- } else if (word === '') {
-
- // if word is empty dont include a span
-
- } else {
- inner.push(
- <span key={word + i + z + '_span'}>
- <span className={highlightSearchClass}>
- {replaceHtmlEntities(word)}
- </span>
- {' '}
- </span>
- );
- }
- highlightSearchClass = '';
- }
- if (i !== lines.length - 1) {
- inner.push(
- <br key={'br_' + i}/>
- );
- }
- }
-
- return inner;
-}
-
export function getFileType(extin) {
var ext = extin.toLowerCase();
if (Constants.IMAGE_TYPES.indexOf(ext) > -1) {
@@ -719,7 +542,131 @@ export function toTitleCase(str) {
return str.replace(/\w\S*/g, doTitleCase);
}
-export function changeCss(className, classValue) {
+export function applyTheme(theme) {
+ if (theme.sidebarBg) {
+ changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1);
+ changeCss('@media(max-width: 768px){.search-bar__container', 'background:' + theme.sidebarBg, 1);
+ }
+
+ if (theme.sidebarText) {
+ changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
+ changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
+ changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
+ changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
+ }
+
+ if (theme.sidebarUnreadText) {
+ changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1);
+ }
+
+ if (theme.sidebarTextHoverBg) {
+ changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1);
+ }
+
+ if (theme.sidebarTextHoverColor) {
+ changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2);
+ }
+
+ if (theme.sidebarTextActiveBg) {
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1);
+ }
+
+ if (theme.sidebarTextActiveColor) {
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2);
+ }
+
+ if (theme.sidebarHeaderBg) {
+ changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1);
+ changeCss('#navbar .navbar-default', 'background:' + theme.sidebarHeaderBg, 1);
+ }
+
+ if (theme.sidebarHeaderTextColor) {
+ changeCss('.sidebar--left .team__header .header__info, .sidebar--menu .team__header .header__info', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.sidebar--left .team__header .user__name, .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8), 1);
+ changeCss('.sidebar--left .team__header:hover .user__name, .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('.modal .modal-header .modal-title, .modal .modal-header .modal-title .name, .modal .modal-header button.close', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-brand .heading, ', 'color:' + theme.sidebarHeaderTextColor, 1);
+ changeCss('#navbar .navbar-default .navbar-toggle .icon-bar, ', 'background:' + theme.sidebarHeaderTextColor, 1);
+ }
+
+ if (theme.onlineIndicator) {
+ changeCss('.sidebar--left .status .online--icon', 'fill:' + theme.onlineIndicator, 1);
+ }
+
+ if (theme.mentionBj) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'background:' + theme.mentionBj, 1);
+ changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj, 1);
+ }
+
+ if (theme.mentionColor) {
+ changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor, 2);
+ changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor, 2);
+ }
+
+ if (theme.centerChannelBg) {
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
+ changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.search-bar__container .search__form .search-bar', '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', '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, .loading-screen .loading__content .round', 'color:' + theme.centerChannelColor, 1);
+ changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-body hr', '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);
+ changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
+ changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2);
+ changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
+ changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
+ changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
+ }
+
+ if (theme.newMessageSeparator) {
+ changeCss('.new-separator .separator__text', 'color:' + theme.newMessageSeparator, 1);
+ changeCss('.new-separator .separator__hr', 'border-color:' + changeOpacity(theme.newMessageSeparator, 0.5), 1);
+ }
+
+ if (theme.linkColor) {
+ changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1);
+ changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1);
+ }
+
+ if (theme.buttonBg) {
+ changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
+ changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ }
+
+ if (theme.buttonColor) {
+ changeCss('.btn.btn-primary', 'color:' + theme.buttonColor, 2);
+ }
+}
+
+export function changeCss(className, classValue, classRepeat) {
// we need invisible container to store additional css definitions
var cssMainContainer = $('#css-modifier-container');
if (cssMainContainer.length === 0) {
@@ -729,9 +676,9 @@ export function changeCss(className, classValue) {
}
// and we need one div for each class
- var classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
+ var classContainer = cssMainContainer.find('div[data-class="' + className + classRepeat + '"]');
if (classContainer.length === 0) {
- classContainer = $('<div data-class="' + className + '"></div>');
+ classContainer = $('<div data-class="' + className + classRepeat + '"></div>');
classContainer.appendTo(cssMainContainer);
}
@@ -828,14 +775,12 @@ export function isValidUsername(name) {
} else if (name.length < 3 || name.length > 15) {
error = 'Must be between 3 and 15 characters';
} else if (!(/^[a-z0-9\.\-\_]+$/).test(name)) {
- error = "Must contain only lowercase letters, numbers, and the symbols '.', '-', and '_'.";
+ error = "Must contain only letters, numbers, and the symbols '.', '-', and '_'.";
} else if (!(/[a-z]/).test(name.charAt(0))) {
error = 'First character must be a letter.';
} else {
- var lowerName = name.toLowerCase().trim();
-
for (var i = 0; i < Constants.RESERVED_USERNAMES.length; i++) {
- if (lowerName === Constants.RESERVED_USERNAMES[i]) {
+ if (name === Constants.RESERVED_USERNAMES[i]) {
error = 'Cannot use a reserved word as a username.';
break;
}
@@ -939,57 +884,47 @@ Image.prototype.load = function imageLoad(url, progressCallback) {
Image.prototype.completedPercentage = 0;
export function changeColor(colourIn, amt) {
- var usePound = false;
- var col = colourIn;
-
- if (col[0] === '#') {
- col = col.slice(1);
- usePound = true;
- }
-
- var num = parseInt(col, 16);
-
- var r = (num >> 16) + amt;
+ var hex = colourIn;
+ var lum = amt;
- if (r > 255) {
- r = 255;
- } else if (r < 0) {
- r = 0;
+ // validate hex string
+ hex = String(hex).replace(/[^0-9a-f]/gi, '');
+ if (hex.length < 6) {
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
+ lum = lum || 0;
- var b = ((num >> 8) & 0x00FF) + amt;
-
- if (b > 255) {
- b = 255;
- } else if (b < 0) {
- b = 0;
+ // convert to decimal and change luminosity
+ var rgb = '#';
+ var c;
+ var i;
+ for (i = 0; i < 3; i++) {
+ c = parseInt(hex.substr(i * 2, 2), 16);
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
+ rgb += ('00' + c).substr(c.length);
}
- var g = (num & 0x0000FF) + amt;
-
- if (g > 255) {
- g = 255;
- } else if (g < 0) {
- g = 0;
- }
+ return rgb;
+}
- var pound = '#';
- if (!usePound) {
- pound = '';
+export function changeOpacity(oldColor, opacity) {
+ var color = oldColor;
+ if (color[0] === '#') {
+ color = color.slice(1);
}
- return pound + String('000000' + (g | (b << 8) | (r << 16)).toString(16)).slice(-6);
-}
+ if (color.length === 3) {
+ const tempColor = color;
+ color = '';
-export function changeOpacity(oldColor, opacity) {
- var col = oldColor;
- if (col[0] === '#') {
- col = col.slice(1);
+ color += tempColor[0] + tempColor[0];
+ color += tempColor[1] + tempColor[1];
+ color += tempColor[2] + tempColor[2];
}
- var r = parseInt(col.substring(0, 2), 16);
- var g = parseInt(col.substring(2, 4), 16);
- var b = parseInt(col.substring(4, 6), 16);
+ var r = parseInt(color.substring(0, 2), 16);
+ var g = parseInt(color.substring(2, 4), 16);
+ var b = parseInt(color.substring(4, 6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
}
@@ -1127,3 +1062,15 @@ export function importSlack(file, success, error) {
client.importSlack(formData, success, error);
}
+
+export function getTeamURLFromAddressBar() {
+ return window.location.href.split('/channels')[0];
+}
+
+export function getShortenedTeamURL() {
+ const teamURL = getTeamURLFromAddressBar();
+ if (teamURL.length > 35) {
+ return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length) + '/';
+ }
+ return teamURL + '/';
+}