summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/access_history_modal.jsx38
-rw-r--r--web/react/components/activity_log_modal.jsx24
-rw-r--r--web/react/components/admin_console/admin_controller.jsx48
-rw-r--r--web/react/components/admin_console/admin_navbar_dropdown.jsx8
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx31
-rw-r--r--web/react/components/admin_console/admin_sidebar_header.jsx6
-rw-r--r--web/react/components/admin_console/email_settings.jsx89
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx4
-rw-r--r--web/react/components/admin_console/image_settings.jsx6
-rw-r--r--web/react/components/admin_console/log_settings.jsx4
-rw-r--r--web/react/components/admin_console/logs.jsx6
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx4
-rw-r--r--web/react/components/admin_console/rate_settings.jsx4
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx2
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx2
-rw-r--r--web/react/components/admin_console/service_settings.jsx4
-rw-r--r--web/react/components/admin_console/sql_settings.jsx6
-rw-r--r--web/react/components/admin_console/team_analytics.jsx8
-rw-r--r--web/react/components/admin_console/team_settings.jsx4
-rw-r--r--web/react/components/admin_console/team_users.jsx8
-rw-r--r--web/react/components/admin_console/user_item.jsx6
-rw-r--r--web/react/components/authorize.jsx2
-rw-r--r--web/react/components/center_panel.jsx73
-rw-r--r--web/react/components/change_url_modal.jsx2
-rw-r--r--web/react/components/channel_header.jsx198
-rw-r--r--web/react/components/channel_info_modal.jsx111
-rw-r--r--web/react/components/channel_invite_modal.jsx76
-rw-r--r--web/react/components/channel_loader.jsx23
-rw-r--r--web/react/components/channel_members_modal.jsx47
-rw-r--r--web/react/components/channel_notifications_modal.jsx (renamed from web/react/components/channel_notifications.jsx)139
-rw-r--r--web/react/components/channel_view.jsx8
-rw-r--r--web/react/components/command_list.jsx99
-rw-r--r--web/react/components/create_comment.jsx37
-rw-r--r--web/react/components/create_post.jsx60
-rw-r--r--web/react/components/delete_channel_modal.jsx120
-rw-r--r--web/react/components/delete_post_modal.jsx159
-rw-r--r--web/react/components/docs.jsx41
-rw-r--r--web/react/components/edit_channel_header_modal.jsx126
-rw-r--r--web/react/components/edit_channel_modal.jsx150
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx8
-rw-r--r--web/react/components/edit_post_modal.jsx20
-rw-r--r--web/react/components/error_bar.jsx2
-rw-r--r--web/react/components/file_attachment.jsx8
-rw-r--r--web/react/components/file_attachment_list.jsx6
-rw-r--r--web/react/components/file_preview.jsx23
-rw-r--r--web/react/components/file_upload.jsx8
-rw-r--r--web/react/components/find_team.jsx4
-rw-r--r--web/react/components/get_link_modal.jsx144
-rw-r--r--web/react/components/get_team_invite_link_modal.jsx45
-rw-r--r--web/react/components/invite_member_modal.jsx97
-rw-r--r--web/react/components/login.jsx26
-rw-r--r--web/react/components/member_list.jsx2
-rw-r--r--web/react/components/member_list_item.jsx16
-rw-r--r--web/react/components/member_list_team.jsx49
-rw-r--r--web/react/components/member_list_team_item.jsx14
-rw-r--r--web/react/components/mention.jsx61
-rw-r--r--web/react/components/mention_list.jsx276
-rw-r--r--web/react/components/message_wrapper.jsx2
-rw-r--r--web/react/components/more_channels.jsx30
-rw-r--r--web/react/components/more_direct_channels.jsx6
-rw-r--r--web/react/components/msg_typing.jsx9
-rw-r--r--web/react/components/navbar.jsx191
-rw-r--r--web/react/components/navbar_dropdown.jsx35
-rw-r--r--web/react/components/new_channel_flow.jsx12
-rw-r--r--web/react/components/new_channel_modal.jsx4
-rw-r--r--web/react/components/notify_counts.jsx6
-rw-r--r--web/react/components/password_reset.jsx4
-rw-r--r--web/react/components/password_reset_form.jsx2
-rw-r--r--web/react/components/password_reset_send_link.jsx4
-rw-r--r--web/react/components/popover_list_members.jsx70
-rw-r--r--web/react/components/post.jsx135
-rw-r--r--web/react/components/post_attachment.jsx2
-rw-r--r--web/react/components/post_attachment_list.jsx2
-rw-r--r--web/react/components/post_attachment_oembed.jsx103
-rw-r--r--web/react/components/post_body.jsx186
-rw-r--r--web/react/components/post_body_additional_content.jsx28
-rw-r--r--web/react/components/post_deleted_modal.jsx6
-rw-r--r--web/react/components/post_focus_view.jsx110
-rw-r--r--web/react/components/post_header.jsx24
-rw-r--r--web/react/components/post_info.jsx170
-rw-r--r--web/react/components/posts_view.jsx187
-rw-r--r--web/react/components/posts_view_container.jsx156
-rw-r--r--web/react/components/providers.json376
-rw-r--r--web/react/components/register_app_modal.jsx194
-rw-r--r--web/react/components/removed_from_channel_modal.jsx8
-rw-r--r--web/react/components/rename_channel_modal.jsx59
-rw-r--r--web/react/components/rhs_comment.jsx97
-rw-r--r--web/react/components/rhs_header_post.jsx4
-rw-r--r--web/react/components/rhs_root_post.jsx206
-rw-r--r--web/react/components/rhs_thread.jsx40
-rw-r--r--web/react/components/search_autocomplete.jsx339
-rw-r--r--web/react/components/search_bar.jsx63
-rw-r--r--web/react/components/search_results.jsx35
-rw-r--r--web/react/components/search_results_header.jsx4
-rw-r--r--web/react/components/search_results_item.jsx114
-rw-r--r--web/react/components/settings_sidebar.jsx4
-rw-r--r--web/react/components/sidebar.jsx116
-rw-r--r--web/react/components/sidebar_header.jsx12
-rw-r--r--web/react/components/sidebar_right.jsx50
-rw-r--r--web/react/components/sidebar_right_menu.jsx56
-rw-r--r--web/react/components/signup_team.jsx8
-rw-r--r--web/react/components/signup_team_complete.jsx14
-rw-r--r--web/react/components/signup_user_complete.jsx8
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx100
-rw-r--r--web/react/components/suggestion/command_provider.jsx46
-rw-r--r--web/react/components/suggestion/emoticon_provider.jsx91
-rw-r--r--web/react/components/suggestion/search_channel_provider.jsx69
-rw-r--r--web/react/components/suggestion/search_suggestion_list.jsx86
-rw-r--r--web/react/components/suggestion/search_user_provider.jsx62
-rw-r--r--web/react/components/suggestion/suggestion_box.jsx163
-rw-r--r--web/react/components/suggestion/suggestion_list.jsx125
-rw-r--r--web/react/components/team_export_tab.jsx2
-rw-r--r--web/react/components/team_general_tab.jsx14
-rw-r--r--web/react/components/team_import_tab.jsx4
-rw-r--r--web/react/components/team_members.jsx130
-rw-r--r--web/react/components/team_members_modal.jsx71
-rw-r--r--web/react/components/team_settings.jsx12
-rw-r--r--web/react/components/team_settings_modal.jsx4
-rw-r--r--web/react/components/team_signup_display_name_page.jsx4
-rw-r--r--web/react/components/team_signup_email_item.jsx2
-rw-r--r--web/react/components/team_signup_password_page.jsx6
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx4
-rw-r--r--web/react/components/team_signup_url_page.jsx6
-rw-r--r--web/react/components/team_signup_username_page.jsx4
-rw-r--r--web/react/components/team_signup_welcome_page.jsx6
-rw-r--r--web/react/components/team_signup_with_email.jsx23
-rw-r--r--web/react/components/team_signup_with_sso.jsx6
-rw-r--r--web/react/components/textbox.jsx265
-rw-r--r--web/react/components/time_since.jsx4
-rw-r--r--web/react/components/toggle_modal_button.jsx73
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx28
-rw-r--r--web/react/components/tutorial/tutorial_tip.jsx35
-rw-r--r--web/react/components/user_profile.jsx23
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx7
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx12
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx10
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx10
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx4
-rw-r--r--web/react/components/user_settings/user_settings.jsx22
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx154
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx18
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx16
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx80
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx168
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx8
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx29
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx77
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx62
-rw-r--r--web/react/components/view_image.jsx45
149 files changed, 4601 insertions, 3472 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 27959ec7e..85c28ca5c 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -2,11 +2,11 @@
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
-var UserStore = require('../stores/user_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var LoadingScreen = require('./loading_screen.jsx');
-var Utils = require('../utils/utils.jsx');
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import LoadingScreen from './loading_screen.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class AccessHistoryModal extends React.Component {
constructor(props) {
@@ -14,8 +14,8 @@ export default class AccessHistoryModal extends React.Component {
this.onAuditChange = this.onAuditChange.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
- this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
this.formatAuditInfo = this.formatAuditInfo.bind(this);
this.handleRevokedSession = this.handleRevokedSession.bind(this);
@@ -32,17 +32,23 @@ export default class AccessHistoryModal extends React.Component {
onShow() {
AsyncClient.getAudits();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ } else {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
}
}
onHide() {
this.setState({moreInfo: []});
- this.props.onModalDismissed();
+ this.props.onHide();
}
componentDidMount() {
UserStore.addAuditsChangeListener(this.onAuditChange);
+
+ if (this.props.show) {
+ this.onShow();
+ }
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -54,7 +60,7 @@ export default class AccessHistoryModal extends React.Component {
}
onAuditChange() {
var newState = this.getStateFromStoresForAudits();
- if (!Utils.areStatesEqual(newState.audits, this.state.audits)) {
+ if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) {
this.setState(newState);
}
}
@@ -102,7 +108,7 @@ export default class AccessHistoryModal extends React.Component {
case '/channels/update_header':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group header';
break;
- default:
+ default: {
let userIdField = [];
let userId = '';
let username = '';
@@ -126,11 +132,12 @@ export default class AccessHistoryModal extends React.Component {
break;
}
+ }
} else if (currentActionURL.indexOf('/oauth') === 0) {
const oauthInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
- case '/oauth/register':
+ case '/oauth/register': {
const clientIdField = oauthInfo[0].split('=');
if (clientIdField[0] === 'client_id') {
@@ -138,6 +145,7 @@ export default class AccessHistoryModal extends React.Component {
}
break;
+ }
case '/oauth/allow':
if (oauthInfo[0] === 'attempt') {
currentAuditDesc = 'Attempted to allow a new OAuth service access';
@@ -198,7 +206,7 @@ export default class AccessHistoryModal extends React.Component {
}
break;
- case '/users/update_roles':
+ case '/users/update_roles': {
const userRoles = userInfo[0].split('=')[1];
currentAuditDesc = 'Updated user role(s) to ';
@@ -209,7 +217,8 @@ export default class AccessHistoryModal extends React.Component {
}
break;
- case '/users/update_active':
+ }
+ case '/users/update_active': {
const updateType = userInfo[0].split('=')[0];
const updateField = userInfo[0].split('=')[1];
@@ -236,6 +245,7 @@ export default class AccessHistoryModal extends React.Component {
}
break;
+ }
case '/users/send_password_reset':
currentAuditDesc = 'Sent an email to ' + userInfo[0].split('=')[1] + ' to reset your password';
break;
@@ -406,5 +416,5 @@ export default class AccessHistoryModal extends React.Component {
AccessHistoryModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index ef3077470..f5341c0bc 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const UserStore = require('../stores/user_store.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
+import UserStore from '../stores/user_store.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
const Modal = ReactBootstrap.Modal;
-const LoadingScreen = require('./loading_screen.jsx');
-const Utils = require('../utils/utils.jsx');
+import LoadingScreen from './loading_screen.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class ActivityLogModal extends React.Component {
constructor(props) {
@@ -51,17 +51,23 @@ export default class ActivityLogModal extends React.Component {
onShow() {
AsyncClient.getSessions();
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ } else {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
}
}
onHide() {
this.setState({moreInfo: []});
- this.props.onModalDismissed();
+ this.props.onHide();
}
componentDidMount() {
UserStore.addSessionsChangeListener(this.onListenerChange);
+
+ if (this.props.show) {
+ this.onShow();
+ }
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -73,7 +79,7 @@ export default class ActivityLogModal extends React.Component {
}
onListenerChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState.sessions, this.state.sessions)) {
+ if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) {
this.setState(newState);
}
}
@@ -178,5 +184,5 @@ export default class ActivityLogModal extends React.Component {
ActivityLogModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 8e0ab0555..4f144b0dd 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -1,25 +1,25 @@
// Copyright (c) 2015 Mattermost, 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 Utils = require('../../utils/utils.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');
-var TeamAnalyticsTab = require('./team_analytics.jsx');
+import AdminSidebar from './admin_sidebar.jsx';
+import AdminStore from '../../stores/admin_store.jsx';
+import TeamStore from '../../stores/team_store.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+import EmailSettingsTab from './email_settings.jsx';
+import LogSettingsTab from './log_settings.jsx';
+import LogsTab from './logs.jsx';
+import FileSettingsTab from './image_settings.jsx';
+import PrivacySettingsTab from './privacy_settings.jsx';
+import RateSettingsTab from './rate_settings.jsx';
+import GitLabSettingsTab from './gitlab_settings.jsx';
+import SqlSettingsTab from './sql_settings.jsx';
+import TeamSettingsTab from './team_settings.jsx';
+import ServiceSettingsTab from './service_settings.jsx';
+import TeamUsersTab from './team_users.jsx';
+import TeamAnalyticsTab from './team_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {
@@ -189,4 +189,12 @@ export default class AdminController extends React.Component {
</div>
);
}
-} \ No newline at end of file
+}
+
+AdminController.defaultProps = {
+};
+
+AdminController.propTypes = {
+ tab: React.PropTypes.string,
+ teamId: React.PropTypes.string
+};
diff --git a/web/react/components/admin_console/admin_navbar_dropdown.jsx b/web/react/components/admin_console/admin_navbar_dropdown.jsx
index df8da94e1..783d45de6 100644
--- a/web/react/components/admin_console/admin_navbar_dropdown.jsx
+++ b/web/react/components/admin_console/admin_navbar_dropdown.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Utils from '../../utils/utils.jsx';
+import * as Client from '../../utils/client.jsx';
+import TeamStore from '../../stores/team_store.jsx';
-var Constants = require('../../utils/constants.jsx');
+import Constants from '../../utils/constants.jsx';
function getStateFromStores() {
return {currentTeam: TeamStore.getCurrent()};
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 0d52ae347..cc98c495e 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -1,9 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
-var SelectTeamModal = require('./select_team_modal.jsx');
-var Utils = require('../../utils/utils.jsx');
+import AdminSidebarHeader from './admin_sidebar_header.jsx';
+import SelectTeamModal from './select_team_modal.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -80,6 +83,12 @@ export default class AdminSidebar extends React.Component {
render() {
var count = '*';
var teams = 'Loading';
+ const removeTooltip = (
+ <Tooltip id='remove-team-tooltip'>{'Remove team from sidebar menu'}</Tooltip>
+ );
+ const addTeamTooltip = (
+ <Tooltip id='add-team-tooltip'>{'Add team from sidebar menu'}</Tooltip>
+ );
if (this.props.teams != null) {
count = '' + Object.keys(this.props.teams).length;
@@ -102,14 +111,19 @@ export default class AdminSidebar extends React.Component {
className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
>
{team.name}
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={removeTooltip}
+ >
<span
className='menu-icon--right menu__close'
onClick={this.removeTeam.bind(this, team.id)}
style={{cursor: 'pointer'}}
- title='Remove team from sidebar menu'
>
- {'x'}
+ {'×'}
</span>
+ </OverlayTrigger>
</a>
</li>
<li>
@@ -245,15 +259,20 @@ export default class AdminSidebar extends React.Component {
<span className='icon fa fa-gear'></span>
<span>{'TEAMS (' + count + ')'}</span>
<span className='menu-icon--right'>
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={addTeamTooltip}
+ >
<a
href='#'
onClick={this.showTeamSelect}
>
<i
className='fa fa-plus'
- title='Add team to sidebar menu'
></i>
</a>
+ </OverlayTrigger>
</span>
</h4>
</li>
diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx
index fd6d92c4a..bfd479939 100644
--- a/web/react/components/admin_console/admin_sidebar_header.jsx
+++ b/web/react/components/admin_console/admin_sidebar_header.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var AdminNavbarDropdown = require('./admin_navbar_dropdown.jsx');
-var UserStore = require('../../stores/user_store.jsx');
-var Utils = require('../../utils/utils.jsx');
+import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
+import UserStore from '../../stores/user_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
export default class SidebarHeader extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 40e00ff04..238ace3da 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import crypto from 'crypto';
export default class EmailSettings extends React.Component {
constructor(props) {
@@ -18,6 +18,7 @@ export default class EmailSettings extends React.Component {
this.state = {
sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
saveNeeded: false,
serverError: null,
emailSuccess: null,
@@ -36,6 +37,14 @@ export default class EmailSettings extends React.Component {
s.sendEmailNotifications = false;
}
+ if (action === 'sendPushNotifications_true') {
+ s.sendPushNotifications = true;
+ }
+
+ if (action === 'sendPushNotifications_false') {
+ s.sendPushNotifications = false;
+ }
+
this.setState(s);
}
@@ -43,11 +52,12 @@ export default class EmailSettings extends React.Component {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.SendPushlNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim();
config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
@@ -296,7 +306,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='feedbackName'
ref='feedbackName'
- placeholder='Ex: "Mattermost Notification", "System", "No-Reply"'
+ placeholder='E.g.: "Mattermost Notification", "System", "No-Reply"'
defaultValue={this.props.config.EmailSettings.FeedbackName}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -318,7 +328,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='feedbackEmail'
ref='feedbackEmail'
- placeholder='Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
+ placeholder='E.g.: "mattermost@yourcompany.com", "admin@yourcompany.com"'
defaultValue={this.props.config.EmailSettings.FeedbackEmail}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -340,7 +350,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='SMTPUsername'
ref='SMTPUsername'
- placeholder='Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
+ placeholder='E.g.: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
defaultValue={this.props.config.EmailSettings.SMTPUsername}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -362,7 +372,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='SMTPPassword'
ref='SMTPPassword'
- placeholder='Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ placeholder='E.g.: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
defaultValue={this.props.config.EmailSettings.SMTPPassword}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -384,7 +394,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='SMTPServer'
ref='SMTPServer'
- placeholder='Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
+ placeholder='E.g.: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
defaultValue={this.props.config.EmailSettings.SMTPServer}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -406,7 +416,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='SMTPPort'
ref='SMTPPort'
- placeholder='Ex: "25", "465"'
+ placeholder='E.g.: "25", "465"'
defaultValue={this.props.config.EmailSettings.SMTPPort}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -476,7 +486,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='InviteSalt'
ref='InviteSalt'
- placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ placeholder='E.g.: "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
defaultValue={this.props.config.EmailSettings.InviteSalt}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -507,7 +517,7 @@ export default class EmailSettings extends React.Component {
className='form-control'
id='PasswordResetSalt'
ref='PasswordResetSalt'
- placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ placeholder='E.g.: "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
onChange={this.handleChange}
disabled={!this.state.sendEmailNotifications}
@@ -526,6 +536,61 @@ export default class EmailSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='sendPushNotifications'
+ >
+ {'Send Push Notifications: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='true'
+ ref='sendPushNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PushNotificationServer'
+ >
+ {'Push Notification Server:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PushNotificationServer'
+ ref='PushNotificationServer'
+ placeholder='E.g.: "https://push.mattermost.com"'
+ defaultValue={this.props.config.EmailSettings.PushNotificationServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendPushNotifications}
+ />
+ <p className='help-text'>{'Location of the push notification server.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index f8fb6d115..8c689a2d8 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class GitLabSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
index 8b577e012..e1ffad7d3 100644
--- a/web/react/components/admin_console/image_settings.jsx
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import crypto from 'crypto';
export default class FileSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 7e9eda89b..a91cc57ab 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class LogSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/logs.jsx b/web/react/components/admin_console/logs.jsx
index 3449c78d9..01135f1b8 100644
--- a/web/react/components/admin_console/logs.jsx
+++ b/web/react/components/admin_console/logs.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, 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');
+import AdminStore from '../../stores/admin_store.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class Logs extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
index f2d22f36e..61393f1c6 100644
--- a/web/react/components/admin_console/privacy_settings.jsx
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class PrivacySettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
index 4d71777c4..ca9fcb074 100644
--- a/web/react/components/admin_console/rate_settings.jsx
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class RateSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
index 35d3cdd17..5ff7c3413 100644
--- a/web/react/components/admin_console/reset_password_modal.jsx
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
+import * as Client from '../../utils/client.jsx';
var Modal = ReactBootstrap.Modal;
export default class ResetPasswordModal extends React.Component {
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index 22189821b..858b6bbfe 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -57,7 +57,7 @@ export default class SelectTeamModal extends React.Component {
<select
ref='team'
size='10'
- style={{width: '100%'}}
+ className='form-control'
>
{options}
</select>
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 53c89a942..908eb709a 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class ServiceSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
index b43108bf7..2a55f7324 100644
--- a/web/react/components/admin_console/sql_settings.jsx
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import crypto from 'crypto';
export default class SqlSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
index 0c9d1f61b..e28699d3c 100644
--- a/web/react/components/admin_console/team_analytics.jsx
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Utils = require('../../utils/utils.jsx');
-var LineChart = require('./line_chart.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as Utils from '../../utils/utils.jsx';
+import LineChart from './line_chart.jsx';
export default class TeamAnalytics extends React.Component {
constructor(props) {
@@ -221,7 +221,7 @@ export default class TeamAnalytics extends React.Component {
var openChannelCount = (
<div className='col-sm-3'>
<div className='total-count'>
- <div className='title'>{'Public Groups'}<i className='fa fa-unlock-alt'/></div>
+ <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
<div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
</div>
</div>
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index 6587184ea..7991b9a01 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
export default class TeamSettings extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
index 7161139e6..2d9657956 100644
--- a/web/react/components/admin_console/team_users.jsx
+++ b/web/react/components/admin_console/team_users.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Client from '../../utils/client.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+import UserItem from './user_item.jsx';
+import ResetPasswordModal from './reset_password_modal.jsx';
export default class UserList extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 2badaf0e5..ef0b61460 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Utils = require('../../utils/utils.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as Utils from '../../utils/utils.jsx';
export default class UserItem extends React.Component {
constructor(props) {
@@ -227,7 +227,6 @@ export default class UserItem extends React.Component {
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
@@ -237,7 +236,6 @@ export default class UserItem extends React.Component {
<ul
className='dropdown-menu member-menu'
role='menu'
- aria-labelledby='channel_header_dropdown'
>
{makeAdmin}
{makeMember}
diff --git a/web/react/components/authorize.jsx b/web/react/components/authorize.jsx
index 74709bcab..32e39fbff 100644
--- a/web/react/components/authorize.jsx
+++ b/web/react/components/authorize.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
+import * as Client from '../utils/client.jsx';
export default class Authorize extends React.Component {
constructor(props) {
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 3ee40bb86..a1043431d 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -1,17 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const TutorialIntroScreens = require('./tutorial/tutorial_intro_screens.jsx');
-const CreatePost = require('./create_post.jsx');
-const PostsViewContainer = require('./posts_view_container.jsx');
-const ChannelHeader = require('./channel_header.jsx');
-const Navbar = require('./navbar.jsx');
-const FileUploadOverlay = require('./file_upload_overlay.jsx');
+import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
+import CreatePost from './create_post.jsx';
+import PostsViewContainer from './posts_view_container.jsx';
+import PostFocusView from './post_focus_view.jsx';
+import ChannelHeader from './channel_header.jsx';
+import Navbar from './navbar.jsx';
+import FileUploadOverlay from './file_upload_overlay.jsx';
-const PreferenceStore = require('../stores/preference_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
+import PreferenceStore from '../stores/preference_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
-const Constants = require('../utils/constants.jsx');
+import * as Utils from '../utils/utils.jsx';
+
+import Constants from '../utils/constants.jsx';
const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
@@ -20,26 +24,68 @@ export default class CenterPanel extends React.Component {
super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onChannelChange = this.onChannelChange.bind(this);
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
- this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
+ this.state = {
+ showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS,
+ showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS
+ };
}
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
+ ChannelStore.addChangeListener(this.onChannelChange);
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ ChannelStore.removeChangeListener(this.onChannelChange);
}
onPreferenceChange() {
const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'});
this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
}
+ onChannelChange() {
+ this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
+ }
render() {
+ const channel = ChannelStore.getCurrent();
+ var handleClick = null;
let postsContainer;
+ let createPost;
if (this.state.showTutorialScreens) {
postsContainer = <TutorialIntroScreens />;
+ createPost = null;
+ } else if (this.state.showPostFocus) {
+ postsContainer = <PostFocusView />;
+
+ handleClick = function clickHandler(e) {
+ e.preventDefault();
+ Utils.switchChannel(channel);
+ };
+
+ createPost = (
+ <div
+ id='archive-link-home'
+ >
+ <a
+ href=''
+ onClick={handleClick}
+ >
+ {'You are viewing the Archives. Click here to jump to recent messages. '}
+ {<i className='fa fa-arrow-down'></i>}
+ </a>
+ </div>
+ );
} else {
postsContainer = <PostsViewContainer />;
+ createPost = (
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ );
}
return (
@@ -62,12 +108,7 @@ export default class CenterPanel extends React.Component {
<ChannelHeader />
</div>
{postsContainer}
- <div
- className='post-create__container'
- id='post-create'
- >
- <CreatePost />
- </div>
+ {createPost}
</div>
</div>
</div>
diff --git a/web/react/components/change_url_modal.jsx b/web/react/components/change_url_modal.jsx
index 714e93ff8..bbe93f58d 100644
--- a/web/react/components/change_url_modal.jsx
+++ b/web/react/components/change_url_modal.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
-var Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
export default class ChangeUrlModal extends React.Component {
constructor(props) {
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 895dc5fe4..59ceb038e 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -1,28 +1,34 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const NavbarSearchBox = require('./search_bar.jsx');
-const MessageWrapper = require('./message_wrapper.jsx');
-const PopoverListMembers = require('./popover_list_members.jsx');
-const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
-const ChannelInviteModal = require('./channel_invite_modal.jsx');
-const ChannelMembersModal = require('./channel_members_modal.jsx');
+import NavbarSearchBox from './search_bar.jsx';
+import MessageWrapper from './message_wrapper.jsx';
+import PopoverListMembers from './popover_list_members.jsx';
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
+import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
+import ChannelInfoModal from './channel_info_modal.jsx';
+import ChannelInviteModal from './channel_invite_modal.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
+import ChannelNotificationsModal from './channel_notifications_modal.jsx';
+import DeleteChannelModal from './delete_channel_modal.jsx';
+import ToggleModalButton from './toggle_modal_button.jsx';
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const SearchStore = require('../stores/search_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import SearchStore from '../stores/search_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const Utils = require('../utils/utils.jsx');
-const TextFormatting = require('../utils/text_formatting.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as Utils from '../utils/utils.jsx';
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const Popover = ReactBootstrap.Popover;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+const Tooltip = ReactBootstrap.Tooltip;
export default class ChannelHeader extends React.Component {
constructor(props) {
@@ -34,16 +40,18 @@ export default class ChannelHeader extends React.Component {
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
- state.showInviteModal = false;
state.showMembersModal = false;
this.state = state;
}
getStateFromStores() {
+ const extraInfo = ChannelStore.getCurrentExtraInfo();
+
return {
channel: ChannelStore.getCurrent(),
memberChannel: ChannelStore.getCurrentMember(),
memberTeam: UserStore.getCurrentUser(),
- users: ChannelStore.getCurrentExtraInfo().members,
+ users: extraInfo.members,
+ userCount: extraInfo.member_count,
searchVisible: SearchStore.getSearchResults() !== null
};
}
@@ -63,7 +71,7 @@ export default class ChannelHeader extends React.Component {
}
onListenerChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
$('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
@@ -92,9 +100,11 @@ export default class ChannelHeader extends React.Component {
let terms = '';
if (user.notify_props && user.notify_props.mention_keys) {
const termKeys = UserStore.getCurrentMentionKeys();
+
if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) {
termKeys.splice(termKeys.indexOf('@all'), 1);
}
+
if (user.notify_props.channel === 'true' && termKeys.indexOf('@channel') !== -1) {
termKeys.splice(termKeys.indexOf('@channel'), 1);
}
@@ -114,6 +124,7 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
+ const recentMentionsTooltip = <Tooltip id='recentMentionsTooltip'>{'Recent Mentions'}</Tooltip>;
const popoverContent = (
<Popover
id='hader-popover'
@@ -158,17 +169,13 @@ export default class ChannelHeader extends React.Component {
key='edit_header_direct'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
{'Set Channel Header...'}
- </a>
+ </ToggleModalButton>
</li>
);
} else {
@@ -177,15 +184,13 @@ export default class ChannelHeader extends React.Component {
key='view_info'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- data-toggle='modal'
- data-target='#channel_info'
- data-channelid={channel.id}
- href='#'
+ dialogType={ChannelInfoModal}
+ dialogProps={{channel}}
>
{'View Info'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -195,13 +200,13 @@ export default class ChannelHeader extends React.Component {
key='add_members'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- onClick={() => this.setState({showInviteModal: true})}
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
>
{'Add Members'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -228,17 +233,13 @@ export default class ChannelHeader extends React.Component {
key='set_channel_header'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={EditChannelHeaderModal}
+ dialogProps={{channel}}
>
- {'Set '}{channelTerm}{' Header...'}
- </a>
+ {`Set ${channelTerm} Header...`}
+ </ToggleModalButton>
</li>
);
dropdownContents.push(
@@ -260,58 +261,55 @@ export default class ChannelHeader extends React.Component {
key='notification_preferences'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#channel_notifications'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{channel}}
>
{'Notification Preferences'}
- </a>
+ </ToggleModalButton>
</li>
);
- if (!ChannelStore.isDefault(channel)) {
- if (isAdmin) {
- dropdownContents.push(
- <li
- key='rename_channel'
- role='presentation'
+ if (isAdmin) {
+ dropdownContents.push(
+ <li
+ key='rename_channel'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ data-toggle='modal'
+ data-target='#rename_channel'
+ data-display={channel.display_name}
+ data-name={channel.name}
+ data-channelid={channel.id}
>
- <a
- role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#rename_channel'
- data-display={channel.display_name}
- data-name={channel.name}
- data-channelid={channel.id}
- >
- {'Rename '}{channelTerm}{'...'}
- </a>
- </li>
- );
+ {'Rename '}{channelTerm}{'...'}
+ </a>
+ </li>
+ );
+
+ if (!ChannelStore.isDefault(channel)) {
dropdownContents.push(
<li
key='delete_channel'
role='presentation'
>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#delete_channel'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={DeleteChannelModal}
+ dialogProps={{channel}}
>
{'Delete '}{channelTerm}{'...'}
- </a>
+ </ToggleModalButton>
</li>
);
}
+ }
+ if (!ChannelStore.isDefault(channel)) {
dropdownContents.push(
<li
key='leave_channel'
@@ -373,37 +371,26 @@ export default class ChannelHeader extends React.Component {
<th>
<PopoverListMembers
members={this.state.users}
+ memberCount={this.state.userCount}
channelId={channel.id}
/>
</th>
<th className='search-bar__container'><NavbarSearchBox /></th>
<th>
<div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
+ <OverlayTrigger
+ delayShow={400}
+ placement='bottom'
+ overlay={recentMentionsTooltip}
>
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- {'Recent Mentions'}
- </a>
- </li>
- </ul>
+ <a
+ href='#'
+ type='button'
+ onClick={this.searchMentions}
+ >
+ {'@'}
+ </a>
+ </OverlayTrigger>
</div>
</th>
</tr>
@@ -414,13 +401,10 @@ export default class ChannelHeader extends React.Component {
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
channel={channel}
/>
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
- />
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
+ channel={channel}
/>
</div>
);
diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx
index bccd8d0b9..18e125de3 100644
--- a/web/react/components/channel_info_modal.jsx
+++ b/web/react/components/channel_info_modal.jsx
@@ -1,88 +1,57 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-
-export default class CommandList extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- channel_id: ChannelStore.getCurrentId()
- };
- }
-
- componentDidMount() {
- var self = this;
- if (this.refs.modal) {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
- var button = e.relatedTarget;
- self.setState({channel_id: $(button).attr('data-channelid')});
- });
- }
- }
+const Modal = ReactBootstrap.Modal;
+export default class ChannelInfoModal extends React.Component {
render() {
- var channel = ChannelStore.get(this.state.channel_id);
-
+ let channel = this.props.channel;
if (!channel) {
- channel = {};
- channel.display_name = 'No Channel Found';
- channel.name = 'No Channel Found';
- channel.id = 'No Channel Found';
+ channel = {
+ display_name: 'No Channel Found',
+ name: 'No Channel Found',
+ id: 'No Channel Found'
+ };
}
return (
- <div
- className='modal fade'
- ref='modal'
- id='channel_info'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onHide}
>
- <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'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >
- <span className='name'>{channel.display_name}</span>
- </h4>
- </div>
- <div className='modal-body'>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>Channel Name: </div>
+ <Modal.Header closeButtton={true}>
+ {channel.display_name}
+ </Modal.Header>
+ <Modal.Body ref='modalBody'>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Channel Name:'}</div>
<div className='col-sm-9'>{channel.display_name}</div>
- </div>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>Channel Handle:</div>
+ </div>
+ <div className='row form-group'>
+ <div className='col-sm-3 info__label'>{'Channel Handle:'}</div>
<div className='col-sm-9'>{channel.name}</div>
- </div>
- <div className='row'>
- <div className='col-sm-3 info__label'>Channel ID:</div>
- <div className='col-sm-9'>{channel.id}</div>
- </div>
</div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >Close</button>
+ <div className='row'>
+ <div className='col-sm-3 info__label'>{'Channel ID:'}</div>
+ <div className='col-sm-9'>{channel.id}</div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Close'}
+ </button>
+ </Modal.Footer>
+ </Modal>
);
}
}
+
+ChannelInfoModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx
index 7c1032321..7dac39942 100644
--- a/web/react/components/channel_invite_modal.jsx
+++ b/web/react/components/channel_invite_modal.jsx
@@ -1,15 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const MemberList = require('./member_list.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
+import MemberList from './member_list.jsx';
+import LoadingScreen from './loading_screen.jsx';
-const UserStore = require('../stores/user_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
const Modal = ReactBootstrap.Modal;
@@ -22,6 +22,17 @@ export default class ChannelInviteModal extends React.Component {
this.state = this.getStateFromStores();
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(this.props, nextProps)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(this.state, nextState)) {
+ return true;
+ }
+
+ return false;
+ }
getStateFromStores() {
function getId(user) {
return user.id;
@@ -42,21 +53,17 @@ export default class ChannelInviteModal extends React.Component {
return a.username.localeCompare(b.username);
});
- var channelName = '';
- if (ChannelStore.getCurrent()) {
- channelName = ChannelStore.getCurrent().display_name;
- }
-
return {
nonmembers,
- memberIds,
- channelName,
loading
};
}
onShow() {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ } else {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
}
}
componentDidUpdate(prevProps) {
@@ -78,34 +85,20 @@ export default class ChannelInviteModal extends React.Component {
}
onListenerChange() {
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
}
handleInvite(userId) {
- // Make sure the user isn't already a member of the channel
- if (this.state.memberIds.indexOf(userId) > -1) {
- return;
- }
-
var data = {};
data.user_id = userId;
- Client.addChannelMember(ChannelStore.getCurrentId(), data,
+ Client.addChannelMember(
+ this.props.channel.id,
+ data,
() => {
- var nonmembers = this.state.nonmembers;
- var memberIds = this.state.memberIds;
-
- for (var i = 0; i < nonmembers.length; i++) {
- if (userId === nonmembers[i].id) {
- nonmembers[i].invited = true;
- memberIds.push(userId);
- break;
- }
- }
-
- this.setState({inviteError: null, memberIds, nonmembers});
- AsyncClient.getChannelExtraInfo(true);
+ this.setState({inviteError: null});
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
@@ -113,11 +106,6 @@ export default class ChannelInviteModal extends React.Component {
);
}
render() {
- var maxHeight = 1000;
- if (Utils.windowHeight() <= 1200) {
- maxHeight = Utils.windowHeight() - 300;
- }
-
var inviteError = null;
if (this.state.inviteError) {
inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>);
@@ -146,14 +134,13 @@ export default class ChannelInviteModal extends React.Component {
<Modal
dialogClassName='more-modal'
show={this.props.show}
- onHide={this.props.onModalDismissed}
+ onHide={this.props.onHide}
>
<Modal.Header closeButton={true}>
- <Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title>
+ <Modal.Title>{'Add New Members to '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
</Modal.Header>
<Modal.Body
ref='modalBody'
- style={{maxHeight}}
>
{inviteError}
{content}
@@ -162,7 +149,7 @@ export default class ChannelInviteModal extends React.Component {
<button
type='button'
className='btn btn-default'
- onClick={this.props.onModalDismissed}
+ onClick={this.props.onHide}
>
{'Close'}
</button>
@@ -174,5 +161,6 @@ export default class ChannelInviteModal extends React.Component {
ChannelInviteModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 4fc115a92..0d1d9efd7 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -5,14 +5,15 @@
to the server on page load. This is to prevent other React controls from spamming
AsyncClient with requests. */
-var AsyncClient = require('../utils/async_client.jsx');
-var SocketStore = require('../stores/socket_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
+import * as AsyncClient from '../utils/async_client.jsx';
+import SocketStore from '../stores/socket_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
-var Utils = require('../utils/utils.jsx');
-var Constants = require('../utils/constants.jsx');
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
export default class ChannelLoader extends React.Component {
constructor(props) {
@@ -27,8 +28,8 @@ export default class ChannelLoader extends React.Component {
componentDidMount() {
/* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId());
- AsyncClient.getChannels(true, true);
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannels();
+ AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams();
AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
@@ -69,6 +70,10 @@ export default class ChannelLoader extends React.Component {
Utils.applyTheme(Constants.THEMES.default);
}
+ // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
+ const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}).value;
+ Utils.applyFont(selectedFont);
+
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx
index 2fa7ae8ff..d1b9df988 100644
--- a/web/react/components/channel_members_modal.jsx
+++ b/web/react/components/channel_members_modal.jsx
@@ -1,15 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const MemberList = require('./member_list.jsx');
-const ChannelInviteModal = require('./channel_invite_modal.jsx');
+import MemberList from './member_list.jsx';
+import ChannelInviteModal from './channel_invite_modal.jsx';
-const UserStore = require('../stores/user_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
-const AsyncClient = require('../utils/async_client.jsx');
-const Client = require('../utils/client.jsx');
-const Utils = require('../utils/utils.jsx');
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
const Modal = ReactBootstrap.Modal;
@@ -25,6 +25,17 @@ export default class ChannelMembersModal extends React.Component {
state.showInviteModal = false;
this.state = state;
}
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(this.props, nextProps)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(this.state, nextState)) {
+ return true;
+ }
+
+ return false;
+ }
getStateFromStores() {
const users = UserStore.getActiveOnlyProfiles();
const memberList = ChannelStore.getCurrentExtraInfo().members;
@@ -58,22 +69,16 @@ export default class ChannelMembersModal extends React.Component {
memberList.sort(compareByUsername);
nonmemberList.sort(compareByUsername);
- const channel = ChannelStore.getCurrent();
- let channelName = '';
- if (channel) {
- channelName = channel.display_name;
- }
-
return {
nonmemberList,
- memberList,
- channelName
+ memberList
};
}
onShow() {
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
+ this.onChange();
}
componentDidUpdate(prevProps) {
if (this.props.show && !prevProps.show) {
@@ -91,7 +96,7 @@ export default class ChannelMembersModal extends React.Component {
}
onChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
}
@@ -130,7 +135,7 @@ export default class ChannelMembersModal extends React.Component {
}
this.setState({memberList, nonmemberList});
- AsyncClient.getChannelExtraInfo(true);
+ AsyncClient.getChannelExtraInfo();
},
(err) => {
this.setState({inviteError: err.message});
@@ -157,7 +162,7 @@ export default class ChannelMembersModal extends React.Component {
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
- <Modal.Title><span className='name'>{this.state.channelName}</span>{' Members'}</Modal.Title>
+ <Modal.Title><span className='name'>{this.props.channel.display_name}</span>{' Members'}</Modal.Title>
<a
className='btn btn-md btn-primary'
href='#'
@@ -193,7 +198,8 @@ export default class ChannelMembersModal extends React.Component {
</Modal>
<ChannelInviteModal
show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
+ onHide={() => this.setState({showInviteModal: false})}
+ channel={this.props.channel}
/>
</div>
);
@@ -206,5 +212,6 @@ ChannelMembersModal.defaultProps = {
ChannelMembersModal.propTypes = {
show: React.PropTypes.bool.isRequired,
- onModalDismissed: React.PropTypes.func.isRequired
+ onModalDismissed: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
};
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications_modal.jsx
index 43700bf36..e70d3a634 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications_modal.jsx
@@ -1,15 +1,15 @@
// Copyright (c) 2015 Mattermost, 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 Modal = ReactBootstrap.Modal;
+import SettingItemMin from './setting_item_min.jsx';
+import SettingItemMax from './setting_item_max.jsx';
-var Utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
+import * as Client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
-export default class ChannelNotifications extends React.Component {
+export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
@@ -23,55 +23,39 @@ export default class ChannelNotifications extends React.Component {
this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
- this.onShow = this.onShow.bind(this);
+ const member = ChannelStore.getMember(props.channel.id);
this.state = {
- notifyLevel: '',
- markUnreadLevel: '',
- title: '',
- channelId: '',
+ notifyLevel: member.notify_props.desktop,
+ markUnreadLevel: member.notify_props.mark_unread,
+ channelId: ChannelStore.getCurrentId(),
activeSection: ''
};
}
- onShow(e) {
- var button = e.relatedTarget;
- var channelId = button.getAttribute('data-channelid');
-
- const member = ChannelStore.getMember(channelId);
- var notifyLevel = member.notify_props.desktop;
- var markUnreadLevel = member.notify_props.mark_unread;
-
- this.setState({
- notifyLevel,
- markUnreadLevel,
- title: button.getAttribute('data-title'),
- channelId
- });
- }
- componentDidMount() {
- ChannelStore.addChangeListener(this.onListenerChange);
-
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- }
- componentWillUnmount() {
- ChannelStore.removeChangeListener(this.onListenerChange);
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.show && nextProps.show) {
+ this.onListenerChange();
+ ChannelStore.addChangeListener(this.onListenerChange);
+ } else {
+ ChannelStore.removeChangeListener(this.onListenerChange);
+ }
}
onListenerChange() {
- if (!this.state.channelId) {
+ const curChannelId = ChannelStore.getCurrentId();
+
+ if (!curChannelId) {
return;
}
- const member = ChannelStore.getMember(this.state.channelId);
- var notifyLevel = member.notify_props.desktop;
- var markUnreadLevel = member.notify_props.mark_unread;
+ const newState = {channelId: curChannelId};
+ const member = ChannelStore.getMember(curChannelId);
- var newState = this.state;
- newState.notifyLevel = notifyLevel;
- newState.markUnreadLevel = markUnreadLevel;
-
- if (!Utils.areStatesEqual(this.state, newState)) {
- this.setState(newState);
+ if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
+ newState.notifyLevel = member.notify_props.desktop;
+ newState.markUnreadLevel = member.notify_props.mark_unread;
}
+
+ this.setState(newState);
}
updateSection(section) {
this.setState({activeSection: section});
@@ -104,7 +88,6 @@ export default class ChannelNotifications extends React.Component {
}
handleUpdateNotifyLevel(notifyLevel) {
this.setState({notifyLevel});
- ReactDOM.findDOMNode(this.refs.modal).focus();
}
createNotifyLevelSection(serverError) {
var handleUpdateSection;
@@ -262,7 +245,6 @@ export default class ChannelNotifications extends React.Component {
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
- ReactDOM.findDOMNode(this.refs.modal).focus();
}
createMarkUnreadLevelSection(serverError) {
@@ -347,48 +329,39 @@ export default class ChannelNotifications extends React.Component {
}
return (
- <div
- className='modal fade'
- id='channel_notifications'
- ref='modal'
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ dialogClassName='settings-modal'
+ onHide={this.props.onHide}
>
- <div className='modal-dialog settings-modal'>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Notification Preferences for '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <div className='settings-table'>
+ <div className='settings-content'>
+ <div
+ ref='wrapper'
+ className='user-settings'
>
- <span aria-hidden='true'>&times;</span>
- <span className='sr-only'>{'Close'}</span>
- </button>
- <h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
- </div>
- <div className='modal-body'>
- <div className='settings-table'>
- <div className='settings-content'>
- <div
- ref='wrapper'
- className='user-settings'
- >
- <br/>
- <div className='divider-dark first'/>
- {this.createNotifyLevelSection(serverError)}
- <div className='divider-light'/>
- {this.createMarkUnreadLevelSection(serverError)}
- <div className='divider-dark'/>
- </div>
+ <br/>
+ <div className='divider-dark first'/>
+ {this.createNotifyLevelSection(serverError)}
+ <div className='divider-light'/>
+ {this.createMarkUnreadLevelSection(serverError)}
+ <div className='divider-dark'/>
</div>
- </div>
- {serverError}
</div>
</div>
- </div>
- </div>
+ {serverError}
+ </Modal.Body>
+ </Modal>
);
}
}
+
+ChannelNotificationsModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
index 3f53a94c2..7cbb638a0 100644
--- a/web/react/components/channel_view.jsx
+++ b/web/react/components/channel_view.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const CenterPanel = require('../components/center_panel.jsx');
-const Sidebar = require('../components/sidebar.jsx');
-const SidebarRight = require('../components/sidebar_right.jsx');
-const SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+import CenterPanel from '../components/center_panel.jsx';
+import Sidebar from '../components/sidebar.jsx';
+import SidebarRight from '../components/sidebar_right.jsx';
+import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
export default class ChannelView extends React.Component {
constructor(props) {
diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx
deleted file mode 100644
index a6d9d5d70..000000000
--- a/web/react/components/command_list.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var client = require('../utils/client.jsx');
-
-export default class CommandList extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
- this.addFirstCommand = this.addFirstCommand.bind(this);
- this.isEmpty = this.isEmpty.bind(this);
- this.getSuggestedCommands = this.getSuggestedCommands.bind(this);
-
- this.state = {
- suggestions: [ ],
- cmd: ''
- };
- }
-
- handleClick(i) {
- this.props.addCommand(this.state.suggestions[i].suggestion);
- this.setState({suggestions: [ ], cmd: ''});
- }
-
- addFirstCommand() {
- if (this.state.suggestions.length === 0) {
- return;
- }
- this.handleClick(0);
- }
-
- isEmpty() {
- return this.state.suggestions.length === 0;
- }
-
- getSuggestedCommands(cmd) {
- if (!cmd || cmd.charAt(0) !== '/') {
- this.setState({suggestions: [ ], cmd: ''});
- return;
- }
-
- client.executeCommand(
- this.props.channelId,
- cmd,
- true,
- function success(data) {
- if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) {
- data.suggestions = [];
- }
- this.setState({suggestions: data.suggestions, cmd: cmd});
- }.bind(this),
- function fail() {
- }
- );
- }
-
- render() {
- if (this.state.suggestions.length === 0) {
- return (<div/>);
- }
-
- var suggestions = [];
-
- for (var i = 0; i < this.state.suggestions.length; i++) {
- if (this.state.suggestions[i].suggestion !== this.state.cmd) {
- suggestions.push(
- <div
- key={i}
- className='command-name'
- onClick={this.handleClick.bind(this, i)}
- >
- <div className='command__title'><strong>{this.state.suggestions[i].suggestion}</strong></div>
- <div className='command__desc'>{this.state.suggestions[i].description}</div>
- </div>
- );
- }
- }
-
- return (
- <div
- ref='mentionlist'
- className='command-box'
- style={{height: (this.state.suggestions.length * 56) + 2}}
- >
- {suggestions}
- </div>
- );
- }
-}
-
-CommandList.defaultProps = {
- channelId: null
-};
-
-CommandList.propTypes = {
- addCommand: React.PropTypes.func,
- channelId: React.PropTypes.string
-};
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 058594165..b0f33eda1 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -1,21 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const PostStore = require('../stores/post_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-const Textbox = require('./textbox.jsx');
-const MsgTyping = require('./msg_typing.jsx');
-const FileUpload = require('./file_upload.jsx');
-const FilePreview = require('./file_preview.jsx');
-const Utils = require('../utils/utils.jsx');
-
-const Constants = require('../utils/constants.jsx');
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import SocketStore from '../stores/socket_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import Textbox from './textbox.jsx';
+import MsgTyping from './msg_typing.jsx';
+import FileUpload from './file_upload.jsx';
+import FilePreview from './file_preview.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
@@ -34,7 +34,6 @@ export default class CreateComment extends React.Component {
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);
this.handleResize = this.handleResize.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
@@ -194,7 +193,8 @@ export default class CreateComment extends React.Component {
title: 'Comment',
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
@@ -334,6 +334,7 @@ export default class CreateComment extends React.Component {
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
+ supportsCommands={false}
id='reply_textbox'
ref='textbox'
/>
@@ -361,11 +362,11 @@ export default class CreateComment extends React.Component {
onClick={this.handleSubmit}
/>
{uploadsInProgressText}
+ {preview}
{postError}
{serverError}
</div>
</div>
- {preview}
</form>
);
}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 5a69c9bfb..89e984e27 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -1,24 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const MsgTyping = require('./msg_typing.jsx');
-const Textbox = require('./textbox.jsx');
-const FileUpload = require('./file_upload.jsx');
-const FilePreview = require('./file_preview.jsx');
-const TutorialTip = require('./tutorial/tutorial_tip.jsx');
-
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const Utils = require('../utils/utils.jsx');
-
-const ChannelStore = require('../stores/channel_store.jsx');
-const PostStore = require('../stores/post_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
-
-const Constants = require('../utils/constants.jsx');
+import MsgTyping from './msg_typing.jsx';
+import Textbox from './textbox.jsx';
+import FileUpload from './file_upload.jsx';
+import FilePreview from './file_preview.jsx';
+import TutorialTip from './tutorial/tutorial_tip.jsx';
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import ChannelStore from '../stores/channel_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import SocketStore from '../stores/socket_store.jsx';
+
+import Constants from '../utils/constants.jsx';
+
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
@@ -176,9 +178,7 @@ export default class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
- PostStore.storePendingPost(post);
- PostStore.storeDraft(channel.id, null);
- PostStore.jumpPostsViewToBottom();
+ EventHelpers.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -190,10 +190,7 @@ export default class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
+ EventHelpers.emitPostRecievedEvent(data);
},
(err) => {
const state = {};
@@ -372,7 +369,8 @@ export default class CreatePost extends React.Component {
title: type,
message: lastPost.message,
postId: lastPost.id,
- channelId: lastPost.channel_id
+ channelId: lastPost.channel_id,
+ comments: PostStore.getCommentCount(lastPost)
});
}
}
@@ -382,8 +380,8 @@ export default class CreatePost extends React.Component {
screens.push(
<div>
<h4>{'Sending Messages'}</h4>
- <p>{'Type here to write a message.'}</p>
- <p>{'Click the attachment button to upload an image or a file.'}</p>
+ <p>{'Type here to write a message and press '}<strong>{'Enter'}</strong>{' to post it.'}</p>
+ <p>{'Click the '}<strong>{'Attachment'}</strong>{' button to upload an image or a file.'}</p>
</div>
);
@@ -472,13 +470,13 @@ export default class CreatePost extends React.Component {
{tutorialTip}
</div>
<div className={postFooterClassName}>
- {postError}
- {serverError}
- {preview}
<MsgTyping
channelId={this.state.channelId}
parentId=''
/>
+ {preview}
+ {postError}
+ {serverError}
</div>
</div>
</form>
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index b7d633b38..1255067fd 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -1,102 +1,74 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+const Modal = ReactBootstrap.Modal;
+import TeamStore from '../stores/team_store.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
- this.onShow = this.onShow.bind(this);
-
- this.state = {
- title: '',
- channelId: ''
- };
}
+
handleDelete() {
- if (this.state.channelId.length !== 26) {
+ if (this.props.channel.id.length !== 26) {
return;
}
- Client.deleteChannel(this.state.channelId,
- function handleDeleteSuccess() {
+ Client.deleteChannel(
+ this.props.channel.id,
+ () => {
AsyncClient.getChannels(true);
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
- function handleDeleteError(err) {
+ (err) => {
AsyncClient.dispatchError(err, 'handleDelete');
}
);
}
- onShow(e) {
- var button = $(e.relatedTarget);
- this.setState({
- title: button.attr('data-title'),
- channelId: button.attr('data-channelid')
- });
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- }
+
render() {
- const channel = ChannelStore.getCurrent();
- let channelType = 'channel';
- if (channel && channel.type === 'P') {
- channelType = 'private group';
- }
+ const channelTerm = Utils.getChannelTerm(this.props.channel.type).toLowerCase();
return (
- <div
- className='modal fade'
- ref='modal'
- id='delete_channel'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onHide}
>
- <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'>&times;</span>
- </button>
- <h4 className='modal-title'>Confirm DELETE Channel</h4>
- </div>
- <div className='modal-body'>
- <p>
- Are you sure you wish to delete the {this.state.title} {channelType}?
- </p>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Cancel
- </button>
- <button
- type='button'
- className='btn btn-danger'
- data-dismiss='modal'
- onClick={this.handleDelete}
- >
- Delete
- </button>
- </div>
- </div>
- </div>
- </div>
+ <Modal.Header closeButton={true}>
+ <h4 className='modal-title'>{'Confirm DELETE Channel'}</h4>
+ </Modal.Header>
+ <Modal.Body>
+ {`Are you sure you wish to delete the ${this.props.channel.display_name} ${channelTerm}?`}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-danger'
+ data-dismiss='modal'
+ onClick={this.handleDelete}
+ >
+ {'Delete'}
+ </button>
+ </Modal.Footer>
+ </Modal>
);
}
}
+
+DeleteChannelModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 3a3dabce5..827654e1b 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -1,13 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var PostStore = require('../stores/post_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var Utils = require('../utils/utils.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
+import * as Client from '../utils/client.jsx';
+import PostStore from '../stores/post_store.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+var Modal = ReactBootstrap.Modal;
+import * as Utils from '../utils/utils.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
var ActionTypes = Constants.ActionTypes;
export default class DeletePostModal extends React.Component {
@@ -15,18 +16,40 @@ export default class DeletePostModal extends React.Component {
super(props);
this.handleDelete = this.handleDelete.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.handleHide = this.handleHide.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
- this.onShow = this.onShow.bind(this);
- this.state = {title: '', postId: '', channelId: '', selectedList: PostStore.getSelectedPost(), comments: 0};
+ this.selectedList = null;
+
+ this.state = {
+ show: true,
+ post: null,
+ commentCount: 0,
+ error: ''
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
+ PostStore.addSelectedPostChangeListener(this.onListenerChange);
}
+
+ componentWillUnmount() {
+ PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
+ }
+
handleDelete() {
- Client.deletePost(this.state.channelId, this.state.postId,
- function deleteSuccess() {
- var selectedList = this.state.selectedList;
+ Client.deletePost(
+ this.state.post.channel_id,
+ this.state.post.id,
+ () => {
+ var selectedList = this.selectedList;
+
if (selectedList && selectedList.order && selectedList.order.length > 0) {
var selectedPost = selectedList.posts[selectedList.order[0]];
- if ((selectedPost.id === this.state.postId && this.state.title === 'Post') || selectedPost.root_id === this.state.postId) {
+ if ((selectedPost.id === this.state.post.id && !this.state.root_id) || selectedPost.root_id === this.state.post.id) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SEARCH,
results: null
@@ -36,7 +59,7 @@ export default class DeletePostModal extends React.Component {
type: ActionTypes.RECIEVED_POST_SELECTED,
results: null
});
- } else if (selectedPost.id === this.state.postId && this.state.title === 'Comment') {
+ } else if (selectedPost.id === this.state.post.id && this.state.root_id) {
if (selectedPost.root_id && selectedPost.root_id.length > 0 && selectedList.posts[selectedPost.root_id]) {
selectedList.order = [selectedPost.root_id];
delete selectedList.posts[selectedPost.id];
@@ -53,98 +76,88 @@ export default class DeletePostModal extends React.Component {
}
}
}
- PostStore.removePost(this.state.postId, this.state.channelId);
- AsyncClient.getPosts(this.state.channelId);
- }.bind(this),
- function deleteFailed(err) {
+
+ PostStore.removePost(this.state.post.id, this.state.post.channel_id);
+ AsyncClient.getPosts(this.state.post.channel_id);
+ },
+ (err) => {
AsyncClient.dispatchError(err, 'deletePost');
}
);
+
+ this.handleHide();
}
- onShow(e) {
- var newState = {};
- if (BrowserStore.getItem('edit_state_transfer')) {
- newState = BrowserStore.getItem('edit_state_transfer');
- BrowserStore.removeItem('edit_state_transfer');
- } else {
- var button = e.relatedTarget;
- newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')};
- }
- this.setState(newState);
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- PostStore.addSelectedPostChangeListener(this.onListenerChange);
+
+ handleToggle(value, args) {
+ this.setState({
+ show: value,
+ post: args.post,
+ commentCount: args.commentCount,
+ error: ''
+ });
}
- componentWillUnmount() {
- PostStore.removeSelectedPostChangeListener(this.onListenerChange);
+
+ handleHide() {
+ this.setState({show: false});
}
+
onListenerChange() {
var newList = PostStore.getSelectedPost();
- if (!Utils.areStatesEqual(this.state.selectedList, newList)) {
- this.setState({selectedList: newList});
+ if (!Utils.areObjectsEqual(this.selectedList, newList)) {
+ this.selectedList = newList;
}
}
+
render() {
+ if (!this.state.post) {
+ return null;
+ }
+
var error = null;
if (this.state.error) {
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
}
var commentWarning = '';
- if (this.state.comments > 0) {
- commentWarning = 'This post has ' + this.state.comments + ' comment(s) on it.';
+ if (this.state.commentCount > 0) {
+ commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
}
+ const postTerm = Utils.getPostTerm(this.state.post);
+
return (
- <div
- className='modal fade'
- id='delete_post'
- ref='modal'
- role='dialog'
- tabIndex='-1'
- aria-hidden='true'
+ <Modal
+ show={this.state.show}
+ onHide={this.handleHide}
>
- <div className='modal-dialog modal-push-down'>
- <div className='modal-content'>
- <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'>Confirm {this.state.title} Delete</h4>
- </div>
- <div className='modal-body'>
- Are you sure you want to delete the {this.state.title.toLowerCase()}?
- <br/>
- <br/>
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{`Confirm ${postTerm} Delete`}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ {`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
+ <br />
+ <br />
{commentWarning}
- </div>
- {error}
- <div className='modal-footer'>
+ {error}
+ </Modal.Body>
+ <Modal.Footer>
<button
type='button'
className='btn btn-default'
- data-dismiss='modal'
+ onClick={this.handleHide}
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-danger'
- data-dismiss='modal'
onClick={this.handleDelete}
+ autoFocus='autofocus'
>
- Delete
+ {'Delete'}
</button>
- </div>
- </div>
- </div>
- </div>
+ </Modal.Footer>
+ </Modal>
);
}
}
diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx
new file mode 100644
index 000000000..188ca340b
--- /dev/null
+++ b/web/react/components/docs.jsx
@@ -0,0 +1,41 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import UserStore from '../stores/user_store.jsx';
+
+export default class Docs extends React.Component {
+ constructor(props) {
+ super(props);
+ UserStore.setCurrentUser(global.window.mm_user || {});
+
+ this.state = {text: ''};
+ const errorState = {text: '## 404'};
+
+ if (props.site) {
+ $.get('/static/help/' + props.site + '.md').then((response) => {
+ this.setState({text: response});
+ }, () => {
+ this.setState(errorState);
+ });
+ } else {
+ this.setState(errorState);
+ }
+ }
+
+ render() {
+ return (
+ <div
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
+ >
+ </div>
+ );
+ }
+}
+
+Docs.defaultProps = {
+ site: ''
+};
+Docs.propTypes = {
+ site: React.PropTypes.string
+};
diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx
new file mode 100644
index 000000000..209e30fcc
--- /dev/null
+++ b/web/react/components/edit_channel_header_modal.jsx
@@ -0,0 +1,126 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelHeaderModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleEdit = this.handleEdit.bind(this);
+
+ this.onShow = this.onShow.bind(this);
+ this.onHide = this.onHide.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
+
+ handleEdit() {
+ var data = {};
+ data.channel_id = this.props.channel.id;
+
+ if (data.channel_id.length !== 26) {
+ return;
+ }
+
+ data.channel_header = ReactDOM.findDOMNode(this.refs.textarea).value;
+
+ Client.updateChannelHeader(data,
+ () => {
+ this.setState({serverError: ''});
+ AsyncClient.getChannel(this.props.channel.id);
+ this.onHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ onShow() {
+ const textarea = ReactDOM.findDOMNode(this.refs.textarea);
+ Utils.placeCaretAtEnd(textarea);
+ }
+
+ onHide() {
+ this.setState({
+ serverError: ''
+ });
+
+ this.props.onHide();
+ }
+
+ render() {
+ var serverError = null;
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Edit Header for ' + this.props.channel.display_name}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
+ <textarea
+ ref='textarea'
+ className='form-control no-resize'
+ rows='6'
+ id='edit_header'
+ maxLength='1024'
+ defaultValue={this.props.channel.header}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleEdit}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelHeaderModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
deleted file mode 100644
index 2557a55ca..000000000
--- a/web/react/components/edit_channel_modal.jsx
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-
-export default class EditChannelModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleEdit = this.handleEdit.bind(this);
- this.handleUserInput = this.handleUserInput.bind(this);
- this.handleClose = this.handleClose.bind(this);
- this.onShow = this.onShow.bind(this);
- this.handleShown = this.handleShown.bind(this);
-
- this.state = {
- header: '',
- title: '',
- channelId: '',
- serverError: ''
- };
- }
- handleEdit() {
- var data = {};
- data.channel_id = this.state.channelId;
-
- if (data.channel_id.length !== 26) {
- return;
- }
-
- data.channel_header = this.state.header.trim();
-
- Client.updateChannelHeader(data,
- () => {
- this.setState({serverError: ''});
- AsyncClient.getChannel(this.state.channelId);
- $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- },
- (err) => {
- if (err.message === 'Invalid channel_header parameter') {
- this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
- } else {
- this.setState({serverError: err.message});
- }
- }
- );
- }
- handleUserInput(e) {
- this.setState({header: e.target.value});
- }
- handleClose() {
- this.setState({header: '', serverError: ''});
- }
- onShow(e) {
- const button = e.relatedTarget;
- this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
- }
- handleShown() {
- $('#edit_channel #edit_header').focus();
- }
- componentDidMount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown);
- }
- componentWillUnmount() {
- $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
- }
- render() {
- var serverError = null;
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header'}
- </h4>
- );
- if (this.state.title) {
- editTitle = (
- <h4
- className='modal-title'
- ref='title'
- >
- {'Edit Header for '}<span className='name'>{this.state.title}</span>
- </h4>
- );
- }
-
- return (
- <div
- className='modal fade'
- ref='modal'
- id='edit_channel'
- role='dialog'
- tabIndex='-1'
- 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>
- {editTitle}
- </div>
- <div className='modal-body'>
- <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
- <textarea
- className='form-control no-resize'
- rows='6'
- id='edit_header'
- maxLength='1024'
- value={this.state.header}
- onChange={this.handleUserInput}
- />
- {serverError}
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Cancel'}
- </button>
- <button
- type='button'
- className='btn btn-primary'
- onClick={this.handleEdit}
- >
- {'Save'}
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
index 65e8183de..af23342ae 100644
--- a/web/react/components/edit_channel_purpose_modal.jsx
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.jsx');
-const Client = require('../utils/client.jsx');
-const Utils = require('../utils/utils.jsx');
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
const Modal = ReactBootstrap.Modal;
@@ -90,7 +90,7 @@ export default class EditChannelPurposeModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used.`}</p>
+ <p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.`}</p>
<textarea
ref='purpose'
className='form-control no-resize'
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index ef32baa7d..be57fe7c3 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -1,14 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var Textbox = require('./textbox.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
-var PreferenceStore = require('../stores/preference_store.jsx');
-
-var Constants = require('../utils/constants.jsx');
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import Textbox from './textbox.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+
+import Constants from '../utils/constants.jsx';
var KeyCodes = Constants.KeyCodes;
export default class EditPostModal extends React.Component {
@@ -34,7 +35,7 @@ export default class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
- $('#delete_post').modal('show');
+ EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}
@@ -159,6 +160,7 @@ export default class EditPostModal extends React.Component {
onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
+ supportsCommands={false}
id='edit_textbox'
ref='editbox'
/>
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index f098384aa..921e8afe1 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ErrorStore = require('../stores/error_store.jsx');
+import ErrorStore from '../stores/error_store.jsx';
export default class ErrorBar extends React.Component {
constructor() {
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index e707e32f5..2474b3d8a 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
-var Constants = require('../utils/constants.jsx');
+import * as utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
export default class FileAttachment extends React.Component {
constructor(props) {
@@ -67,7 +67,7 @@ export default class FileAttachment extends React.Component {
this.canSetState = false;
}
shouldComponentUpdate(nextProps, nextState) {
- if (!utils.areStatesEqual(nextProps, this.props)) {
+ if (!utils.areObjectsEqual(nextProps, this.props)) {
return true;
}
diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx
index ae08e5635..da1b2ba3d 100644
--- a/web/react/components/file_attachment_list.jsx
+++ b/web/react/components/file_attachment_list.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ViewImageModal = require('./view_image.jsx');
-var FileAttachment = require('./file_attachment.jsx');
-var Constants = require('../utils/constants.jsx');
+import ViewImageModal from './view_image.jsx';
+import FileAttachment from './file_attachment.jsx';
+import Constants from '../utils/constants.jsx';
export default class FileAttachmentList extends React.Component {
constructor(props) {
diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx
index df5deb8bc..d625a811e 100644
--- a/web/react/components/file_preview.jsx
+++ b/web/react/components/file_preview.jsx
@@ -1,16 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
export default class FilePreview extends React.Component {
constructor(props) {
super(props);
this.handleRemove = this.handleRemove.bind(this);
+ }
- this.state = {};
+ componentDidUpdate() {
+ if (this.props.uploadsInProgress.length > 0) {
+ ReactDOM.findDOMNode(this.refs[this.props.uploadsInProgress[0]]).scrollIntoView();
+ }
}
+
handleRemove(e) {
var previewDiv = e.target.parentNode.parentNode;
@@ -20,9 +25,10 @@ export default class FilePreview extends React.Component {
this.props.onRemove(previewDiv.getAttribute('data-client-id'));
}
}
+
render() {
var previews = [];
- this.props.files.forEach(function setupPreview(fullFilename) {
+ this.props.files.forEach((fullFilename) => {
var filename = fullFilename;
var originalFilename = filename;
var filenameSplit = filename.split('.');
@@ -72,11 +78,12 @@ export default class FilePreview extends React.Component {
</div>
);
}
- }.bind(this));
+ });
- this.props.uploadsInProgress.forEach(function addUploadsInProgress(clientId) {
+ this.props.uploadsInProgress.forEach((clientId) => {
previews.push(
<div
+ ref={clientId}
key={clientId}
className='preview-div'
data-client-id={clientId}
@@ -93,7 +100,7 @@ export default class FilePreview extends React.Component {
</a>
</div>
);
- }.bind(this));
+ });
return (
<div className='preview-container'>
@@ -104,8 +111,8 @@ export default class FilePreview extends React.Component {
}
FilePreview.defaultProps = {
- files: null,
- uploadsInProgress: null
+ files: [],
+ uploadsInProgress: []
};
FilePreview.propTypes = {
onRemove: React.PropTypes.func.isRequired,
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 8854a54df..9316ca9a5 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var client = require('../utils/client.jsx');
-var Constants = require('../utils/constants.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var utils = require('../utils/utils.jsx');
+import * as client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as utils from '../utils/utils.jsx';
export default class FileUpload extends React.Component {
constructor(props) {
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index bd3c11973..94ca48dbf 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
+import * as utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
export default class FindTeam extends React.Component {
constructor(props) {
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 8839bc3c7..fd20834f4 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -1,32 +1,28 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
+const Modal = ReactBootstrap.Modal;
export default class GetLinkModal extends React.Component {
constructor(props) {
super(props);
- this.handleClick = this.handleClick.bind(this);
- this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
- this.state = {copiedLink: false};
- }
- onShow(e) {
- var button = e.relatedTarget;
- this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
+ this.copyLink = this.copyLink.bind(this);
+
+ this.state = {
+ copiedLink: false
+ };
}
+
onHide() {
this.setState({copiedLink: false});
+
+ this.props.onHide();
}
- componentDidMount() {
- if (this.refs.modal) {
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide);
- }
- }
- handleClick() {
+
+ copyLink() {
var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea));
copyTextarea.select();
@@ -41,8 +37,18 @@ export default class GetLinkModal extends React.Component {
this.setState({copiedLink: false});
}
}
+
render() {
- var currentUser = UserStore.getCurrentUser();
+ let helpText = null;
+ if (this.props.helpText) {
+ helpText = (
+ <p>
+ {this.props.helpText}
+ <br />
+ <br />
+ </p>
+ );
+ }
let copyLink = null;
if (document.queryCommandSupported('copy')) {
@@ -51,75 +57,59 @@ export default class GetLinkModal extends React.Component {
data-copy-btn='true'
type='button'
className='btn btn-primary pull-left'
- onClick={this.handleClick}
- data-clipboard-text={this.state.value}
+ onClick={this.copyLink}
>
- Copy Link
+ {'Copy Link'}
</button>
);
}
var copyLinkConfirm = null;
if (this.state.copiedLink) {
- copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>;
+ copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i>{' Link copied to clipboard.'}</p>;
}
- if (currentUser != null) {
- return (
- <div
- className='modal fade'
- ref='modal'
- id='get_link'
- 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'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >
- {this.state.title} Link
- </h4>
- </div>
- <div className='modal-body'>
- <p>
- Send teammates the link below for them to sign-up to this team site.
- <br /><br />
- </p>
- <textarea
- className='form-control no-resize min-height'
- readOnly='true'
- ref='textarea'
- value={this.state.value}
- />
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- Close
- </button>
- {copyLink}
- {copyLinkConfirm}
- </div>
- </div>
- </div>
- </div>
- );
- }
- return <div/>;
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ <h4 className='modal-title'>{this.props.title}</h4>
+ </Modal.Header>
+ <Modal.Body>
+ {helpText}
+ <textarea
+ className='form-control no-resize min-height'
+ readOnly='true'
+ ref='textarea'
+ value={this.props.link}
+ />
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.onHide}
+ >
+ {'Close'}
+ </button>
+ {copyLink}
+ {copyLinkConfirm}
+ </Modal.Footer>
+ </Modal>
+ );
}
}
+
+GetLinkModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired,
+ title: React.PropTypes.string.isRequired,
+ helpText: React.PropTypes.string,
+ link: React.PropTypes.string.isRequired
+};
+
+GetLinkModal.defaultProps = {
+ helpText: null
+};
diff --git a/web/react/components/get_team_invite_link_modal.jsx b/web/react/components/get_team_invite_link_modal.jsx
new file mode 100644
index 000000000..a926c4451
--- /dev/null
+++ b/web/react/components/get_team_invite_link_modal.jsx
@@ -0,0 +1,45 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../utils/constants.jsx';
+import GetLinkModal from './get_link_modal.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+
+export default class GetTeamInviteLinkModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleToggle = this.handleToggle.bind(this);
+
+ this.state = {
+ show: false
+ };
+ }
+
+ componentDidMount() {
+ ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ componentWillUnmount() {
+ ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
+ }
+
+ handleToggle(value) {
+ this.setState({
+ show: value
+ });
+ }
+
+ render() {
+ return (
+ <GetLinkModal
+ show={this.state.show}
+ onHide={() => this.setState({show: false})}
+ title='Team Invite Link'
+ helpText='Send teammates the link below for them to sign-up to this team site.'
+ link={TeamStore.getCurrentInviteLink()}
+ />
+ );
+ }
+}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index c09477a69..649ec7321 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -1,14 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var ActionTypes = require('../utils/constants.jsx').ActionTypes;
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Client = require('../utils/client.jsx');
-var ModalStore = require('../stores/modal_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
-var ConfirmModal = require('./confirm_modal.jsx');
+import * as utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+import * as Client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import ConfirmModal from './confirm_modal.jsx';
const Modal = ReactBootstrap.Modal;
@@ -22,6 +23,7 @@ export default class InviteMemberModal extends React.Component {
this.addInviteFields = this.addInviteFields.bind(this);
this.clearFields = this.clearFields.bind(this);
this.removeInviteFields = this.removeInviteFields.bind(this);
+ this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
this.state = {
show: false,
@@ -31,7 +33,8 @@ export default class InviteMemberModal extends React.Component {
firstNameErrors: {},
lastNameErrors: {},
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true',
- showConfirmModal: false
+ showConfirmModal: false,
+ isSendingEmails: false
};
}
@@ -89,10 +92,13 @@ export default class InviteMemberModal extends React.Component {
var data = {};
data.invites = invites;
+ this.setState({isSendingEmails: true});
+
Client.inviteMembers(
data,
() => {
this.handleHide(false);
+ this.setState({isSendingEmails: false});
},
(err) => {
if (err.message === 'This person is already on your team') {
@@ -101,6 +107,8 @@ export default class InviteMemberModal extends React.Component {
} else {
this.setState({serverError: err.message});
}
+
+ this.setState({isSendingEmails: false});
}
);
}
@@ -135,7 +143,7 @@ export default class InviteMemberModal extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (!prevState.show && this.state.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
}
@@ -181,6 +189,12 @@ export default class InviteMemberModal extends React.Component {
this.setState({inviteIds: inviteIds, idCount: count});
}
+ showGetTeamInviteLinkModal() {
+ this.handleHide(false);
+
+ EventHelpers.showGetTeamInviteLinkModal();
+ }
+
render() {
var currentUser = UserStore.getCurrentUser();
@@ -289,11 +303,6 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
- var sendButtonLabel = 'Send Invitation';
- if (this.state.inviteIds.length > 1) {
- sendButtonLabel = 'Send Invitations';
- }
-
if (this.state.emailEnabled) {
content = (
<div>
@@ -309,33 +318,40 @@ export default class InviteMemberModal extends React.Component {
</div>
);
- sendButton =
- (
- <button
- onClick={this.handleSubmit}
- type='button'
- className='btn btn-primary'
- >{sendButtonLabel}</button>
+ var sendButtonLabel = 'Send Invitation';
+ if (this.state.isSendingEmails) {
+ sendButtonLabel = (
+ <span><i className='fa fa-spinner fa-spin' />{' Sending'}</span>
);
+ } else if (this.state.inviteIds.length > 1) {
+ sendButtonLabel = 'Send Invitations';
+ }
+
+ sendButton = (
+ <button
+ onClick={this.handleSubmit}
+ type='button'
+ className='btn btn-primary'
+ disabled={this.state.isSendingEmails}
+ >
+ {sendButtonLabel}
+ </button>
+ );
} else {
var teamInviteLink = null;
if (currentUser && TeamStore.getCurrent().type === 'O') {
- var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id;
- var link =
- (
- <a
- href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={linkUrl}
- onClick={() => this.handleHide(this, false)}
- >Team Invite Link</a>
+ var link = (
+ <a
+ href='#'
+ onClick={this.showGetTeamInviteLinkModal}
+ >
+ {'Team Invite Link'}
+ </a>
);
teamInviteLink = (
<p>
- You can also invite people using the {link}.
+ {'You can also invite people using the '}{link}{'.'}
</p>
);
}
@@ -351,12 +367,13 @@ export default class InviteMemberModal extends React.Component {
return (
<div>
<Modal
- className='modal-invite-member'
+ dialogClassName='modal-invite-member'
show={this.state.show}
onHide={this.handleHide.bind(this, true)}
enforceFocus={!this.state.showConfirmModal}
+ backdrop={this.state.isSendingEmails ? 'static' : true}
>
- <Modal.Header closeButton={true}>
+ <Modal.Header closeButton={!this.state.isSendingEmails}>
<Modal.Title>{'Invite New Member'}</Modal.Title>
</Modal.Header>
<Modal.Body ref='modalBody'>
@@ -370,6 +387,7 @@ export default class InviteMemberModal extends React.Component {
type='button'
className='btn btn-default'
onClick={this.handleHide.bind(this, true)}
+ disabled={this.state.isSendingEmails}
>
{'Cancel'}
</button>
@@ -390,13 +408,6 @@ export default class InviteMemberModal extends React.Component {
return null;
}
-
- static show() {
- AppDispatcher.handleViewAction({
- type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL,
- value: true
- });
- }
}
InviteMemberModal.propTypes = {
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 2b9ce67ca..d87bd20ad 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
export default class Login extends React.Component {
constructor(props) {
@@ -125,7 +125,7 @@ export default class Login extends React.Component {
let emailSignup;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
- <div>
+ <div className='signup__email-container'>
<div className={'form-group' + errorClass}>
<input
autoFocus={focusEmail}
@@ -201,14 +201,12 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableTeamCreation === 'true') {
teamSignUp = (
<div className='margin--extra'>
- <span>{'Want to create your own team? '}
- <a
- href='/'
- className='signup-team-login'
- >
- {'Sign up now'}
- </a>
- </span>
+ <a
+ href='/'
+ className='signup-team-login'
+ >
+ {'Create a new team'}
+ </a>
</div>
);
}
@@ -227,7 +225,7 @@ export default class Login extends React.Component {
{emailSignup}
{userSignUp}
<div className='form-group margin--extra form-group--small'>
- <span><a href='/find_team'>{'Find other teams'}</a></span>
+ <span><a href='/find_team'>{'Find your other teams'}</a></span>
</div>
{forgotPassword}
{teamSignUp}
diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx
index 0238c7920..8c6dc4209 100644
--- a/web/react/components/member_list.jsx
+++ b/web/react/components/member_list.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var MemberListItem = require('./member_list_item.jsx');
+import MemberListItem from './member_list_item.jsx';
export default class MemberList extends React.Component {
constructor(props) {
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 8251d67bc..a7273f280 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-const Utils = require('../utils/utils.jsx');
+import UserStore from '../stores/user_store.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class MemberListItem extends React.Component {
constructor(props) {
@@ -31,9 +31,7 @@ export default class MemberListItem extends React.Component {
var timestamp = UserStore.getCurrentUser().update_at;
var invite;
- if (member.invited && this.props.handleInvite) {
- invite = <span className='member-role'>Added</span>;
- } else if (this.props.handleInvite) {
+ if (this.props.handleInvite) {
invite = (
<a
onClick={this.handleInvite}
@@ -80,17 +78,15 @@ export default class MemberListItem extends React.Component {
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
+ <span className='fa fa-pencil'></span>
<span className='text-capitalize'>{member.roles || 'Member'} </span>
- <span className='caret'></span>
</a>
<ul
className='dropdown-menu member-menu'
role='menu'
- aria-labelledby='channel_header_dropdown'
>
{makeAdminOption}
{handleRemoveOption}
@@ -98,7 +94,7 @@ export default class MemberListItem extends React.Component {
</div>
);
} else {
- invite = <div className='member-role text-capitalize'>{member.roles || 'Member'}<span className='caret hidden'></span></div>;
+ invite = <div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || 'Member'}</div>;
}
return (
@@ -110,7 +106,7 @@ export default class MemberListItem extends React.Component {
height='36'
width='36'
/>
- <div className='member-name'>{member.username}</div>
+ <div className='member-name'>{Utils.displayUsername(member.id)}</div>
<div className='member-description'>{member.email}</div>
</td>
<td className='td--action lg'>{invite}</td>
diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx
index cb2d0660b..f1c31131f 100644
--- a/web/react/components/member_list_team.jsx
+++ b/web/react/components/member_list_team.jsx
@@ -1,18 +1,57 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const MemberListTeamItem = require('./member_list_team_item.jsx');
+import MemberListTeamItem from './member_list_team_item.jsx';
+import UserStore from '../stores/user_store.jsx';
export default class MemberListTeam extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getUsers = this.getUsers.bind(this);
+ this.onChange = this.onChange.bind(this);
+
+ this.state = {
+ users: this.getUsers()
+ };
+ }
+
+ componentDidMount() {
+ UserStore.addChangeListener(this.onChange);
+ }
+
+ componentWillUnmount() {
+ UserStore.removeChangeListener(this.onChange);
+ }
+
+ getUsers() {
+ const profiles = UserStore.getProfiles();
+ const users = [];
+
+ for (const id of Object.keys(profiles)) {
+ users.push(profiles[id]);
+ }
+
+ users.sort((a, b) => a.username.localeCompare(b.username));
+
+ return users;
+ }
+
+ onChange() {
+ this.setState({
+ users: this.getUsers()
+ });
+ }
+
render() {
- const memberList = this.props.users.map(function makeListItem(user) {
+ const memberList = this.state.users.map((user) => {
return (
<MemberListTeamItem
key={user.id}
user={user}
/>
);
- }, this);
+ });
return (
<table className='table more-table member-list-holder'>
@@ -23,7 +62,3 @@ export default class MemberListTeam extends React.Component {
);
}
}
-
-MemberListTeam.propTypes = {
- users: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
-};
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 1fa369068..7967c410d 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const UserStore = require('../stores/user_store.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const Utils = require('../utils/utils.jsx');
+import UserStore from '../stores/user_store.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class MemberListTeamItem extends React.Component {
constructor(props) {
@@ -174,24 +174,22 @@ export default class MemberListTeamItem extends React.Component {
height='36'
width='36'
/>
- <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-name'>{Utils.displayUsername(user.id)}</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 className='fa fa-pencil'></span>
<span>{currentRoles} </span>
- <span className='caret'></span>
</a>
<ul
className='dropdown-menu member-menu'
role='menu'
- aria-labelledby='channel_header_dropdown'
>
{makeAdmin}
{makeMember}
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx
deleted file mode 100644
index 050887c6f..000000000
--- a/web/react/components/mention.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-const Utils = require('../utils/utils.jsx');
-
-export default class Mention extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
-
- this.state = null;
- }
- handleClick() {
- this.props.handleClick(this.props.username);
- }
- render() {
- var icon;
- var timestamp = UserStore.getCurrentUser().update_at;
- if (this.props.id === 'allmention' || this.props.id === 'channelmention') {
- icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
- } else if (this.props.id == null) {
- icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>;
- } else {
- icon = (
- <span>
- <img
- className='mention-img'
- src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- />
- </span>
- );
- }
- return (
- <div
- className={'mentions-name ' + this.props.isFocused}
- id={this.props.id + '_mentions'}
- onClick={this.handleClick}
- onMouseEnter={this.props.handleMouseEnter}
- >
- <div className='pull-left'>{icon}</div>
- <div className='pull-left mention-align'><span>@{this.props.username}</span><span className='mention-fullname'>{this.props.secondary_text}</span></div>
- </div>
- );
- }
-}
-
-Mention.defaultProps = {
- username: '',
- id: '',
- isFocused: '',
- secondary_text: ''
-};
-Mention.propTypes = {
- handleClick: React.PropTypes.func.isRequired,
- handleMouseEnter: React.PropTypes.func.isRequired,
- username: React.PropTypes.string,
- id: React.PropTypes.string,
- isFocused: React.PropTypes.string,
- secondary_text: React.PropTypes.string
-};
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
deleted file mode 100644
index 61a24c09c..000000000
--- a/web/react/components/mention_list.jsx
+++ /dev/null
@@ -1,276 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var UserStore = require('../stores/user_store.jsx');
-var SearchStore = require('../stores/search_store.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Mention = require('./mention.jsx');
-
-var Constants = require('../utils/constants.jsx');
-var Utils = require('../utils/utils.jsx');
-var ActionTypes = Constants.ActionTypes;
-
-var MAX_HEIGHT_LIST = 292;
-var MAX_ITEMS_IN_LIST = 25;
-var ITEM_HEIGHT = 36;
-
-export default class MentionList extends React.Component {
- constructor(props) {
- super(props);
-
- this.onListenerChange = this.onListenerChange.bind(this);
- this.handleClick = this.handleClick.bind(this);
- this.handleMouseEnter = this.handleMouseEnter.bind(this);
- this.getSelection = this.getSelection.bind(this);
- this.addCurrentMention = this.addCurrentMention.bind(this);
- this.addFirstMention = this.addFirstMention.bind(this);
- this.isEmpty = this.isEmpty.bind(this);
- this.scrollToMention = this.scrollToMention.bind(this);
- this.onScroll = this.onScroll.bind(this);
- this.onMentionListKey = this.onMentionListKey.bind(this);
- this.onClick = this.onClick.bind(this);
-
- this.state = {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''};
- }
- onScroll() {
- if ($('.mentions--top').length) {
- $('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top});
- }
- }
- onMentionListKey(e) {
- if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) {
- e.stopPropagation();
- e.preventDefault();
- this.addCurrentMention();
- } else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) {
- e.stopPropagation();
- e.preventDefault();
-
- if (e.which === 38) {
- if (this.getSelection(this.state.selectedMention - 1)) {
- this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username});
- }
- } else if (e.which === 40) {
- if (this.getSelection(this.state.selectedMention + 1)) {
- this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username});
- }
- }
-
- this.scrollToMention(e.which);
- }
- }
- onClick(e) {
- if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length ||
- ('mentionlist' in this.refs && $(ReactDOM.findDOMNode(this.refs.mentionlist)).has(e.target).length))) {
- this.setState({mentionText: '-1'});
- }
- }
- componentDidMount() {
- SearchStore.addMentionDataChangeListener(this.onListenerChange);
-
- $('.post-right__scroll').scroll(this.onScroll);
-
- $('body').on('keydown.mentionlist', '#' + this.props.id, this.onMentionListKey);
- $(document).click(this.onClick);
- }
- componentWillUnmount() {
- SearchStore.removeMentionDataChangeListener(this.onListenerChange);
- $('body').off('keydown.mentionlist', '#' + this.props.id);
- }
-
- /*
- * This component is poorly designed, nessesitating some state modification
- * in the componentDidUpdate function. This is generally discouraged as it
- * is a performance issue and breaks with good react design. This component
- * should be redesigned.
- */
- componentDidUpdate() {
- if (this.state.mentionText !== '-1') {
- if (this.state.selectedUsername !== '' && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) {
- var tempSelectedMention = -1;
- var foundMatch = false;
- while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) {
- if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) {
- this.setState({selectedMention: tempSelectedMention}); //eslint-disable-line react/no-did-update-set-state
- foundMatch = true;
- break;
- }
- }
- if (this.getSelection(0) && !foundMatch) {
- this.setState({selectedMention: 0, selectedUsername: this.refs.mention0.props.username}); //eslint-disable-line react/no-did-update-set-state
- }
- }
- } else if (this.state.selectedMention !== 0) {
- this.setState({selectedMention: 0, selectedUsername: ''}); //eslint-disable-line react/no-did-update-set-state
- }
- }
- onListenerChange(id, mentionText) {
- if (id !== this.props.id) {
- return;
- }
-
- var newState = this.state;
- if (mentionText != null) {
- newState.mentionText = mentionText;
- }
-
- this.setState(newState);
- }
- handleClick(name) {
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECIEVED_ADD_MENTION,
- id: this.props.id,
- username: name
- });
-
- this.setState({mentionText: '-1'});
- }
- handleMouseEnter(listId) {
- this.setState({selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username});
- }
- getSelection(listId) {
- if (!this.refs['mention' + listId]) {
- return false;
- }
- return true;
- }
- addCurrentMention() {
- if (this.getSelection(this.state.selectedMention)) {
- this.refs['mention' + this.state.selectedMention].handleClick();
- } else {
- this.addFirstMention();
- }
- }
- addFirstMention() {
- if (!this.refs.mention0) {
- return;
- }
- this.refs.mention0.handleClick();
- }
- isEmpty() {
- return (!this.refs.mention0);
- }
- scrollToMention(keyPressed) {
- var direction;
- if (keyPressed === 38) {
- direction = 'up';
- } else {
- direction = 'down';
- }
- var scrollAmount = 0;
-
- if (direction === 'up') {
- scrollAmount = '-=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
- } else if (direction === 'down') {
- scrollAmount = '+=' + ($('#' + this.refs['mention' + this.state.selectedMention].props.id + '_mentions').innerHeight() - 5);
- }
-
- $('#mentionsbox').animate({
- scrollTop: scrollAmount
- }, 75);
- }
- render() {
- var mentionText = this.state.mentionText;
- if (mentionText === '-1') {
- return null;
- }
-
- var profiles = UserStore.getActiveOnlyProfiles();
- var users = [];
- for (let id in profiles) {
- if (profiles[id]) {
- users.push(profiles[id]);
- }
- }
-
- var all = {};
- all.username = 'all';
- all.nickname = '';
- all.secondary_text = 'Notifies everyone in the team';
- all.id = 'allmention';
- users.push(all);
-
- var channel = {};
- channel.username = 'channel';
- channel.nickname = '';
- channel.secondary_text = 'Notifies everyone in the channel';
- channel.id = 'channelmention';
- users.push(channel);
-
- users.sort(function sortByUsername(a, b) {
- if (a.username < b.username) {
- return -1;
- }
- if (a.username > b.username) {
- return 1;
- }
- return 0;
- });
- var mentions = [];
- var index = 0;
-
- for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) {
- if ((users[i].first_name && users[i].first_name.lastIndexOf(mentionText, 0) === 0) ||
- (users[i].last_name && users[i].last_name.lastIndexOf(mentionText, 0) === 0) ||
- users[i].username.lastIndexOf(mentionText, 0) === 0) {
- let isFocused = '';
- if (this.state.selectedMention === index) {
- isFocused = 'mentions-focus';
- }
-
- if (!users[i].secondary_text) {
- users[i].secondary_text = Utils.getFullName(users[i]);
- }
-
- mentions[index] = (
- <Mention
- key={'mention_key_' + index}
- ref={'mention' + index}
- username={users[i].username}
- secondary_text={users[i].secondary_text}
- id={users[i].id}
- listId={index}
- isFocused={isFocused}
- handleMouseEnter={this.handleMouseEnter.bind(this, index)}
- handleClick={this.handleClick}
- />
- );
- index++;
- }
- }
-
- var numMentions = mentions.length;
-
- if (numMentions < 1) {
- return null;
- }
-
- var $mentionTab = $('#' + this.props.id);
- var maxHeight = Math.min(MAX_HEIGHT_LIST, $mentionTab.offset().top - 10);
- var style = {
- height: Math.min(maxHeight, (numMentions * ITEM_HEIGHT) + 4),
- width: $mentionTab.parent().width(),
- bottom: $(window).height() - $mentionTab.offset().top,
- left: $mentionTab.offset().left
- };
-
- return (
- <div
- className='mentions--top'
- style={style}
- >
- <div
- ref='mentionlist'
- className='mentions-box'
- id='mentionsbox'
- >
- {mentions}
- </div>
- </div>
- );
- }
-}
-
-MentionList.propTypes = {
- id: React.PropTypes.string
-};
diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx
index 00c427c79..8e0380c06 100644
--- a/web/react/components/message_wrapper.jsx
+++ b/web/react/components/message_wrapper.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var TextFormatting = require('../utils/text_formatting.jsx');
+import * as TextFormatting from '../utils/text_formatting.jsx';
export default class MessageWrapper extends React.Component {
constructor(props) {
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index c4f831c2e..29512b9b7 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Mattermost, 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 ChannelStore = require('../stores/channel_store.jsx');
-var LoadingScreen = require('./loading_screen.jsx');
-var NewChannelFlow = require('./new_channel_flow.jsx');
+import * as utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import LoadingScreen from './loading_screen.jsx';
+import NewChannelFlow from './new_channel_flow.jsx';
function getStateFromStores() {
return {
@@ -31,12 +31,12 @@ export default class MoreChannels extends React.Component {
}
componentDidMount() {
ChannelStore.addMoreChangeListener(this.onListenerChange);
- $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function shown() {
- asyncClient.getMoreChannels(true);
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => {
+ AsyncClient.getMoreChannels(true);
});
var self = this;
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
var button = e.relatedTarget;
self.setState({channelType: $(button).attr('data-channeltype')});
});
@@ -46,22 +46,22 @@ export default class MoreChannels extends React.Component {
}
onListenerChange() {
var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState.channels, this.state.channels)) {
+ if (!utils.areObjectsEqual(newState.channels, this.state.channels)) {
this.setState(newState);
}
}
handleJoin(channel, channelIndex) {
this.setState({joiningChannel: channelIndex});
client.joinChannel(channel.id,
- function joinSuccess() {
+ () => {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- asyncClient.getChannel(channel.id);
+ AsyncClient.getChannel(channel.id);
utils.switchChannel(channel);
this.setState({joiningChannel: -1});
- }.bind(this),
- function joinFail(err) {
+ },
+ (err) => {
this.setState({joiningChannel: -1, serverError: err.message});
- }.bind(this)
+ }
);
}
handleNewChannel() {
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index d1265f67e..cf40af6ae 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -2,8 +2,8 @@
// See License.txt for license information.
const Modal = ReactBootstrap.Modal;
-const UserStore = require('../stores/user_store.jsx');
-const Utils = require('../utils/utils.jsx');
+import UserStore from '../stores/user_store.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class MoreDirectChannels extends React.Component {
constructor(props) {
@@ -166,7 +166,7 @@ export default class MoreDirectChannels extends React.Component {
componentDidUpdate(prevProps) {
if (!prevProps.show && this.props.show) {
- $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300);
+ $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 50);
if ($(window).width() > 768) {
$(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
}
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index ccf8a2445..78b67a216 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const SocketStore = require('../stores/socket_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
+import SocketStore from '../stores/socket_store.jsx';
+import UserStore from '../stores/user_store.jsx';
-const Constants = require('../utils/constants.jsx');
+import Constants from '../utils/constants.jsx';
const SocketEvents = Constants.SocketEvents;
export default class MsgTyping extends React.Component {
@@ -74,11 +74,12 @@ export default class MsgTyping extends React.Component {
case 1:
text = users[0] + ' is typing...';
break;
- default:
+ default: {
const last = users.pop();
text = users.join(', ') + ' and ' + last + ' are typing...';
break;
}
+ }
this.setState({text});
}
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index af29f219e..ae14fca2f 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -1,23 +1,28 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
-const MessageWrapper = require('./message_wrapper.jsx');
-const NotifyCounts = require('./notify_counts.jsx');
-const ChannelMembersModal = require('./channel_members_modal.jsx');
-const ChannelInviteModal = require('./channel_invite_modal.jsx');
+import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
+import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
+import MessageWrapper from './message_wrapper.jsx';
+import NotifyCounts from './notify_counts.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
+import ChannelInfoModal from './channel_info_modal.jsx';
+import ChannelInviteModal from './channel_invite_modal.jsx';
+import ChannelNotificationsModal from './channel_notifications_modal.jsx';
+import DeleteChannelModal from './delete_channel_modal.jsx';
+import ToggleModalButton from './toggle_modal_button.jsx';
-const UserStore = require('../stores/user_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const TeamStore = require('../stores/team_store.jsx');
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const Utils = require('../utils/utils.jsx');
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Utils from '../utils/utils.jsx';
-const Constants = require('../utils/constants.jsx');
+import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
const Popover = ReactBootstrap.Popover;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -28,13 +33,17 @@ export default class Navbar extends React.Component {
this.onChange = this.onChange.bind(this);
this.handleLeave = this.handleLeave.bind(this);
+ this.showSearch = this.showSearch.bind(this);
+
+ this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this);
+
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
+ state.showEditChannelHeaderModal = false;
state.showMembersModal = false;
- state.showInviteModal = false;
this.state = state;
}
getStateFromStores() {
@@ -96,23 +105,36 @@ export default class Navbar extends React.Component {
$('.inner__wrap').toggleClass('move--left-small');
$('.sidebar--menu').toggleClass('move--left');
}
+ showSearch() {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SHOW_SEARCH
+ });
+ }
onChange() {
this.setState(this.getStateFromStores());
$('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true});
}
+ showEditChannelHeaderModal() {
+ // this can't be done using a ToggleModalButton because we can't use one inside an OverlayTrigger
+ if (this.refs.headerOverlay) {
+ this.refs.headerOverlay.hide();
+ }
+
+ this.setState({
+ showEditChannelHeaderModal: true
+ });
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- data-toggle='modal'
- data-target='#channel_info'
- data-channelid={channel.id}
- href='#'
+ dialogType={ChannelInfoModal}
+ dialogProps={{channel}}
>
{'View Info'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -121,11 +143,7 @@ export default class Navbar extends React.Component {
<a
role='menuitem'
href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
+ onClick={this.showEditChannelHeaderModal}
>
{'Set Channel Header...'}
</a>
@@ -152,13 +170,13 @@ export default class Navbar extends React.Component {
if (!isDirect && !ChannelStore.isDefault(channel)) {
addMembersOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- onClick={() => this.setState({showInviteModal: false})}
+ dialogType={ChannelInviteModal}
+ dialogProps={{channel}}
>
{'Add Members'}
- </a>
+ </ToggleModalButton>
</li>
);
@@ -178,18 +196,32 @@ export default class Navbar extends React.Component {
var manageMembersOption;
var renameChannelOption;
var deleteChannelOption;
- if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
- manageMembersOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={() => this.setState({showMembersModal: true})}
- >
- {'Manage Members'}
- </a>
- </li>
- );
+ if (!isDirect && isAdmin) {
+ if (!ChannelStore.isDefault(channel)) {
+ manageMembersOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showMembersModal: true})}
+ >
+ {'Manage Members'}
+ </a>
+ </li>
+ );
+
+ deleteChannelOption = (
+ <li role='presentation'>
+ <ToggleModalButton
+ role='menuitem'
+ dialogType={DeleteChannelModal}
+ dialogProps={{channel}}
+ >
+ {'Delete Channel...'}
+ </ToggleModalButton>
+ </li>
+ );
+ }
renameChannelOption = (
<li role='presentation'>
@@ -206,37 +238,19 @@ export default class Navbar extends React.Component {
</a>
</li>
);
-
- deleteChannelOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#delete_channel'
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- {'Delete Channel...'}
- </a>
- </li>
- );
}
var notificationPreferenceOption;
if (!isDirect) {
notificationPreferenceOption = (
<li role='presentation'>
- <a
+ <ToggleModalButton
role='menuitem'
- href='#'
- data-toggle='modal'
- data-target='#channel_notifications'
- data-title={channel.display_name}
- data-channelid={channel.id}
+ dialogType={ChannelNotificationsModal}
+ dialogProps={{channel}}
>
{'Notification Preferences'}
- </a>
+ </ToggleModalButton>
</li>
);
}
@@ -245,6 +259,7 @@ export default class Navbar extends React.Component {
<div className='navbar-brand'>
<div className='dropdown'>
<OverlayTrigger
+ ref='headerOverlay'
trigger='click'
placement='bottom'
overlay={popoverContent}
@@ -257,7 +272,6 @@ export default class Navbar extends React.Component {
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
@@ -267,7 +281,6 @@ export default class Navbar extends React.Component {
<ul
className='dropdown-menu'
role='menu'
- aria-labelledby='channel_header_dropdown'
>
{viewInfoOption}
{addMembersOption}
@@ -354,6 +367,9 @@ export default class Navbar extends React.Component {
var isAdmin = false;
var isDirect = false;
+ var editChannelHeaderModal = null;
+ var editChannelPurposeModal = null;
+
if (channel) {
popoverContent = (
<Popover
@@ -396,11 +412,7 @@ export default class Navbar extends React.Component {
<br/>
<a
href='#'
- data-toggle='modal'
- data-header={channel.header}
- data-title={channel.display_name}
- data-channelid={channel.id}
- data-target='#edit_channel'
+ onClick={this.showEditChannelHeaderModal}
>
{'Click here'}
</a>
@@ -409,10 +421,36 @@ export default class Navbar extends React.Component {
</Popover>
);
}
+
+ editChannelHeaderModal = (
+ <EditChannelHeaderModal
+ show={this.state.showEditChannelHeaderModal}
+ onHide={() => this.setState({showEditChannelHeaderModal: false})}
+ channel={channel}
+ />
+ );
+
+ editChannelPurposeModal = (
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
+ const searchButton = (
+ <button
+ type='button'
+ className='navbar-toggle pull-right'
+ onClick={this.showSearch}
+ >
+ <span className='glyphicon glyphicon-search icon--white' />
+ </button>
+ );
+
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return (
@@ -424,22 +462,17 @@ export default class Navbar extends React.Component {
<div className='container-fluid theme'>
<div className='navbar-header'>
{collapseButtons}
+ {searchButton}
{channelMenuDropdown}
</div>
</div>
</nav>
- <EditChannelPurposeModal
- show={this.state.showEditChannelPurposeModal}
- onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
- channel={channel}
- />
+ {editChannelHeaderModal}
+ {editChannelPurposeModal}
<ChannelMembersModal
show={this.state.showMembersModal}
onModalDismissed={() => this.setState({showMembersModal: false})}
- />
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
+ channel={{channel}}
/>
</div>
);
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 0b755f377..c286ee6f9 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -1,16 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-var AboutBuildModal = require('./about_build_modal.jsx');
-var InviteMemberModal = require('./invite_member_modal.jsx');
-var UserSettingsModal = require('./user_settings/user_settings_modal.jsx');
+import AboutBuildModal from './about_build_modal.jsx';
+import TeamMembersModal from './team_members_modal.jsx';
+import ToggleModalButton from './toggle_modal_button.jsx';
+import UserSettingsModal from './user_settings/user_settings_modal.jsx';
-var Constants = require('../utils/constants.jsx');
+import Constants from '../utils/constants.jsx';
function getStateFromStores() {
const teams = [];
@@ -70,7 +72,7 @@ export default class NavbarDropdown extends React.Component {
}
onListenerChange() {
var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -93,7 +95,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
{'Invite New Member'}
</a>
@@ -105,10 +107,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
>
{'Get Team Invite Link'}
</a>
@@ -120,13 +119,9 @@ export default class NavbarDropdown extends React.Component {
if (isAdmin) {
manageLink = (
<li>
- <a
- href='#'
- data-toggle='modal'
- data-target='#team_members'
- >
+ <ToggleModalButton dialogType={TeamMembersModal}>
{'Manage Members'}
- </a>
+ </ToggleModalButton>
</li>
);
diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx
index d6280d118..3a114aa19 100644
--- a/web/react/components/new_channel_flow.jsx
+++ b/web/react/components/new_channel_flow.jsx
@@ -1,13 +1,13 @@
// Copyright (c) 2015 Mattermost, 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');
+import * as Utils from '../utils/utils.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
-var NewChannelModal = require('./new_channel_modal.jsx');
-var ChangeURLModal = require('./change_url_modal.jsx');
+import NewChannelModal from './new_channel_modal.jsx';
+import ChangeURLModal from './change_url_modal.jsx';
const SHOW_NEW_CHANNEL = 1;
const SHOW_EDIT_URL = 2;
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index c0cea496f..70fe10eef 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
var Modal = ReactBootstrap.Modal;
export default class NewChannelModal extends React.Component {
@@ -115,7 +115,7 @@ export default class NewChannelModal extends React.Component {
type='text'
ref='display_name'
className='form-control'
- placeholder='Ex: "Bugs", "Marketing", "办公室恋情"'
+ placeholder='E.g.: "Bugs", "Marketing", "办公室恋情"'
maxLength='22'
value={this.props.channelData.displayName}
autoFocus={true}
diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx
index 54b9e4289..19b81556b 100644
--- a/web/react/components/notify_counts.jsx
+++ b/web/react/components/notify_counts.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
+import * as utils from '../utils/utils.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
function getCountsStateFromStores() {
var count = 0;
@@ -39,7 +39,7 @@ export default class NotifyCounts extends React.Component {
}
onListenerChange() {
var newState = getCountsStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx
index 54d126144..4c9bb6310 100644
--- a/web/react/components/password_reset.jsx
+++ b/web/react/components/password_reset.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PasswordResetSendLink = require('./password_reset_send_link.jsx');
-var PasswordResetForm = require('./password_reset_form.jsx');
+import PasswordResetSendLink from './password_reset_send_link.jsx';
+import PasswordResetForm from './password_reset_form.jsx';
export default class PasswordReset extends React.Component {
constructor(props) {
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index b452c40b7..812911569 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var client = require('../utils/client.jsx');
+import * as client from '../utils/client.jsx';
export default class PasswordResetForm extends React.Component {
constructor(props) {
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 8f1890705..051b8b02c 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
export default class PasswordResetSendLink extends React.Component {
constructor(props) {
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index f3c0fa0b4..f4cb542e4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
+import UserStore from '../stores/user_store.jsx';
var Popover = ReactBootstrap.Popover;
var Overlay = ReactBootstrap.Overlay;
-const Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
-const ChannelStore = require('../stores/channel_store.jsx');
+import ChannelStore from '../stores/channel_store.jsx';
export default class PopoverListMembers extends React.Component {
constructor(props) {
@@ -68,9 +69,7 @@ export default class PopoverListMembers extends React.Component {
}
render() {
- let popoverHtml = [];
- let count = 0;
- let countText = '-';
+ const popoverHtml = [];
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
@@ -78,35 +77,13 @@ export default class PopoverListMembers extends React.Component {
if (members && teamMembers) {
members.sort((a, b) => {
- return a.username.localeCompare(b.username);
+ const aName = Utils.displayUsername(a.id);
+ const bName = Utils.displayUsername(b.id);
+
+ return aName.localeCompare(bName);
});
members.forEach((m, i) => {
- const details = [];
-
- const fullName = Utils.getFullName(m);
- if (fullName) {
- details.push(
- <span
- key={`${m.id}__full-name`}
- className='full-name'
- >
- {fullName}
- </span>
- );
- }
-
- if (m.nickname) {
- const separator = fullName ? ' - ' : '';
- details.push(
- <span
- key={`${m.nickname}__nickname`}
- >
- {separator + m.nickname}
- </span>
- );
- }
-
let button = '';
if (currentUserId !== m.id && ch.type !== 'D') {
button = (
@@ -120,7 +97,12 @@ export default class PopoverListMembers extends React.Component {
);
}
- if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
+ let name = '';
+ if (teamMembers[m.username]) {
+ name = Utils.displayUsername(teamMembers[m.username].id);
+ }
+
+ if (name && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
className='text-nowrap'
@@ -137,7 +119,7 @@ export default class PopoverListMembers extends React.Component {
<div
className='more-name'
>
- {m.username}
+ {name}
</div>
</div>
<div
@@ -147,15 +129,22 @@ export default class PopoverListMembers extends React.Component {
</div>
</div>
);
- count++;
}
});
+ }
- if (count > 20) {
- countText = '20+';
- } else if (count > 0) {
- countText = count.toString();
- }
+ let count = this.props.memberCount;
+ let countText = '-';
+
+ // fall back to checking the length of the member list if the count isn't set
+ if (!count && members) {
+ count = members.length;
+ }
+
+ if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) {
+ countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+';
+ } else if (count > 0) {
+ countText = count.toString();
}
return (
@@ -195,5 +184,6 @@ export default class PopoverListMembers extends React.Component {
PopoverListMembers.propTypes = {
members: React.PropTypes.array.isRequired,
+ memberCount: React.PropTypes.number,
channelId: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index c3c5b3e0b..695d7daef 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -1,19 +1,17 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostHeader = require('./post_header.jsx');
-var PostBody = require('./post_body.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
+import PostHeader from './post_header.jsx';
+import PostBody from './post_body.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
+import UserStore from '../stores/user_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
-var utils = require('../utils/utils.jsx');
-
-var PostInfo = require('./post_info.jsx');
+import * as utils from '../utils/utils.jsx';
export default class Post extends React.Component {
constructor(props) {
@@ -77,7 +75,7 @@ export default class Post extends React.Component {
this.forceUpdate();
}
shouldComponentUpdate(nextProps) {
- if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
@@ -89,10 +87,18 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.displayNameType !== this.props.displayNameType) {
+ return true;
+ }
+
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
return true;
}
+ if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
+ return true;
+ }
+
return false;
}
getCommentCount(props) {
@@ -107,7 +113,7 @@ export default class Post extends React.Component {
} else {
commentRootId = post.id;
}
- for (let postId in posts) {
+ for (const postId in posts) {
if (posts[postId].root_id === commentRootId) {
commentCount += 1;
}
@@ -116,68 +122,79 @@ export default class Post extends React.Component {
return commentCount;
}
render() {
- var post = this.props.post;
- var parentPost = this.props.parentPost;
- var posts = this.props.posts;
+ const post = this.props.post;
+ const parentPost = this.props.parentPost;
+ const posts = this.props.posts;
if (!post.props) {
post.props = {};
}
- var type = 'Post';
+ let type = 'Post';
if (post.root_id && post.root_id.length > 0) {
type = 'Comment';
}
const commentCount = this.getCommentCount(this.props);
- var rootUser;
+ let rootUser;
if (this.props.sameRoot) {
rootUser = 'same--root';
} else {
rootUser = 'other--root';
}
- var postType = '';
+ let postType = '';
if (type !== 'Post') {
postType = 'post--comment';
+ } else if (commentCount > 0) {
+ postType = 'post--root';
}
- var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
+ let currentUserCss = '';
+ if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !utils.isSystemMessage(post)) {
currentUserCss = 'current--user';
}
- var userProfile = UserStore.getProfile(post.user_id);
+ const userProfile = UserStore.getProfile(post.user_id);
- var timestamp = UserStore.getCurrentUser().update_at;
+ let timestamp = UserStore.getCurrentUser().update_at;
if (userProfile) {
timestamp = userProfile.update_at;
}
- var sameUserClass = '';
+ let sameUserClass = '';
if (this.props.sameUser) {
sameUserClass = 'same--user';
}
- var profilePic = null;
+ let shouldHighlightClass = '';
+ if (this.props.shouldHighlight) {
+ shouldHighlightClass = 'post--highlight';
+ }
+
+ let systemMessageClass = '';
+ if (utils.isSystemMessage(post)) {
+ systemMessageClass = 'post--system';
+ }
+
+ let profilePic = null;
if (!this.props.hideProfilePic) {
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
+ } else if (utils.isSystemMessage(post)) {
+ src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
}
profilePic = (
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={src}
- height='36'
- width='36'
- />
- </div>
+ <img
+ src={src}
+ height='36'
+ width='36'
+ />
);
}
@@ -185,34 +202,28 @@ export default class Post extends React.Component {
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}
+ className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
>
- {profilePic}
<div className='post__content'>
- <PostHeader
- ref='header'
- post={post}
- sameRoot={this.props.sameRoot}
- commentCount={commentCount}
- handleCommentClick={this.handleCommentClick}
- isLastComment={this.props.isLastComment}
- />
- <PostBody
- post={post}
- sameRoot={this.props.sameRoot}
- parentPost={parentPost}
- posts={posts}
- handleCommentClick={this.handleCommentClick}
- retryPost={this.retryPost}
- />
- <PostInfo
- ref='info'
- post={post}
- sameRoot={this.props.sameRoot}
- commentCount={commentCount}
- handleCommentClick={this.handleCommentClick}
- allowReply='true'
- />
+ <div className='post__img'>{profilePic}</div>
+ <div>
+ <PostHeader
+ ref='header'
+ post={post}
+ sameRoot={this.props.sameRoot}
+ commentCount={commentCount}
+ handleCommentClick={this.handleCommentClick}
+ isLastComment={this.props.isLastComment}
+ />
+ <PostBody
+ post={post}
+ sameRoot={this.props.sameRoot}
+ parentPost={parentPost}
+ posts={posts}
+ handleCommentClick={this.handleCommentClick}
+ retryPost={this.retryPost}
+ />
+ </div>
</div>
</div>
</div>
@@ -227,5 +238,7 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool
+ isLastComment: React.PropTypes.bool,
+ shouldHighlight: React.PropTypes.bool,
+ displayNameType: React.PropTypes.string
};
diff --git a/web/react/components/post_attachment.jsx b/web/react/components/post_attachment.jsx
index cf65dfbfb..676bc91af 100644
--- a/web/react/components/post_attachment.jsx
+++ b/web/react/components/post_attachment.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const TextFormatting = require('../utils/text_formatting.jsx');
+import * as TextFormatting from '../utils/text_formatting.jsx';
export default class PostAttachment extends React.Component {
constructor(props) {
diff --git a/web/react/components/post_attachment_list.jsx b/web/react/components/post_attachment_list.jsx
index 03b866656..9c6700e2d 100644
--- a/web/react/components/post_attachment_list.jsx
+++ b/web/react/components/post_attachment_list.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const PostAttachment = require('./post_attachment.jsx');
+import PostAttachment from './post_attachment.jsx';
export default class PostAttachmentList extends React.Component {
constructor(props) {
diff --git a/web/react/components/post_attachment_oembed.jsx b/web/react/components/post_attachment_oembed.jsx
new file mode 100644
index 000000000..4b12ee95e
--- /dev/null
+++ b/web/react/components/post_attachment_oembed.jsx
@@ -0,0 +1,103 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class PostAttachmentOEmbed extends React.Component {
+ constructor(props) {
+ super(props);
+ this.fetchData = this.fetchData.bind(this);
+
+ this.isLoading = false;
+ }
+
+ componentWillMount() {
+ this.setState({data: {}});
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.link !== this.props.link) {
+ this.isLoading = false;
+ this.fetchData(nextProps.link);
+ }
+ }
+
+ componentDidMount() {
+ this.fetchData(this.props.link);
+ }
+
+ fetchData(link) {
+ if (!this.isLoading) {
+ this.isLoading = true;
+ let url = 'https://noembed.com/embed?nowrap=on';
+ url += '&url=' + encodeURIComponent(link);
+ url += '&maxheight=' + this.props.provider.height;
+ return $.ajax({
+ url,
+ dataType: 'jsonp',
+ success: (result) => {
+ this.isLoading = false;
+ if (result.error) {
+ this.setState({data: {}});
+ } else {
+ this.setState({data: result});
+ }
+ },
+ error: () => {
+ this.setState({data: {}});
+ }
+ });
+ }
+ }
+
+ render() {
+ let data = {};
+ let content;
+ if ($.isEmptyObject(this.state.data)) {
+ content = <div style={{height: this.props.provider.height}}/>;
+ } else {
+ data = this.state.data;
+ content = (
+ <div
+ style={{height: this.props.provider.height}}
+ dangerouslySetInnerHTML={{__html: data.html}}
+ />
+ );
+ }
+
+ return (
+ <div
+ className='attachment attachment--oembed'
+ ref='attachment'
+ >
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <h1
+ className='attachment__title'
+ >
+ <a
+ className='attachment__title-link'
+ href={data.url}
+ target='_blank'
+ >
+ {data.title}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ {content}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+PostAttachmentOEmbed.propTypes = {
+ link: React.PropTypes.string.isRequired,
+ provider: React.PropTypes.object.isRequired
+};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 61a0c3e2d..dcbe56399 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -1,26 +1,30 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
-const PostBodyAdditionalContent = require('./post_body_additional_content.jsx');
+import FileAttachmentList from './file_attachment_list.jsx';
+import UserStore from '../stores/user_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+import * as Emoji from '../utils/emoticons.jsx';
+import Constants from '../utils/constants.jsx';
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import twemoji from 'twemoji';
+import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+
+import providers from './providers.json';
export default class PostBody extends React.Component {
constructor(props) {
super(props);
this.receivedYoutubeData = false;
- this.isGifLoading = false;
+ this.isImgLoading = false;
this.handleUserChange = this.handleUserChange.bind(this);
this.parseEmojis = this.parseEmojis.bind(this);
this.createEmbed = this.createEmbed.bind(this);
- this.createGifEmbed = this.createGifEmbed.bind(this);
- this.loadGif = this.loadGif.bind(this);
+ this.createImageEmbed = this.createImageEmbed.bind(this);
+ this.loadImg = this.loadImg.bind(this);
this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this);
const linkData = Utils.extractLinks(this.props.post.message);
@@ -29,6 +33,7 @@ export default class PostBody extends React.Component {
this.state = {
links: linkData.links,
message: linkData.text,
+ post: this.props.post,
hasUserProfiles: profiles && Object.keys(profiles).length > 1
};
}
@@ -49,7 +54,17 @@ export default class PostBody extends React.Component {
}
parseEmojis() {
- twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {
+ className: 'emoji twemoji',
+ base: '',
+ folder: Emoji.getImagePathForEmoticon()
+ });
+ }
+
+ componentWillMount() {
+ if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) {
+ this.embed = this.createEmbed(this.state.links[0]);
+ }
}
componentDidMount() {
@@ -76,47 +91,86 @@ export default class PostBody extends React.Component {
componentWillReceiveProps(nextProps) {
const linkData = Utils.extractLinks(nextProps.post.message);
+ if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) {
+ this.embed = this.createEmbed(linkData.links[0]);
+ }
this.setState({links: linkData.links, message: linkData.text});
}
createEmbed(link) {
- let embed = this.createYoutubeEmbed(link);
+ const post = this.state.post;
+
+ if (!link) {
+ if (post.type === 'oEmbed') {
+ post.props.oEmbedLink = '';
+ post.type = '';
+ }
+ return null;
+ }
+
+ const trimmedLink = link.trim();
+
+ if (Utils.isFeatureEnabled(PreReleaseFeatures.EMBED_PREVIEW)) {
+ const provider = this.getOembedProvider(trimmedLink);
+ if (provider != null) {
+ post.props.oEmbedLink = trimmedLink;
+ post.type = 'oEmbed';
+ this.setState({post, provider});
+ return '';
+ }
+ }
+
+ const embed = this.createYoutubeEmbed(link);
if (embed != null) {
return embed;
}
- embed = this.createGifEmbed(link);
+ for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) {
+ const imageType = Constants.IMAGE_TYPES[i];
+ const suffix = link.substring(link.length - (imageType.length + 1));
+ if (suffix === '.' + imageType || suffix === '=' + imageType) {
+ return this.createImageEmbed(link, this.state.imgLoaded);
+ }
+ }
+
+ return null;
+ }
- return embed;
+ getOembedProvider(link) {
+ for (let i = 0; i < providers.length; i++) {
+ for (let j = 0; j < providers[i].patterns.length; j++) {
+ if (link.match(providers[i].patterns[j])) {
+ return providers[i];
+ }
+ }
+ }
+ return null;
}
- loadGif(src) {
- if (this.isGifLoading) {
+ loadImg(src) {
+ if (this.isImgLoading) {
return;
}
- this.isGifLoading = true;
+ this.isImgLoading = true;
- const gif = new Image();
- gif.onload = (
+ const img = new Image();
+ img.onload = (
() => {
- this.setState({gifLoaded: true});
+ this.embed = this.createImageEmbed(src, true);
+ this.setState({imgLoaded: true});
}
);
- gif.src = src;
+ img.src = src;
}
- createGifEmbed(link) {
- if (link.substring(link.length - 4) !== '.gif') {
- return null;
- }
-
- if (!this.state.gifLoaded) {
- this.loadGif(link);
+ createImageEmbed(link, isLoaded) {
+ if (!isLoaded) {
+ this.loadImg(link);
return (
<img
- className='gif-div placeholder'
+ className='img-div placeholder'
height='500px'
/>
);
@@ -124,7 +178,7 @@ export default class PostBody extends React.Component {
return (
<img
- className='gif-div'
+ className='img-div'
src={link}
/>
);
@@ -133,7 +187,7 @@ export default class PostBody extends React.Component {
handleYoutubeTime(link) {
const timeRegex = /[\\?&]t=([0-9hms]+)/;
- const time = link.trim().match(timeRegex);
+ const time = link.match(timeRegex);
if (!time || !time[1]) {
return '';
}
@@ -160,14 +214,14 @@ export default class PostBody extends React.Component {
}
createYoutubeEmbed(link) {
- const ytRegex = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|watch\?(?:[a-zA-Z-_]+=[a-zA-Z0-9-_]+&)+v=)([^#\&\?]*).*/;
+ const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#\&\?]*)/;
const match = link.trim().match(ytRegex);
- if (!match || match[1].length !== 11) {
+ if (!match || match[2].length !== 11) {
return null;
}
- const youtubeId = match[1];
+ const youtubeId = match[2];
const time = this.handleYoutubeTime(link);
function onClick(e) {
@@ -212,7 +266,7 @@ export default class PostBody extends React.Component {
}
return (
- <div className='post-comment'>
+ <div>
<h4>
<span className='video-type'>{header}</span>
<span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
@@ -255,7 +309,15 @@ export default class PostBody extends React.Component {
let apostrophe = '';
let name = '...';
if (profile != null) {
- if (profile.username.slice(-1) === 's') {
+ let username = profile.username;
+ if (parentPost.props &&
+ parentPost.props.from_webhook &&
+ parentPost.props.override_username &&
+ global.window.mm_config.EnablePostUsernameOverride === 'true') {
+ username = parentPost.props.override_username;
+ }
+
+ if (username.slice(-1) === 's') {
apostrophe = '\'';
} else {
apostrophe = '\'s';
@@ -263,9 +325,9 @@ export default class PostBody extends React.Component {
name = (
<a
className='theme'
- onClick={Utils.searchForTerm.bind(null, profile.username)}
+ onClick={Utils.searchForTerm.bind(null, username)}
>
- {profile.username}
+ {username}
</a>
);
}
@@ -284,7 +346,7 @@ export default class PostBody extends React.Component {
}
comment = (
- <p className='post-link'>
+ <div className='post__link'>
<span>
{'Commented on '}{name}{apostrophe}{' message: '}
<a
@@ -294,15 +356,13 @@ export default class PostBody extends React.Component {
{message}
</a>
</span>
- </p>
+ </div>
);
-
- postClass += ' post-comment';
}
let loading;
if (post.state === Constants.POST_FAILED) {
- postClass += ' post-fail';
+ postClass += ' post--fail';
loading = (
<a
className='theme post-retry pull-right'
@@ -322,11 +382,6 @@ export default class PostBody extends React.Component {
);
}
- let embed;
- if (filenames.length === 0 && this.state.links && this.state.links.length > 0) {
- embed = this.createEmbed(this.state.links[0]);
- }
-
let fileAttachmentHolder = '';
if (filenames && filenames.length > 0) {
fileAttachmentHolder = (
@@ -339,25 +394,28 @@ export default class PostBody extends React.Component {
}
return (
- <div className='post-body'>
+ <div>
{comment}
- <div
- key={`${post.id}_message`}
- id={`${post.id}_message`}
- className={postClass}
- >
- {loading}
- <span
- ref='message_span'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ <div className='post__body'>
+ <div
+ key={`${post.id}_message`}
+ id={`${post.id}_message`}
+ className={postClass}
+ >
+ {loading}
+ <span
+ ref='message_span'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
+ />
+ </div>
+ <PostBodyAdditionalContent
+ post={this.state.post}
+ provider={this.state.provider}
/>
+ {fileAttachmentHolder}
+ {this.embed}
</div>
- <PostBodyAdditionalContent
- post={post}
- />
- {fileAttachmentHolder}
- {embed}
</div>
);
}
diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx
index 8189ba2d3..7e6f3f037 100644
--- a/web/react/components/post_body_additional_content.jsx
+++ b/web/react/components/post_body_additional_content.jsx
@@ -1,13 +1,15 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const PostAttachmentList = require('./post_attachment_list.jsx');
+import PostAttachmentList from './post_attachment_list.jsx';
+import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
export default class PostBodyAdditionalContent extends React.Component {
constructor(props) {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
+ this.getOembedAttachment = this.getOembedAttachment.bind(this);
this.getComponent = this.getComponent.bind(this);
}
@@ -25,17 +27,32 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
+ getOembedAttachment() {
+ const link = this.props.post.props && this.props.post.props.oEmbedLink || '';
+ return (
+ <PostAttachmentOEmbed
+ key={'post_body_additional_content' + this.props.post.id}
+ provider={this.props.provider}
+ link={link}
+ />
+ );
+ }
+
getComponent() {
- switch (this.state.type) {
+ switch (this.props.post.type) {
case 'slack_attachment':
return this.getSlackAttachment();
+ case 'oEmbed':
+ return this.getOembedAttachment();
+ default:
+ return '';
}
}
render() {
let content = [];
- if (this.state.shouldRender) {
+ if (Boolean(this.props.post.type)) {
const component = this.getComponent();
if (component) {
@@ -52,5 +69,6 @@ export default class PostBodyAdditionalContent extends React.Component {
}
PostBodyAdditionalContent.propTypes = {
- post: React.PropTypes.object.isRequired
-}; \ No newline at end of file
+ post: React.PropTypes.object.isRequired,
+ provider: React.PropTypes.object
+};
diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx
index ba07a22f5..3723bcaba 100644
--- a/web/react/components/post_deleted_modal.jsx
+++ b/web/react/components/post_deleted_modal.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
+import UserStore from '../stores/user_store.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
var ActionTypes = Constants.ActionTypes;
export default class PostDeletedModal extends React.Component {
diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx
new file mode 100644
index 000000000..adcd78839
--- /dev/null
+++ b/web/react/components/post_focus_view.jsx
@@ -0,0 +1,110 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PostsView from './posts_view.jsx';
+
+import PostStore from '../stores/post_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+
+export default class PostFocusView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChannelChange = this.onChannelChange.bind(this);
+ this.onPostsChange = this.onPostsChange.bind(this);
+ this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
+
+ const focusedPostId = PostStore.getFocusedPostId();
+
+ this.state = {
+ scrollType: PostsView.SCROLL_TYPE_POST,
+ scrollPostId: focusedPostId,
+ postList: PostStore.getVisiblePosts(focusedPostId),
+ atTop: PostStore.getVisibilityAtTop(focusedPostId),
+ atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
+ };
+ }
+
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.onChannelChange);
+ PostStore.addChangeListener(this.onPostsChange);
+ }
+
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.onChannelChange);
+ PostStore.removeChangeListener(this.onPostsChange);
+ }
+
+ onChannelChange() {
+ this.setState({
+ scrollType: PostsView.SCROLL_TYPE_POST
+ });
+ }
+
+ onPostsChange() {
+ const focusedPostId = PostStore.getFocusedPostId();
+ if (focusedPostId == null) {
+ return;
+ }
+
+ this.setState({
+ scrollPostId: focusedPostId,
+ postList: PostStore.getVisiblePosts(focusedPostId),
+ atTop: PostStore.getVisibilityAtTop(focusedPostId),
+ atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
+ });
+ }
+
+ handlePostsViewScroll() {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
+ }
+
+ loadMorePostsTop() {
+ EventHelpers.emitLoadMorePostsFocusedTopEvent();
+ }
+
+ loadMorePostsBottom() {
+ EventHelpers.emitLoadMorePostsFocusedBottomEvent();
+ }
+
+ getIntroMessage() {
+ return (
+ <div className='channel-intro'>
+ <h4 className='channel-intro__title'>{'Beginning of Channel Archives'}</h4>
+ </div>
+ );
+ }
+
+ render() {
+ const postsToHighlight = {};
+ postsToHighlight[this.state.scrollPostId] = true;
+
+ return (
+ <div id='post-list'>
+ <PostsView
+ key={'postfocusview'}
+ isActive={true}
+ postList={this.state.postList}
+ scrollType={this.state.scrollType}
+ scrollPostId={this.state.scrollPostId}
+ postViewScrolled={this.handlePostsViewScroll}
+ loadMorePostsTopClicked={this.loadMorePostsTop}
+ loadMorePostsBottomClicked={this.loadMorePostsBottom}
+ showMoreMessagesTop={!this.state.atTop}
+ showMoreMessagesBottom={!this.state.atBottom}
+ introText={this.getIntroMessage()}
+ messageSeparatorTime={0}
+ postsToHighlight={postsToHighlight}
+ />
+ </div>
+ );
+ }
+}
+PostFocusView.defaultProps = {
+};
+
+PostFocusView.propTypes = {
+};
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 45e60c767..f18024343 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -1,8 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserProfile = require('./user_profile.jsx');
-var PostInfo = require('./post_info.jsx');
+import UserProfile from './user_profile.jsx';
+import PostInfo from './post_info.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import Constants from '../utils/constants.jsx';
export default class PostHeader extends React.Component {
constructor(props) {
@@ -26,14 +29,23 @@ export default class PostHeader extends React.Component {
);
}
- botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ } else if (Utils.isSystemMessage(post)) {
+ userProfile = (
+ <UserProfile
+ userId={''}
+ overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME}
+ overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
+ disablePopover={true}
+ />
+ );
}
return (
- <ul className='post-header post-header-post'>
- <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li>
+ <ul className='post__header'>
+ <li className='col col__name'>{userProfile}</li>
{botIndicator}
- <li className='post-info--hidden'>
+ <li className='col'>
<PostInfo
post={post}
commentCount={this.props.commentCount}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index a01d842e5..21683bb01 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -1,21 +1,31 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var utils = require('../utils/utils.jsx');
-var TimeSince = require('./time_since.jsx');
+import UserStore from '../stores/user_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+import TimeSince from './time_since.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-var Constants = require('../utils/constants.jsx');
+import Constants from '../utils/constants.jsx';
+
+const Overlay = ReactBootstrap.Overlay;
+const Popover = ReactBootstrap.Popover;
export default class PostInfo extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ copiedLink: false,
+ show: false
+ };
+
+ this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
}
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles);
+ var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
return '';
@@ -32,30 +42,37 @@ export default class PostInfo extends React.Component {
dataComments = this.props.commentCount;
}
- if (isOwner) {
+ if (this.props.allowReply === 'true') {
dropdownContents.push(
<li
- key='editPost'
+ key='replyLink'
role='presentation'
>
<a
+ className='link__reply theme'
href='#'
- role='menuitem'
- data-toggle='modal'
- data-target='#edit_post'
- data-refocusid='#post_textbox'
- data-title={type}
- data-message={post.message}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={dataComments}
+ onClick={this.props.handleCommentClick}
>
- Edit
+ {'Reply'}
</a>
</li>
);
}
+ dropdownContents.push(
+ <li
+ key='copyLink'
+ role='presentation'
+ >
+ <a
+ href='#'
+ onClick={(e) => this.setState({target: e.target, show: !this.state.show})}
+ >
+ {'Permalink'}
+ </a>
+ </li>
+ );
+
if (isOwner || isAdmin) {
dropdownContents.push(
<li
@@ -65,31 +82,33 @@ export default class PostInfo extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={dataComments}
+ onClick={() => EventHelpers.showDeletePostModal(post, dataComments)}
>
- Delete
+ {'Delete'}
</a>
</li>
);
}
- if (this.props.allowReply === 'true') {
+ if (isOwner) {
dropdownContents.push(
<li
- key='replyLink'
+ key='editPost'
role='presentation'
>
<a
- className='reply-link theme'
href='#'
- onClick={this.props.handleCommentClick}
+ role='menuitem'
+ data-toggle='modal'
+ data-target='#edit_post'
+ data-refocusid='#post_textbox'
+ data-title={type}
+ data-message={post.message}
+ data-postid={post.id}
+ data-channelid={post.channel_id}
+ data-comments={dataComments}
>
- Reply
+ {'Edit'}
</a>
</li>
);
@@ -103,7 +122,7 @@ export default class PostInfo extends React.Component {
<div>
<a
href='#'
- className='dropdown-toggle theme'
+ className='dropdown-toggle post__dropdown theme'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -117,44 +136,111 @@ export default class PostInfo extends React.Component {
</div>
);
}
+
+ handlePermalinkCopy() {
+ const textBox = $(ReactDOM.findDOMNode(this.refs.permalinkbox));
+ textBox.select();
+
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ this.setState({copiedLink: true, show: false});
+ } else {
+ this.setState({copiedLink: false});
+ }
+ } catch (err) {
+ this.setState({copiedLink: false});
+ }
+ }
render() {
var post = this.props.post;
var comments = '';
- var lastCommentClass = ' comment-icon__container__hide';
- if (this.props.isLastComment) {
- lastCommentClass = ' comment-icon__container__show';
+ var showCommentClass = '';
+ var commentCountText = this.props.commentCount;
+
+ if (this.props.commentCount >= 1) {
+ showCommentClass = ' icon--show';
+ } else {
+ commentCountText = '';
}
- if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
+ if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
comments = (
<a
href='#'
- className={'comment-icon__container theme' + lastCommentClass}
+ className={'comment-icon__container' + showCommentClass}
onClick={this.props.handleCommentClick}
>
<span
className='comment-icon'
dangerouslySetInnerHTML={{__html: Constants.COMMENT_ICON}}
/>
- {this.props.commentCount}
+ {commentCountText}
</a>
);
}
var dropdown = this.createDropdown();
+ const permalink = TeamStore.getCurrentTeamUrl() + '/pl/' + post.id;
+ const copyButtonText = this.state.copiedLink ? (<div>{'Copy '}<i className='fa fa-check'/></div>) : 'Copy';
+ const permalinkOverlay = (
+ <Popover
+ id='permalink-overlay'
+ className='permalink-popover'
+ placement='left'
+ title=''
+ >
+ <div className='form-inline'>
+ <input
+ type='text'
+ readOnly='true'
+ ref='permalinkbox'
+ className='permalink-text form-control no-resize'
+ rows='1'
+ value={permalink}
+ />
+ <button
+ data-copy-btn='true'
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handlePermalinkCopy}
+ data-clipboard-text={permalink}
+ >
+ {copyButtonText}
+ </button>
+ </div>
+ </Popover>
+ );
+
+ const containerPadding = 20;
+
return (
- <ul className='post-header post-info'>
- <li className='post-header-col'>
+ <ul className='post__header post__header--info'>
+ <li className='col'>
<TimeSince
eventTime={post.create_at}
/>
</li>
- <li className='post-header-col post-header__reply'>
- <div className='dropdown'>
+ <li className='col col__reply'>
+ {comments}
+ <div
+ className='dropdown'
+ ref='dotMenu'
+ >
{dropdown}
</div>
- {comments}
+ <Overlay
+ show={this.state.show}
+ target={() => ReactDOM.findDOMNode(this.refs.dotMenu)}
+ onHide={() => this.setState({show: false})}
+ placement='left'
+ container={this}
+ containerPadding={containerPadding}
+ rootClose={true}
+ >
+ {permalinkOverlay}
+ </Overlay>
</li>
</ul>
);
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
index b782268fa..b7ac92672 100644
--- a/web/react/components/posts_view.jsx
+++ b/web/react/components/posts_view.jsx
@@ -1,18 +1,23 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const UserStore = require('../stores/user_store.jsx');
-const Utils = require('../utils/utils.jsx');
-const Post = require('./post.jsx');
-const Constants = require('../utils/constants.jsx');
+import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as Utils from '../utils/utils.jsx';
+import Post from './post.jsx';
+import Constants from '../utils/constants.jsx';
+const Preferences = Constants.Preferences;
export default class PostsView extends React.Component {
constructor(props) {
super(props);
+ this.updateState = this.updateState.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.isAtBottom = this.isAtBottom.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
this.createPosts = this.createPosts.bind(this);
this.updateScrolling = this.updateScrolling.bind(this);
this.handleResize = this.handleResize.bind(this);
@@ -20,6 +25,8 @@ export default class PostsView extends React.Component {
this.jumpToPostNode = null;
this.wasAtBottom = true;
this.scrollHeight = 0;
+
+ this.state = {displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value};
}
static get SCROLL_TYPE_FREE() {
return 1;
@@ -27,12 +34,18 @@ export default class PostsView extends React.Component {
static get SCROLL_TYPE_BOTTOM() {
return 2;
}
- static get SIDEBAR_OPEN() {
+ static get SCROLL_TYPE_SIDEBAR_OPEN() {
return 3;
}
static get SCROLL_TYPE_NEW_MESSAGE() {
return 4;
}
+ static get SCROLL_TYPE_POST() {
+ return 5;
+ }
+ updateState() {
+ this.setState({displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value});
+ }
isAtBottom() {
return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
}
@@ -47,15 +60,22 @@ export default class PostsView extends React.Component {
}
}
this.wasAtBottom = this.isAtBottom();
+ if (!this.jumpToPostNode && childNodes.length > 0) {
+ this.jumpToPostNode = childNodes[childNodes.length - 1];
+ }
// --- --------
this.props.postViewScrolled(this.isAtBottom());
this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ this.prevOffsetTop = this.jumpToPostNode.offsetTop;
}
loadMorePostsTop() {
this.props.loadMorePostsTopClicked();
}
+ loadMorePostsBottom() {
+ this.props.loadMorePostsBottomClicked();
+ }
createPosts(posts, order) {
const postCtls = [];
let previousPostDay = new Date(0);
@@ -63,15 +83,11 @@ export default class PostsView extends React.Component {
let renderedLastViewed = false;
- let numToDisplay = this.props.numPostsToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (let i = numToDisplay; i >= 0; i--) {
+ for (let i = order.length - 1; i >= 0; i--) {
const post = posts[order[i]];
const parentPost = posts[post.parent_id];
const prevPost = posts[order[i + 1]];
+ const postUserId = Utils.isSystemMessage(post) ? '' : post.user_id;
// If the post is a comment whose parent has been deleted, don't add it to the list.
if (parentPost && parentPost.state === Constants.POST_DELETED) {
@@ -83,32 +99,73 @@ export default class PostsView extends React.Component {
let hideProfilePic = false;
if (prevPost) {
- sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+ const postIsComment = Utils.isComment(post);
+ const prevPostIsComment = Utils.isComment(prevPost);
+ const postFromWebhook = Boolean(post.props && post.props.from_webhook);
+ const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
+ const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
+ let prevWebhookName = '';
+ if (prevPost.props && prevPost.props.override_username) {
+ prevWebhookName = prevPost.props.override_username;
+ }
+ let curWebhookName = '';
+ if (post.props && post.props.override_username) {
+ curWebhookName = post.props.override_username;
+ }
- sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+ // consider posts from the same user if:
+ // the previous post was made by the same user as the current post,
+ // the previous post was made within 5 minutes of the current post,
+ // the previous post and current post are both from webhooks or both not,
+ // the previous post and current post have the same webhook usernames
+ if (prevPostUserId === postUserId &&
+ post.create_at - prevPost.create_at <= 1000 * 60 * 5 &&
+ postFromWebhook === prevPostFromWebhook &&
+ prevWebhookName === curWebhookName) {
+ sameUser = true;
+ }
+
+ // consider posts from the same root if:
+ // the current post is a comment,
+ // the current post has the same root as the previous post
+ if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
+ sameRoot = true;
+ }
+
+ // consider posts from the same root if:
+ // the current post is not a comment,
+ // the previous post is not a comment,
+ // the previous post is from the same user
+ if (!postIsComment && !prevPostIsComment && sameUser) {
+ sameRoot = true;
+ }
// hide the profile pic if:
// the previous post was made by the same user as the current post,
// the previous post is not a comment,
// the current post is not a comment,
- // the current post is not from a webhook
- // and the previous post is not from a webhook
- if ((prevPost.user_id === post.user_id) &&
- !Utils.isComment(prevPost) &&
- !Utils.isComment(post) &&
- (!post.props || !post.props.from_webhook) &&
- (!prevPost.props || !prevPost.props.from_webhook)) {
+ // the previous post and current post are both from webhooks or both not,
+ // the previous post and current post have the same webhook usernames
+ if (prevPostUserId === postUserId &&
+ !prevPostIsComment &&
+ !postIsComment &&
+ postFromWebhook === prevPostFromWebhook &&
+ prevWebhookName === curWebhookName) {
hideProfilePic = true;
}
}
// check if it's the last comment in a consecutive string of comments on the same post
// it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+ const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+
+ const keyPrefix = post.id ? post.id : i;
+
+ const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
- var postCtl = (
+ const postCtl = (
<Post
- key={post.id + 'postKey'}
+ key={keyPrefix + 'postKey'}
ref={post.id}
sameUser={sameUser}
sameRoot={sameRoot}
@@ -117,6 +174,9 @@ export default class PostsView extends React.Component {
posts={posts}
hideProfilePic={hideProfilePic}
isLastComment={isLastComment}
+ shouldHighlight={shouldHighlight}
+ onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
+ displayNameType={this.state.displayNameType}
/>
);
@@ -133,7 +193,7 @@ export default class PostsView extends React.Component {
);
}
- if (post.user_id !== userId &&
+ if (postUserId !== userId &&
this.props.messageSeparatorTime !== 0 &&
post.create_at > this.props.messageSeparatorTime &&
!renderedLastViewed) {
@@ -178,9 +238,12 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
}
});
- } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => {
- const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
+ if (postNode == null) {
+ return;
+ }
postNode.scrollIntoView();
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
@@ -188,7 +251,7 @@ export default class PostsView extends React.Component {
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
}
});
- } else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) {
// If we are at the bottom then stay there
if (this.wasAtBottom) {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
@@ -204,7 +267,10 @@ export default class PostsView extends React.Component {
}
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
window.requestAnimationFrame(() => {
- this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ // Only need to jump if we added posts to the top.
+ if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
+ this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ }
});
}
}
@@ -212,35 +278,47 @@ export default class PostsView extends React.Component {
this.updateScrolling();
}
componentDidMount() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
componentDidUpdate() {
- this.updateScrolling();
+ if (this.props.postList != null) {
+ this.updateScrolling();
+ }
+ }
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.isActive && nextProps.isActive) {
+ this.updateState();
+ PreferenceStore.addChangeListener(this.updateState);
+ } else if (this.props.isActive && !nextProps.isActive) {
+ PreferenceStore.removeChangeListener(this.updateState);
+ }
}
- shouldComponentUpdate(nextProps) {
+ shouldComponentUpdate(nextProps, nextState) {
if (this.props.isActive !== nextProps.isActive) {
return true;
}
if (this.props.postList !== nextProps.postList) {
return true;
}
- if (this.props.scrollPost !== nextProps.scrollPost) {
+ if (this.props.scrollPostId !== nextProps.scrollPostId) {
return true;
}
if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
return true;
}
- if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
+ if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
return true;
}
- if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
+ if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) {
return true;
}
- if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) {
+ if (nextState.displayNameType !== this.state.displayNameType) {
return true;
}
@@ -249,7 +327,8 @@ export default class PostsView extends React.Component {
render() {
let posts = [];
let order = [];
- let moreMessages;
+ let moreMessagesTop;
+ let moreMessagesBottom;
let postElements;
let activeClass = 'inactive';
if (this.props.postList != null) {
@@ -257,10 +336,10 @@ export default class PostsView extends React.Component {
order = this.props.postList.order;
// Create intro message or top loadmore link
- if (order.length >= this.props.numPostsToDisplay) {
- moreMessages = (
+ if (this.props.showMoreMessagesTop) {
+ moreMessagesTop = (
<a
- ref='loadmore'
+ ref='loadmoretop'
className='more-messages-text theme'
href='#'
onClick={this.loadMorePostsTop}
@@ -269,7 +348,23 @@ export default class PostsView extends React.Component {
</a>
);
} else {
- moreMessages = this.props.introText;
+ moreMessagesTop = this.props.introText;
+ }
+
+ // Give option to load more posts at bottom if nessisary
+ if (this.props.showMoreMessagesBottom) {
+ moreMessagesBottom = (
+ <a
+ ref='loadmorebottom'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePostsBottom}
+ >
+ {'Load more messages'}
+ </a>
+ );
+ } else {
+ moreMessagesBottom = null;
}
// Create post elements
@@ -292,8 +387,9 @@ export default class PostsView extends React.Component {
ref='postlistcontent'
className='post-list__content'
>
- {moreMessages}
+ {moreMessagesTop}
{postElements}
+ {moreMessagesBottom}
</div>
</div>
</div>
@@ -306,11 +402,14 @@ PostsView.defaultProps = {
PostsView.propTypes = {
isActive: React.PropTypes.bool,
postList: React.PropTypes.object,
- scrollPost: React.PropTypes.string,
+ scrollPostId: React.PropTypes.string,
scrollType: React.PropTypes.number,
postViewScrolled: React.PropTypes.func.isRequired,
loadMorePostsTopClicked: React.PropTypes.func.isRequired,
- numPostsToDisplay: React.PropTypes.number,
+ loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
+ showMoreMessagesTop: React.PropTypes.bool,
+ showMoreMessagesBottom: React.PropTypes.bool,
introText: React.PropTypes.element,
- messageSeparatorTime: React.PropTypes.number
+ messageSeparatorTime: React.PropTypes.number,
+ postsToHighlight: React.PropTypes.object
};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
index 8b92a26a7..631bd1872 100644
--- a/web/react/components/posts_view_container.jsx
+++ b/web/react/components/posts_view_container.jsx
@@ -1,22 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const PostsView = require('./posts_view.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
-const ChannelInviteModal = require('./channel_invite_modal.jsx');
+import PostsView from './posts_view.jsx';
+import LoadingScreen from './loading_screen.jsx';
-const ChannelStore = require('../stores/channel_store.jsx');
-const PostStore = require('../stores/post_store.jsx');
+import ChannelStore from '../stores/channel_store.jsx';
+import PostStore from '../stores/post_store.jsx';
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
+import Constants from '../utils/constants.jsx';
-import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
+import {createChannelIntroMessage} from '../utils/channel_intro_messages.jsx';
export default class PostsViewContainer extends React.Component {
constructor() {
@@ -27,27 +23,26 @@ export default class PostsViewContainer extends React.Component {
this.onPostsChange = this.onPostsChange.bind(this);
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
- this.postsLoaded = this.postsLoaded.bind(this);
- this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
const currentChannelId = ChannelStore.getCurrentId();
const state = {
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
- scrollPost: null,
- numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ scrollPost: null
};
if (currentChannelId) {
Object.assign(state, {
currentChannelIndex: 0,
channels: [currentChannelId],
- postLists: [this.getChannelPosts(currentChannelId)]
+ postLists: [this.getChannelPosts(currentChannelId)],
+ atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
});
} else {
Object.assign(state, {
currentChannelIndex: null,
channels: [],
- postLists: []
+ postLists: [],
+ atTop: []
});
}
@@ -78,24 +73,21 @@ export default class PostsViewContainer extends React.Component {
});
break;
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
- this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN});
break;
}
}
onChannelChange() {
const postLists = this.state.postLists.slice();
+ const atTop = this.state.atTop.slice();
const channels = this.state.channels.slice();
const channelId = ChannelStore.getCurrentId();
// Has the channel really changed?
if (channelId === channels[this.state.currentChannelIndex]) {
- // Dirty hack
- this.forceUpdate();
return;
}
- PostStore.clearUnseenDeletedPosts(channelId);
-
let lastViewed = Number.MAX_VALUE;
const member = ChannelStore.getMember(channelId);
if (member != null) {
@@ -106,116 +98,48 @@ export default class PostsViewContainer extends React.Component {
if (newIndex === -1) {
newIndex = channels.length;
channels.push(channelId);
- postLists[newIndex] = this.getChannelPosts(channelId);
+ atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
}
+
+ // make sure we have the latest posts from the store
+ postLists[newIndex] = this.getChannelPosts(channelId);
+
this.setState({
currentChannelIndex: newIndex,
currentLastViewed: lastViewed,
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
channels,
- postLists});
+ postLists,
+ atTop});
}
onChannelLeave(id) {
const postLists = this.state.postLists.slice();
const channels = this.state.channels.slice();
+ const atTop = this.state.atTop.slice();
const index = channels.indexOf(id);
if (index !== -1) {
postLists.splice(index, 1);
channels.splice(index, 1);
+ atTop.splice(index, 1);
}
- this.setState({channels, postLists});
+ this.setState({channels, postLists, atTop});
}
onPostsChange() {
const channels = this.state.channels;
const postLists = this.state.postLists.slice();
- const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
+ const atTop = this.state.atTop.slice();
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const newPostsView = this.getChannelPosts(currentChannelId);
postLists[this.state.currentChannelIndex] = newPostsView;
- this.setState({postLists});
+ atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
+ this.setState({postLists, atTop});
}
getChannelPosts(id) {
- const postList = PostStore.getPosts(id);
-
- if (postList != null) {
- const deletedPosts = PostStore.getUnseenDeletedPosts(id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (const pid in deletedPosts) {
- if (deletedPosts.hasOwnProperty(pid)) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
- }
- }
-
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
- const pendingPostList = PostStore.getPendingPosts(id);
-
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (const ppid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(ppid)) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
- }
- }
-
- return postList;
+ return PostStore.getVisiblePosts(id);
}
loadMorePostsTop() {
- const postLists = this.state.postLists;
- const channels = this.state.channels;
- const currentChannelId = channels[this.state.currentChannelIndex];
- const currentPostList = postLists[this.state.currentChannelIndex];
-
- this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
-
- Client.getPostsPage(
- currentChannelId,
- currentPostList.order.length,
- Constants.POST_CHUNK_SIZE,
- this.postsLoaded,
- this.postsLoadedFailure
- );
- }
- postsLoaded(data) {
- if (!data) {
- return;
- }
-
- if (data.order.length === 0) {
- return;
- }
-
- const postLists = this.state.postLists;
- const currentPostList = postLists[this.state.currentChannelIndex];
- const channels = this.state.channels;
- const currentChannelId = channels[this.state.currentChannelIndex];
-
- var newPostList = {};
- newPostList.posts = Object.assign(currentPostList.posts, data.posts);
- newPostList.order = currentPostList.order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: currentChannelId,
- post_list: newPostList
- });
-
- Client.getProfiles();
- }
- postsLoadedFailure(err) {
- AsyncClient.dispatchError(err, 'getPosts');
+ EventHelpers.emitLoadMorePostsEvent();
}
handlePostsViewScroll(atBottom) {
if (atBottom) {
@@ -225,7 +149,7 @@ export default class PostsViewContainer extends React.Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
- if (Utils.areStatesEqual(this.state, nextState)) {
+ if (Utils.areObjectsEqual(this.state, nextState)) {
return false;
}
@@ -246,15 +170,17 @@ export default class PostsViewContainer extends React.Component {
isActive={isActive}
postList={postLists[i]}
scrollType={this.state.scrollType}
- scrollPost={this.state.scrollPost}
+ scrollPostId={this.state.scrollPost}
postViewScrolled={this.handlePostsViewScroll}
loadMorePostsTopClicked={this.loadMorePostsTop}
- numPostsToDisplay={this.state.numPostsToDisplay}
- introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null}
+ loadMorePostsBottomClicked={() => {}}
+ showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
+ showMoreMessagesBottom={false}
+ introText={channel ? createChannelIntroMessage(channel) : null}
messageSeparatorTime={this.state.currentLastViewed}
/>
);
- if ((!postLists[i] || !channel) && isActive) {
+ if (!postLists[i] && isActive) {
postListCtls.push(
<LoadingScreen
position='absolute'
@@ -267,10 +193,6 @@ export default class PostsViewContainer extends React.Component {
return (
<div id='post-list'>
{postListCtls}
- <ChannelInviteModal
- show={this.state.showInviteModal}
- onModalDismissed={() => this.setState({showInviteModal: false})}
- />
</div>
);
}
diff --git a/web/react/components/providers.json b/web/react/components/providers.json
new file mode 100644
index 000000000..b5899c225
--- /dev/null
+++ b/web/react/components/providers.json
@@ -0,0 +1,376 @@
+[
+ {
+ "patterns": [
+ "http://(?:www\\.)?xkcd\\.com/\\d+/?"
+ ],
+ "name": "XKCD",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://soundcloud.com/.*/.*"
+ ],
+ "name": "SoundCloud",
+ "height": 140
+ },
+ {
+ "patterns": [
+ "https?://(?:www\\.)?flickr\\.com/.*",
+ "https?://flic\\.kr/p/[a-zA-Z0-9]+"
+ ],
+ "name": "Flickr",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.ted\\.com/talks/.+\\.html"
+ ],
+ "name": "TED",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$"
+ ],
+ "name": "The Verge",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://.*\\.viddler\\.com/.*"
+ ],
+ "name": "Viddler",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$"
+ ],
+ "name": "The AV Club",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$"
+ ],
+ "name": "Wired",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.theonion\\.com/articles/[^/]+/?"
+ ],
+ "name": "The Onion",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://yfrog\\.com/[0-9a-zA-Z]+/?$"
+ ],
+ "name": "YFrog",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$"
+ ],
+ "name": "The Duffel Blog",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.clickhole\\.com/article/[^/]+/?"
+ ],
+ "name": "Clickhole",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+",
+ "http://skit.ch/[^/]+"
+ ],
+ "name": "Skitch",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(alpha|posts|photos)\\.app\\.net/.*"
+ ],
+ "name": "ADN",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)"
+ ],
+ "name": "Gist",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))",
+ "https?://db\\.tt/[a-zA-Z0-9]+"
+ ],
+ "name": "Dropbox",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?"
+ ],
+ "name": "Wikipedia",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www.traileraddict.com/trailer/[^/]+/trailer"
+ ],
+ "name": "TrailerAddict",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://lockerz\\.com/[sd]/\\d+"
+ ],
+ "name": "Lockerz",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://gifuk\\.com/s/[0-9a-f]{16}"
+ ],
+ "name": "GIFUK",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+"
+ ],
+ "name": "iTunes Movie Trailers",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://gfycat\\.com/([a-zA-Z]+)"
+ ],
+ "name": "Gfycat",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://bash\\.org/\\?(\\d+)"
+ ],
+ "name": "Bash.org",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$"
+ ],
+ "name": "Ars Technica",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://imgur\\.com/gallery/[0-9a-zA-Z]+"
+ ],
+ "name": "Imgur",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.asciiartfarts\\.com/[0-9]+\\.html"
+ ],
+ "name": "ASCII Art Farts",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+"
+ ],
+ "name": "Monoprice",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html"
+ ],
+ "name": "Boing Boing",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)",
+ "http://git\\.io/[_0-9a-zA-Z]+"
+ ],
+ "name": "Github Commit",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})"
+ ],
+ "name": "Spotify",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://path\\.com/p/([0-9a-zA-Z]+)$"
+ ],
+ "name": "Path",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www.funnyordie.com/videos/[^/]+/.+"
+ ],
+ "name": "Funny or Die",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://(?:www\\.)?twitpic\\.com/([^/]+)"
+ ],
+ "name": "Twitpic",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?"
+ ],
+ "name": "GiantBomb",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+"
+ ],
+ "name": "Beer Advocate",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://(?:www\\.)?imdb.com/title/(tt\\d+)"
+ ],
+ "name": "IMDB",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$"
+ ],
+ "name": "CloudApp",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://clyp\\.it/.*"
+ ],
+ "name": "Clyp",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.hulu\\.com/watch/.*"
+ ],
+ "name": "Hulu",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$",
+ "https?://t\\.co/[a-zA-Z0-9]+"
+ ],
+ "name": "Twitter",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www\\.)?vimeo\\.com/.+"
+ ],
+ "name": "Vimeo",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)",
+ "http://amzn\\.com/([^/]+)"
+ ],
+ "name": "Amazon",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://qik\\.com/video/.*"
+ ],
+ "name": "Qik",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/?",
+ "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?",
+ "http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+"
+ ],
+ "name": "Rdio",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.slideshare\\.net/.*/.*"
+ ],
+ "name": "SlideShare",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://imgur\\.com/([0-9a-zA-Z]+)$"
+ ],
+ "name": "Imgur",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://instagr(?:\\.am|am\\.com)/p/.+"
+ ],
+ "name": "Instagram",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+",
+ "http://tl\\.gd/[^/]+"
+ ],
+ "name": "Twitlonger",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://vine.co/v/[a-zA-Z0-9]+"
+ ],
+ "name": "Vine",
+ "height": 490
+ },
+ {
+ "patterns": [
+ "http://www\\.urbandictionary\\.com/define\\.php\\?term=.+"
+ ],
+ "name": "Urban Dictionary",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "http://picplz\\.com/user/[^/]+/pic/[^/]+"
+ ],
+ "name": "Picplz",
+ "height": 110
+ },
+ {
+ "patterns": [
+ "https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$",
+ "https?://pic\\.twitter\\.com/.+"
+ ],
+ "name": "Twitter",
+ "height": 110
+ }
+]
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index c40409dcc..f49b33f73 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -1,22 +1,58 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
+import * as Client from '../utils/client.jsx';
+import ModalStore from '../stores/modal_store.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
export default class RegisterAppModal extends React.Component {
constructor() {
super();
- this.register = this.register.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
this.onHide = this.onHide.bind(this);
this.save = this.save.bind(this);
+ this.updateShow = this.updateShow.bind(this);
- this.state = {clientId: '', clientSecret: '', saved: false};
+ this.state = {
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ show: false
+ };
}
componentDidMount() {
- $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide);
+ ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ componentWillUnmount() {
+ ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
+ }
+ updateShow(show) {
+ if (!show) {
+ if (this.state.clientId !== '' && !this.state.saved) {
+ return;
+ }
+
+ this.setState({
+ clientId: '',
+ clientSecret: '',
+ saved: false,
+ homepageError: null,
+ callbackError: null,
+ serverError: null,
+ nameError: null
+ });
+ }
+
+ this.setState({show});
}
- register() {
+ handleSubmit(e) {
+ e.preventDefault();
+
var state = this.state;
state.serverError = null;
@@ -94,6 +130,7 @@ export default class RegisterAppModal extends React.Component {
}
var body = '';
+ var footer = '';
if (this.state.clientId === '') {
body = (
<div className='settings-modal'>
@@ -148,24 +185,29 @@ export default class RegisterAppModal extends React.Component {
</div>
</div>
{serverError}
- <hr />
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
</div>
);
+
+ footer = (
+ <div>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={() => this.updateShow(false)}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Register'}
+ </button>
+ </div>
+ );
} else {
var btnClass = ' disabled';
if (this.state.saved) {
@@ -173,17 +215,35 @@ export default class RegisterAppModal extends React.Component {
}
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>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Your Application Credentials'}</h4>
<br/>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Client ID'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientId}
+ readOnly='true'
+ />
+ </div>
+ </div>
<br/>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Client Secret'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ value={this.state.clientSecret}
+ readOnly='true'
+ />
+ </div>
+ </div>
<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>
+ <strong>{'Save these somewhere SAFE and SECURE. Treat your Client ID as your app\'s username and your Client Secret as the app\'s password.'}</strong>
<br/>
<br/>
<div className='checkbox'>
@@ -192,56 +252,50 @@ export default class RegisterAppModal extends React.Component {
ref='save'
type='checkbox'
checked={this.state.saved}
- onClick={this.save}
- >
- {'I have saved both my Client Id and Client Secret somewhere safe'}
- </input>
+ onChange={this.save}
+ />
+ {'I have saved both my Client Id and Client Secret somewhere safe'}
</label>
</div>
- <a
- className={'btn btn-sm btn-primary pull-right' + btnClass}
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Close'}
- </a>
</div>
);
+
+ footer = (
+ <a
+ className={'btn btn-sm btn-primary pull-right' + btnClass}
+ href='#'
+ onClick={(e) => {
+ e.preventDefault();
+ this.updateShow(false);
+ }}
+ >
+ {'Close'}
+ </a>
+ );
}
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>
+ <span>
+ <Modal
+ show={this.state.show}
+ onHide={() => this.updateShow(false)}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Developer Applications'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ {body}
+ </Modal.Body>
+ <Modal.Footer>
+ {footer}
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
);
}
}
diff --git a/web/react/components/removed_from_channel_modal.jsx b/web/react/components/removed_from_channel_modal.jsx
index 7cf0a2ef1..69d038c22 100644
--- a/web/react/components/removed_from_channel_modal.jsx
+++ b/web/react/components/removed_from_channel_modal.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var utils = require('../utils/utils.jsx');
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import * as utils from '../utils/utils.jsx';
export default class RemovedFromChannelModal extends React.Component {
constructor(props) {
diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx
index 9fb3af035..c16216c68 100644
--- a/web/react/components/rename_channel_modal.jsx
+++ b/web/react/components/rename_channel_modal.jsx
@@ -1,10 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import Constants from '../utils/constants.jsx';
export default class RenameChannelModal extends React.Component {
constructor(props) {
@@ -36,10 +37,10 @@ export default class RenameChannelModal extends React.Component {
return;
}
- let channel = ChannelStore.get(this.state.channelId);
+ const channel = ChannelStore.get(this.state.channelId);
const oldName = channel.name;
const oldDisplayName = channel.displayName;
- let state = {serverError: ''};
+ const state = {serverError: ''};
channel.display_name = this.state.displayName.trim();
if (!channel.display_name) {
@@ -60,7 +61,7 @@ export default class RenameChannelModal extends React.Component {
state.nameError = 'This field must be less than 22 characters';
state.invalid = true;
} else {
- let cleanedName = Utils.cleanUpUrlable(channel.name);
+ const cleanedName = Utils.cleanUpUrlable(channel.name);
if (cleanedName === channel.name) {
state.nameError = '';
} else {
@@ -76,7 +77,7 @@ export default class RenameChannelModal extends React.Component {
}
Client.updateChannel(channel,
- function handleUpdateSuccess() {
+ () => {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
AsyncClient.getChannel(channel.id);
@@ -84,12 +85,12 @@ export default class RenameChannelModal extends React.Component {
ReactDOM.findDOMNode(this.refs.displayName).value = '';
ReactDOM.findDOMNode(this.refs.channelName).value = '';
- }.bind(this),
- function handleUpdateError(err) {
+ },
+ (err) => {
state.serverError = err.message;
state.invalid = true;
this.setState(state);
- }.bind(this)
+ }
);
}
onNameChange() {
@@ -99,10 +100,12 @@ export default class RenameChannelModal extends React.Component {
this.setState({displayName: ReactDOM.findDOMNode(this.refs.displayName).value});
}
displayNameKeyUp() {
- const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
- const channelName = Utils.cleanUpUrlable(displayName);
- ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
- this.setState({channelName: channelName});
+ if (this.state.channelName !== Constants.DEFAULT_CHANNEL) {
+ const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim();
+ const channelName = Utils.cleanUpUrlable(displayName);
+ ReactDOM.findDOMNode(this.refs.channelName).value = channelName;
+ this.setState({channelName: channelName});
+ }
}
handleClose() {
this.setState({
@@ -150,6 +153,15 @@ export default class RenameChannelModal extends React.Component {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
+ let handleInputLabel = 'Handle';
+ let handleInputClass = 'form-control';
+ let readOnlyHandleInput = false;
+ if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
+ handleInputLabel += ' - Cannot be changed for the default channel';
+ handleInputClass += ' disabled-input';
+ readOnlyHandleInput = true;
+ }
+
return (
<div
className='modal fade'
@@ -167,15 +179,15 @@ export default class RenameChannelModal 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'>Rename Channel</h4>
+ <h4 className='modal-title'>{'Rename Channel'}</h4>
</div>
<form role='form'>
<div className='modal-body'>
<div className={displayNameClass}>
- <label className='control-label'>Display Name</label>
+ <label className='control-label'>{'Display Name'}</label>
<input
onKeyUp={this.displayNameKeyUp}
onChange={this.onDisplayNameChange}
@@ -190,15 +202,16 @@ export default class RenameChannelModal extends React.Component {
{displayNameError}
</div>
<div className={nameClass}>
- <label className='control-label'>Handle</label>
+ <label className='control-label'>{handleInputLabel}</label>
<input
onChange={this.onNameChange}
type='text'
- className='form-control'
+ className={handleInputClass}
ref='channelName'
placeholder='lowercase alphanumeric&#39;s only'
value={this.state.channelName}
maxLength='64'
+ readOnly={readOnlyHandleInput}
/>
{nameError}
</div>
@@ -210,14 +223,14 @@ export default class RenameChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Cancel
+ {'Cancel'}
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
- Save
+ {'Save'}
</button>
</div>
</form>
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 8c6324c72..7aae5177e 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -1,19 +1,20 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserProfile = require('./user_profile.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Utils = require('../utils/utils.jsx');
-var Constants = require('../utils/constants.jsx');
-var FileAttachmentList = require('./file_attachment_list.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
+import PostStore from '../stores/post_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import UserProfile from './user_profile.jsx';
+import UserStore from '../stores/user_store.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+import FileAttachmentList from './file_attachment_list.jsx';
+import * as Client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
var ActionTypes = Constants.ActionTypes;
-var TextFormatting = require('../utils/text_formatting.jsx');
-var twemoji = require('twemoji');
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import twemoji from 'twemoji';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
export default class RhsComment extends React.Component {
constructor(props) {
@@ -61,7 +62,7 @@ export default class RhsComment extends React.Component {
this.parseEmojis();
}
shouldComponentUpdate(nextProps) {
- if (!Utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
@@ -114,12 +115,7 @@ export default class RhsComment extends React.Component {
<a
href='#'
role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title='Comment'
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={0}
+ onClick={() => EventHelpers.showDeletePostModal(post, 0)}
>
{'Delete'}
</a>
@@ -135,7 +131,7 @@ export default class RhsComment extends React.Component {
<div className='dropdown'>
<a
href='#'
- className='dropdown-toggle theme'
+ className='post__dropdown dropdown-toggle'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -197,38 +193,39 @@ export default class RhsComment extends React.Component {
return (
<div className={'post ' + currentUserCss}>
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
- height='36'
- width='36'
- />
- </div>
<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-profile-time'>
- {Utils.displayCommentDateTime(post.create_at)}
- </time>
- </li>
- <li className='post-header-col post-header__reply'>
- {dropdown}
- </li>
- </ul>
- <div className='post-body'>
- <div className={postClass}>
- {loading}
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
+ <div className='post__img'>
+ <img
+ src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
+ height='36'
+ width='36'
+ />
+ </div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'>
+ <strong><UserProfile userId={post.user_id} /></strong>
+ </li>
+ <li className='col'>
+ <time className='post__time'>
+ {Utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
+ <li className='col col__reply'>
+ {dropdown}
+ </li>
+ </ul>
+ <div className='post__body'>
+ <div className={postClass}>
+ {loading}
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ </div>
+ {fileAttachment}
</div>
- {fileAttachment}
</div>
</div>
</div>
diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx
index 856eea91d..990b33eb5 100644
--- a/web/react/components/rhs_header_post.jsx
+++ b/web/react/components/rhs_header_post.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const Constants = require('../utils/constants.jsx');
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
export default class RhsHeaderPost extends React.Component {
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index e3b023841..dd9a793be 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -1,15 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
-var Constants = require('../utils/constants.jsx');
-const PostBodyAdditionalContent = require('./post_body_additional_content.jsx');
+import ChannelStore from '../stores/channel_store.jsx';
+import UserProfile from './user_profile.jsx';
+import UserStore from '../stores/user_store.jsx';
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import * as utils from '../utils/utils.jsx';
+import * as Emoji from '../utils/emoticons.jsx';
+import FileAttachmentList from './file_attachment_list.jsx';
+import twemoji from 'twemoji';
+import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+
+import Constants from '../utils/constants.jsx';
export default class RhsRootPost extends React.Component {
constructor(props) {
@@ -20,13 +23,17 @@ export default class RhsRootPost extends React.Component {
this.state = {};
}
parseEmojis() {
- twemoji.parse(ReactDOM.findDOMNode(this), {size: Constants.EMOJI_SIZE});
+ twemoji.parse(ReactDOM.findDOMNode(this), {
+ className: 'emoji twemoji',
+ base: '',
+ folder: Emoji.getImagePathForEmoticon()
+ });
}
componentDidMount() {
this.parseEmojis();
}
shouldComponentUpdate(nextProps) {
- if (!utils.areStatesEqual(nextProps.post, this.props.post)) {
+ if (!utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
}
@@ -37,7 +44,9 @@ export default class RhsRootPost extends React.Component {
}
render() {
var post = this.props.post;
- var isOwner = UserStore.getCurrentId() === post.user_id;
+ var currentUser = UserStore.getCurrentUser();
+ var isOwner = currentUser.id === post.user_id;
+ var isAdmin = utils.isAdmin(currentUser.roles);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -51,6 +60,11 @@ export default class RhsRootPost extends React.Component {
currentUserCss = 'current--user';
}
+ var systemMessageClass = '';
+ if (utils.isSystemMessage(post)) {
+ systemMessageClass = 'post--system';
+ }
+
var channelName;
if (channel) {
if (channel.type === 'D') {
@@ -60,12 +74,55 @@ export default class RhsRootPost extends React.Component {
}
}
- var ownerOptions;
+ var dropdownContents = [];
+
if (isOwner) {
- ownerOptions = (
- <div>
- <a href='#'
- className='dropdown-toggle theme'
+ dropdownContents.push(
+ <li
+ key='rhs-root-edit'
+ role='presentation'
+ >
+ <a
+ href='#'
+ role='menuitem'
+ data-toggle='modal'
+ data-target='#edit_post'
+ data-refocusid='#reply_textbox'
+ data-title={type}
+ data-message={post.message}
+ data-postid={post.id}
+ data-channelid={post.channel_id}
+ >
+ {'Edit'}
+ </a>
+ </li>
+ );
+ }
+
+ if (isOwner || isAdmin) {
+ dropdownContents.push(
+ <li
+ key='rhs-root-delete'
+ role='presentation'
+ >
+ <a
+ href='#'
+ role='menuitem'
+ onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)}
+ >
+ {'Delete'}
+ </a>
+ </li>
+ );
+ }
+
+ var rootOptions = '';
+ if (dropdownContents.length > 0) {
+ rootOptions = (
+ <div className='dropdown'>
+ <a
+ href='#'
+ className='post__dropdown dropdown-toggle'
type='button'
data-toggle='dropdown'
aria-expanded='false'
@@ -74,35 +131,7 @@ export default class RhsRootPost extends React.Component {
className='dropdown-menu'
role='menu'
>
- <li role='presentation'>
- <a
- href='#'
- role='menuitem'
- data-toggle='modal'
- data-target='#edit_post'
- data-refocusid='#reply_textbox'
- data-title={type}
- data-message={post.message}
- data-postid={post.id}
- data-channelid={post.channel_id}
- >
- Edit
- </a>
- </li>
- <li role='presentation'>
- <a
- href='#'
- role='menuitem'
- data-toggle='modal'
- data-target='#delete_post'
- data-title={type}
- data-postid={post.id}
- data-channelid={post.channel_id}
- data-comments={this.props.commentCount}
- >
- Delete
- </a>
- </li>
+ {dropdownContents}
</ul>
</div>
);
@@ -133,7 +162,16 @@ export default class RhsRootPost extends React.Component {
);
}
- botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ } else if (utils.isSystemMessage(post)) {
+ userProfile = (
+ <UserProfile
+ userId={''}
+ overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME}
+ overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
+ disablePopover={true}
+ />
+ );
}
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
@@ -141,50 +179,52 @@ export default class RhsRootPost extends React.Component {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
+ } else if (utils.isSystemMessage(post)) {
+ src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
}
const profilePic = (
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={src}
- height='36'
- width='36'
- />
- </div>
+ <img
+ className='post-profile-img'
+ src={src}
+ height='36'
+ width='36'
+ />
);
return (
- <div className={'post post--root ' + currentUserCss}>
+ <div className={'post post--root ' + currentUserCss + ' ' + systemMessageClass}>
<div className='post-right-channel__name'>{channelName}</div>
- <div className='post-profile-img__container'>
- {profilePic}
- </div>
<div className='post__content'>
- <ul className='post-header'>
- <li className='post-header-col'><strong>{userProfile}</strong></li>
- {botIndicator}
- <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}
- </div>
- </li>
- </ul>
- <div className='post-body'>
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
- <PostBodyAdditionalContent
- post={post}
- />
- {fileAttachment}
+ <div className='post__img'>
+ {profilePic}
+ </div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'>{userProfile}</li>
+ {botIndicator}
+ <li className='col'>
+ <time className='post__time'>
+ {utils.displayCommentDateTime(post.create_at)}
+ </time>
+ </li>
+ <li className='col col__reply'>
+ <div>
+ {rootOptions}
+ </div>
+ </li>
+ </ul>
+ <div className='post__body'>
+ <div
+ ref='message_holder'
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
+ />
+ <PostBodyAdditionalContent
+ post={post}
+ />
+ {fileAttachment}
+ </div>
</div>
</div>
<hr />
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index fe57bed28..2edcd8b37 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -1,17 +1,17 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var PreferenceStore = require('../stores/preference_store.jsx');
-var Utils = require('../utils/utils.jsx');
-var SearchBox = require('./search_bar.jsx');
-var CreateComment = require('./create_comment.jsx');
-var RhsHeaderPost = require('./rhs_header_post.jsx');
-var RootPost = require('./rhs_root_post.jsx');
-var Comment = require('./rhs_comment.jsx');
-var Constants = require('../utils/constants.jsx');
-var FileUploadOverlay = require('./file_upload_overlay.jsx');
+import PostStore from '../stores/post_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+import SearchBox from './search_bar.jsx';
+import CreateComment from './create_comment.jsx';
+import RhsHeaderPost from './rhs_header_post.jsx';
+import RootPost from './rhs_root_post.jsx';
+import Comment from './rhs_comment.jsx';
+import Constants from '../utils/constants.jsx';
+import FileUploadOverlay from './file_upload_overlay.jsx';
export default class RhsThread extends React.Component {
constructor(props) {
@@ -82,7 +82,7 @@ export default class RhsThread extends React.Component {
}
onChange() {
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -94,14 +94,22 @@ export default class RhsThread extends React.Component {
return;
}
- var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id);
+ var currentPosts = PostStore.getVisiblePosts(currentSelected.posts[currentSelected.order[0]].channel_id);
if (!currentPosts || currentPosts.order.length === 0) {
return;
}
if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) {
- currentSelected.posts = {};
+ for (var key in currentSelected.posts) {
+ if (currentSelected.posts.hasOwnProperty(key)) {
+ var post = currentSelected.posts[key];
+ if (post.pending_post_id) {
+ Reflect.deleteProperty(currentSelected.posts, key);
+ }
+ }
+ }
+
for (var postId in currentPosts.posts) {
if (currentPosts.posts.hasOwnProperty(postId)) {
currentSelected.posts[postId] = currentPosts.posts[postId];
@@ -112,13 +120,11 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
resize() {
- var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
- $('.post-right__scroll').css('height', height + 'px');
$('.post-right__scroll').scrollTop(100000);
if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar();
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
deleted file mode 100644
index d245c6bac..000000000
--- a/web/react/components/search_autocomplete.jsx
+++ /dev/null
@@ -1,339 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const ChannelStore = require('../stores/channel_store.jsx');
-const KeyCodes = require('../utils/constants.jsx').KeyCodes;
-const Popover = ReactBootstrap.Popover;
-const UserStore = require('../stores/user_store.jsx');
-const Utils = require('../utils/utils.jsx');
-const Constants = require('../utils/constants.jsx');
-
-const patterns = new Map([
- ['channels', /\b(?:in|channel):\s*(\S*)$/i],
- ['users', /\bfrom:\s*(\S*)$/i]
-]);
-
-export default class SearchAutocomplete extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
- this.handleDocumentClick = this.handleDocumentClick.bind(this);
- this.handleInputChange = this.handleInputChange.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
-
- this.completeWord = this.completeWord.bind(this);
- this.getSelection = this.getSelection.bind(this);
- this.scrollToItem = this.scrollToItem.bind(this);
- this.updateSuggestions = this.updateSuggestions.bind(this);
-
- this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this);
- this.renderUserSuggestion = this.renderUserSuggestion.bind(this);
-
- this.state = {
- show: false,
- mode: '',
- filter: '',
- selection: 0,
- suggestions: new Map()
- };
- }
-
- componentDidMount() {
- $(document).on('click', this.handleDocumentClick);
- }
-
- componentDidUpdate(prevProps, prevState) {
- const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
-
- if (this.state.show) {
- if (!prevState.show) {
- content.perfectScrollbar();
- content.css('max-height', $(window).height() - 200);
- }
-
- // keep the keyboard selection visible when scrolling
- this.scrollToItem(this.getSelection());
- }
- }
-
- componentWillUnmount() {
- $(document).off('click', this.handleDocumentClick);
- }
-
- handleClick(value) {
- this.completeWord(value);
- }
-
- handleDocumentClick(e) {
- const container = $(ReactDOM.findDOMNode(this.refs.searchPopover));
-
- if (!(container.is(e.target) || container.has(e.target).length > 0)) {
- this.setState({
- show: false
- });
- }
- }
-
- handleInputChange(textbox, text) {
- const caret = Utils.getCaretPosition(textbox);
- const preText = text.substring(0, caret);
-
- let mode = '';
- let filter = '';
- for (const [modeForPattern, pattern] of patterns) {
- const result = pattern.exec(preText);
-
- if (result) {
- mode = modeForPattern;
- filter = result[1];
- break;
- }
- }
-
- if (mode !== this.state.mode || filter !== this.state.filter) {
- this.updateSuggestions(mode, filter);
- }
-
- this.setState({
- mode,
- filter,
- show: mode || filter
- });
- }
-
- handleKeyDown(e) {
- if (!this.state.show || this.state.suggestions.length === 0) {
- return;
- }
-
- if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
- e.preventDefault();
-
- let selection = this.state.selection;
-
- if (e.which === KeyCodes.UP) {
- selection -= 1;
- } else {
- selection += 1;
- }
-
- if (selection >= 0 && selection < this.state.suggestions.length) {
- this.setState({
- selection
- });
- }
- } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
- e.preventDefault();
-
- this.completeWord(this.getSelection());
- }
- }
-
- completeWord(value) {
- // add a space so that anything else typed doesn't interfere with the search flag
- this.props.completeWord(this.state.filter, value + ' ');
-
- this.setState({
- show: false,
- mode: '',
- filter: '',
- selection: 0
- });
- }
-
- getSelection() {
- if (this.state.mode === 'channels') {
- return this.state.suggestions[this.state.selection].name;
- } else if (this.state.mode === 'users') {
- return this.state.suggestions[this.state.selection].username;
- }
-
- return '';
- }
-
- scrollToItem(itemName) {
- const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
- const visibleContentHeight = content[0].clientHeight;
- const actualContentHeight = content[0].scrollHeight;
-
- if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) {
- const contentTop = content.scrollTop();
- const contentTopPadding = parseInt(content.css('padding-top'), 10);
- const contentBottomPadding = parseInt(content.css('padding-top'), 10);
-
- const item = $(this.refs[itemName]);
- const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
- const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
-
- if (itemTop - contentTopPadding < contentTop) {
- // the item is off the top of the visible space
- content.scrollTop(itemTop - contentTopPadding);
- } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
- // the item has gone off the bottom of the visible space
- content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
- }
- }
- }
-
- updateSuggestions(mode, filter) {
- let suggestions = [];
-
- if (mode === 'channels') {
- let channels = ChannelStore.getAll();
-
- if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
- } else {
- // don't show direct channels
- channels = channels.filter((channel) => channel.type !== 'D');
- }
-
- channels.sort((a, b) => {
- // put public channels first and then sort alphabebetically
- if (a.type === b.type) {
- return a.name.localeCompare(b.name);
- } else if (a.type === Constants.OPEN_CHANNEL) {
- return -1;
- }
-
- return 1;
- });
-
- suggestions = channels;
- } else if (mode === 'users') {
- let users = UserStore.getActiveOnlyProfileList();
-
- if (filter) {
- users = users.filter((user) => user.username.startsWith(filter));
- }
-
- users.sort((a, b) => a.username.localeCompare(b.username));
-
- suggestions = users;
- }
-
- let selection = this.state.selection;
-
- // keep the same user/channel selected if it's still visible as a suggestion
- if (selection > 0 && this.state.suggestions.length > 0) {
- // we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
- const currentSelectionId = this.state.suggestions[selection].id;
- let found = false;
-
- for (let i = 0; i < suggestions.length; i++) {
- if (suggestions[i].id === currentSelectionId) {
- selection = i;
- found = true;
-
- break;
- }
- }
-
- if (!found) {
- selection = 0;
- }
- } else {
- selection = 0;
- }
-
- this.setState({
- suggestions,
- selection
- });
- }
-
- renderChannelSuggestion(channel) {
- let className = 'search-autocomplete__item';
- if (channel.name === this.getSelection()) {
- className += ' selected';
- }
-
- return (
- <div
- key={channel.name}
- ref={channel.name}
- onClick={this.handleClick.bind(this, channel.name)}
- className={className}
- >
- {channel.name}
- </div>
- );
- }
-
- renderUserSuggestion(user) {
- let className = 'search-autocomplete__item';
- if (user.username === this.getSelection()) {
- className += ' selected';
- }
-
- return (
- <div
- key={user.username}
- ref={user.username}
- onClick={this.handleClick.bind(this, user.username)}
- className={className}
- >
- <img
- className='profile-img rounded'
- src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
- />
- {user.username}
- </div>
- );
- }
-
- render() {
- if (!this.state.show || this.state.suggestions.length === 0) {
- return null;
- }
-
- let suggestions = [];
-
- if (this.state.mode === 'channels') {
- const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
- if (publicChannels.length > 0) {
- suggestions.push(
- <div
- key='public-channel-divider'
- className='search-autocomplete__divider'
- >
- <span>{'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}</span>
- </div>
- );
- suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion));
- }
-
- const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
- if (privateChannels.length > 0) {
- suggestions.push(
- <div
- key='private-channel-divider'
- className='search-autocomplete__divider'
- >
- <span>{'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}</span>
- </div>
- );
- suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion));
- }
- } else if (this.state.mode === 'users') {
- suggestions = this.state.suggestions.map(this.renderUserSuggestion);
- }
-
- return (
- <Popover
- ref='searchPopover'
- onShow={this.componentDidMount}
- id='search-autocomplete__popover'
- className='search-help-popover autocomplete visible'
- placement='bottom'
- >
- {suggestions}
- </Popover>
- );
- }
-}
-
-SearchAutocomplete.propTypes = {
- completeWord: React.PropTypes.func.isRequired
-};
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 90865475b..77c9e39b9 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -1,15 +1,18 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var SearchStore = require('../stores/search_store.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var utils = require('../utils/utils.jsx');
-var Constants = require('../utils/constants.jsx');
+import * as client from '../utils/client.jsx';
+import * as AsyncClient from '../utils/async_client.jsx';
+import SearchStore from '../stores/search_store.jsx';
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import SuggestionBox from './suggestion/suggestion_box.jsx';
+import SearchChannelProvider from './suggestion/search_channel_provider.jsx';
+import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
+import SearchUserProvider from './suggestion/search_user_provider.jsx';
+import * as utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
var ActionTypes = Constants.ActionTypes;
var Popover = ReactBootstrap.Popover;
-var SearchAutocomplete = require('./search_autocomplete.jsx');
export default class SearchBar extends React.Component {
constructor() {
@@ -17,17 +20,17 @@ export default class SearchBar extends React.Component {
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleUserFocus = this.handleUserFocus.bind(this);
this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
- this.completeWord = this.completeWord.bind(this);
const state = this.getSearchTermStateFromStores();
state.focused = false;
this.state = state;
+
+ this.suggestionProviders = [new SearchChannelProvider(), new SearchUserProvider()];
}
getSearchTermStateFromStores() {
var term = SearchStore.getSearchTerm() || '';
@@ -46,7 +49,7 @@ export default class SearchBar extends React.Component {
onListenerChange(doSearch, isMentionSearch) {
if (this.mounted) {
var newState = this.getSearchTermStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
if (doSearch) {
@@ -77,18 +80,11 @@ export default class SearchBar extends React.Component {
results: null
});
}
- handleKeyDown(e) {
- if (this.refs.autocomplete) {
- this.refs.autocomplete.handleKeyDown(e);
- }
- }
- handleUserInput(e) {
- var term = e.target.value;
+ handleUserInput(text) {
+ var term = text;
SearchStore.storeSearchTerm(term);
SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
-
- this.refs.autocomplete.handleInputChange(e.target, term);
}
handleUserBlur() {
this.setState({focused: false});
@@ -128,23 +124,6 @@ export default class SearchBar extends React.Component {
this.performSearch(this.state.searchTerm.trim());
}
- completeWord(partialWord, word) {
- const textbox = ReactDOM.findDOMNode(this.refs.search);
- let text = textbox.value;
-
- const caret = utils.getCaretPosition(textbox);
- const preText = text.substring(0, caret - partialWord.length);
- const postText = text.substring(caret);
- text = preText + word + postText;
-
- textbox.value = text;
- utils.setCaretPosition(textbox, preText.length + word.length);
-
- SearchStore.storeSearchTerm(text);
- SearchStore.emitSearchTermChange(false);
- this.setState({searchTerm: text});
- }
-
render() {
var isSearching = null;
if (this.state.isSearching) {
@@ -178,22 +157,18 @@ export default class SearchBar extends React.Component {
autoComplete='off'
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
- <input
- type='text'
+ <SuggestionBox
ref='search'
className='form-control search-bar'
placeholder='Search'
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
onBlur={this.handleUserBlur}
- onChange={this.handleUserInput}
- onKeyDown={this.handleKeyDown}
+ onUserInput={this.handleUserInput}
+ listComponent={SearchSuggestionList}
+ providers={this.suggestionProviders}
/>
{isSearching}
- <SearchAutocomplete
- ref='autocomplete'
- completeWord={this.completeWord}
- />
<Popover
id='searchbar-help-popup'
placement='bottom'
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index b56a7b006..141181701 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SearchStore = require('../stores/search_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var SearchBox = require('./search_bar.jsx');
-var Utils = require('../utils/utils.jsx');
-var SearchResultsHeader = require('./search_results_header.jsx');
-var SearchResultsItem = require('./search_results_item.jsx');
+import SearchStore from '../stores/search_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import SearchBox from './search_bar.jsx';
+import * as Utils from '../utils/utils.jsx';
+import SearchResultsHeader from './search_results_header.jsx';
+import SearchResultsItem from './search_results_item.jsx';
function getStateFromStores() {
return {results: SearchStore.getSearchResults()};
@@ -55,15 +55,13 @@ export default class SearchResults extends React.Component {
onChange() {
if (this.mounted) {
var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
}
resize() {
- var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
- $('#search-items-container').css('height', height + 'px');
$('#search-items-container').scrollTop(0);
if (this.state.windowWidth > 768) {
$('#search-items-container').perfectScrollbar();
@@ -82,14 +80,27 @@ export default class SearchResults extends React.Component {
var ctls = null;
- if (noResults) {
+ if (!searchTerm && noResults) {
+ ctls = (
+ <div className='sidebar--right__subheader'>
+ <ul>
+ <li>
+ {'Use '}<b>{'"quotation marks"'}</b>{' to search for phrases'}
+ </li>
+ <li>
+ {'Use '}<b>{'from:'}</b>{' to find posts from specific users and '}<b>{'in:'}</b>{' to find posts in specific channels'}
+ </li>
+ </ul>
+ </div>
+ );
+ } else if (noResults) {
ctls =
(
<div className='sidebar--right__subheader'>
<h4>{'NO RESULTS'}</h4>
<ul>
- <li>If you're searching a partial phrase (ex. searching "rea", looking for "reach" or "reaction"), append a * to your search term</li>
- <li>Due to the volume of results, two letter searches and common words like "this", "a" and "is" won't appear in search results</li>
+ <li>{'If you\'re searching a partial phrase (ex. searching "rea", looking for "reach" or "reaction"), append a * to your search term'}</li>
+ <li>{'Due to the volume of results, two letter searches and common words like "this", "a" and "is" won\'t appear in search results'}</li>
</ul>
</div>
);
diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx
index fdd449c2d..581976494 100644
--- a/web/react/components/search_results_header.jsx
+++ b/web/react/components/search_results_header.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from '../utils/constants.jsx';
var ActionTypes = Constants.ActionTypes;
export default class SearchResultsHeader extends React.Component {
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index a8bd4db2c..f71abf971 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -1,54 +1,38 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SearchStore = require('../stores/search_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var UserProfile = require('./user_profile.jsx');
-var utils = require('../utils/utils.jsx');
-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;
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import UserProfile from './user_profile.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as utils from '../utils/utils.jsx';
+import * as TextFormatting from '../utils/text_formatting.jsx';
+
+import Constants from '../utils/constants.jsx';
export default class SearchResultsItem extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
+ this.handleFocusRHSClick = this.handleFocusRHSClick.bind(this);
}
handleClick(e) {
e.preventDefault();
- var self = this;
-
- client.getPost(
- this.props.post.channel_id,
- this.props.post.id,
- function success(data) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST_SELECTED,
- post_list: data,
- from_search: SearchStore.getSearchTerm()
- });
+ EventHelpers.emitPostFocusEvent(this.props.post.id);
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_SEARCH,
- results: null,
- is_mention_search: self.props.isMentionSearch
- });
- },
- function success(err) {
- AsyncClient.dispatchError(err, 'getPost');
- }
- );
+ if ($(window).width() < 768) {
+ $('.sidebar--right').removeClass('move--left');
+ $('.inner__wrap').removeClass('move--left');
+ }
+ }
- var postChannel = ChannelStore.get(this.props.post.channel_id);
+ handleFocusRHSClick(e) {
+ e.preventDefault();
- utils.switchChannel(postChannel);
+ EventHelpers.emitPostFocusRightHandSideEvent(this.props.post);
}
render() {
@@ -71,32 +55,52 @@ export default class SearchResultsItem extends React.Component {
return (
<div
className='search-item-container post'
- onClick={this.handleClick}
>
<div className='search-channel__name'>{channelName}</div>
- <div className='post-profile-img__container'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
- height='36'
- width='36'
- />
- </div>
<div className='post__content'>
- <ul className='post-header'>
- <li className='post-header-col'><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
- <li className='post-header-col'>
- <time className='search-item-time'>
- {utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)}
- </time>
- </li>
- </ul>
- <div className='search-item-snippet'>
- <span
- onClick={this.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ <div className='post__img'>
+ <img
+ src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
+ height='36'
+ width='36'
/>
</div>
+ <div>
+ <ul className='post__header'>
+ <li className='col__name'><strong><UserProfile userId={this.props.post.user_id} /></strong></li>
+ <li className='col'>
+ <time className='search-item-time'>
+ {utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)}
+ </time>
+ </li>
+ <li>
+ <a
+ href='#'
+ className='search-item__jump'
+ onClick={this.handleClick}
+ >
+ {'Jump'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className='comment-icon__container search-item__comment'
+ onClick={this.handleFocusRHSClick}
+ >
+ <span
+ className='comment-icon'
+ dangerouslySetInnerHTML={{__html: Constants.COMMENT_ICON}}
+ />
+ </a>
+ </li>
+ </ul>
+ <div className='search-item-snippet'>
+ <span
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}}
+ />
+ </div>
+ </div>
</div>
</div>
);
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index 68d9cea48..4af46c35a 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -1,14 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
export default class SettingsSidebar extends React.Component {
componentDidUpdate() {
$('.settings-modal').find('.modal-body').scrollTop(0);
$('.settings-modal').find('.modal-body').perfectScrollbar('update');
- if (utils.isSafari()) {
- $('.settings-modal .settings-links .nav').addClass('absolute');
- }
}
constructor(props) {
super(props);
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 0b1abe4fe..3d7f449d1 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,26 +1,24 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const NewChannelFlow = require('./new_channel_flow.jsx');
-const MoreDirectChannels = require('./more_direct_channels.jsx');
-const SearchBox = require('./search_bar.jsx');
-const SidebarHeader = require('./sidebar_header.jsx');
-const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
-const TutorialTip = require('./tutorial/tutorial_tip.jsx');
-
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const TeamStore = require('../stores/team_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-
-const AsyncClient = require('../utils/async_client.jsx');
-const Client = require('../utils/client.jsx');
-const Utils = require('../utils/utils.jsx');
-
-const Constants = require('../utils/constants.jsx');
+import NewChannelFlow from './new_channel_flow.jsx';
+import MoreDirectChannels from './more_direct_channels.jsx';
+import SidebarHeader from './sidebar_header.jsx';
+import UnreadChannelIndicator from './unread_channel_indicator.jsx';
+import TutorialTip from './tutorial/tutorial_tip.jsx';
+
+import ChannelStore from '../stores/channel_store.jsx';
+import UserStore from '../stores/user_store.jsx';
+import TeamStore from '../stores/team_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
+
+import * as AsyncClient from '../utils/async_client.jsx';
+import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import Constants from '../utils/constants.jsx';
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
-const NotificationPrefs = Constants.NotificationPrefs;
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -39,7 +37,6 @@ export default class Sidebar extends React.Component {
this.onScroll = this.onScroll.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
- this.updateScrollbar = this.updateScrollbar.bind(this);
this.handleResize = this.handleResize.bind(this);
this.showNewChannelModal = this.showNewChannelModal.bind(this);
@@ -49,8 +46,6 @@ export default class Sidebar extends React.Component {
this.createChannelElement = this.createChannelElement.bind(this);
this.updateTitle = this.updateTitle.bind(this);
- this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
- this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -60,43 +55,15 @@ export default class Sidebar extends React.Component {
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
this.state = state;
-
- this.unreadCountPerChannel = {};
- this.setUnreadCountPerChannel();
- }
- setUnreadCountPerChannel() {
- const channels = ChannelStore.getAll();
- const members = ChannelStore.getAllMembers();
- const channelUnreadCounts = {};
-
- channels.forEach((ch) => {
- const chMember = members[ch.id];
- let chMentionCount = chMember.mention_count;
- let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
-
- if (ch.type === 'D') {
- chMentionCount = chUnreadCount;
- chUnreadCount = 0;
- } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) {
- chUnreadCount = 0;
- }
-
- channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
- });
-
- this.unreadCountPerChannel = channelUnreadCounts;
}
- getUnreadCount(channelId) {
- let mentions = 0;
+ getTotalUnreadCount() {
let msgs = 0;
+ let mentions = 0;
+ const unreadCounts = this.state.unreadCounts;
- if (channelId) {
- return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
- }
-
- Object.keys(this.unreadCountPerChannel).forEach((chId) => {
- msgs += this.unreadCountPerChannel[chId].msgs;
- mentions += this.unreadCountPerChannel[chId].mentions;
+ Object.keys(unreadCounts).forEach((chId) => {
+ msgs += unreadCounts[chId].msgs;
+ mentions += unreadCounts[chId].mentions;
});
return {msgs, mentions};
@@ -106,6 +73,8 @@ export default class Sidebar extends React.Component {
const currentChannelId = ChannelStore.getCurrentId();
const channels = Object.assign([], ChannelStore.getAll());
+ channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
+
const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL);
@@ -155,6 +124,7 @@ export default class Sidebar extends React.Component {
privateChannels,
visibleDirectChannels,
hiddenDirectChannelCount,
+ unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
};
}
@@ -168,12 +138,15 @@ export default class Sidebar extends React.Component {
this.updateTitle();
this.updateUnreadIndicators();
- this.updateScrollbar();
window.addEventListener('resize', this.handleResize);
+
+ if ($(window).width() > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ }
}
shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areStatesEqual(nextState, this.state)) {
+ if (!Utils.areObjectsEqual(nextState, this.state)) {
return true;
}
return false;
@@ -181,7 +154,6 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
- this.updateScrollbar();
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
@@ -198,17 +170,8 @@ export default class Sidebar extends React.Component {
windowHeight: Utils.windowHeight()
});
}
- updateScrollbar() {
- if (this.state.windowWidth > 768) {
- $('.nav-pills__container').perfectScrollbar();
- $('.nav-pills__container').perfectScrollbar('update');
- }
- }
onChange() {
- var newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState(this.getStateFromStores());
}
updateTitle() {
const channel = ChannelStore.getCurrent();
@@ -223,7 +186,7 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
- const unread = this.getUnreadCount();
+ const unread = this.getTotalUnreadCount();
const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
const unreadTitle = unread.msgs > 0 ? '* ' : '';
document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
@@ -349,13 +312,13 @@ export default class Sidebar extends React.Component {
}
createChannelElement(channel, index, arr, handleClose) {
- var members = this.state.members;
- var activeId = this.state.activeId;
- var channelMember = members[channel.id];
- var unreadCount = this.getUnreadCount(channel.id);
- var msgCount;
+ const members = this.state.members;
+ const activeId = this.state.activeId;
+ const channelMember = members[channel.id];
+ const unreadCount = this.state.unreadCounts[channel.id] || {msgs: 0, mentions: 0};
+ let msgCount;
- var linkClass = '';
+ let linkClass = '';
if (channel.id === activeId) {
linkClass = 'active';
}
@@ -512,8 +475,6 @@ export default class Sidebar extends React.Component {
render() {
this.badgesActive = false;
- this.setUnreadCountPerChannel();
-
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
@@ -587,7 +548,6 @@ export default class Sidebar extends React.Component {
teamName={TeamStore.getCurrent().name}
teamType={TeamStore.getCurrent().type}
/>
- <SearchBox />
<UnreadChannelIndicator
show={this.state.showTopUnread}
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index bc7f6ba50..20f4fd511 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -1,14 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const NavbarDropdown = require('./navbar_dropdown.jsx');
-const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+import NavbarDropdown from './navbar_dropdown.jsx';
+import TutorialTip from './tutorial/tutorial_tip.jsx';
-const UserStore = require('../stores/user_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
+import UserStore from '../stores/user_store.jsx';
+import PreferenceStore from '../stores/preference_store.jsx';
-const Utils = require('../utils/utils.jsx');
-const Constants = require('../utils/constants.jsx');
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index e2ef60959..ac1049da0 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -1,15 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SearchResults = require('./search_results.jsx');
-var RhsThread = require('./rhs_thread.jsx');
-var SearchStore = require('../stores/search_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
-var Utils = require('../utils/utils.jsx');
+import SearchResults from './search_results.jsx';
+import RhsThread from './rhs_thread.jsx';
+import SearchStore from '../stores/search_store.jsx';
+import PostStore from '../stores/post_store.jsx';
+import * as Utils from '../utils/utils.jsx';
-function getStateFromStores() {
- return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
-}
+const SIDEBAR_SCROLL_DELAY = 500;
export default class SidebarRight extends React.Component {
constructor(props) {
@@ -19,22 +17,37 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.onShowSearch = this.onShowSearch.bind(this);
this.doStrangeThings = this.doStrangeThings.bind(this);
- this.state = getStateFromStores();
+ this.state = this.getStateFromStores();
+ }
+ getStateFromStores() {
+ return {
+ search_visible: SearchStore.getSearchResults() != null,
+ post_right_visible: PostStore.getSelectedPost() != null,
+ is_mention_search: SearchStore.getIsMentionSearch()
+ };
}
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
+ SearchStore.addShowSearchListener(this.onShowSearch);
this.doStrangeThings();
}
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
+ SearchStore.removeShowSearchListener(this.onShowSearch);
}
- componentWillUpdate() {
- PostStore.jumpPostsViewSidebarOpen();
+ componentWillUpdate(nextProps, nextState) {
+ const isOpen = this.state.search_visible || this.state.post_right_visible;
+ const willOpen = nextState.search_visible || nextState.post_right_visible;
+
+ if (!isOpen && willOpen) {
+ setTimeout(() => PostStore.jumpPostsViewSidebarOpen(), SIDEBAR_SCROLL_DELAY);
+ }
}
doStrangeThings() {
// We should have a better way to do this stuff
@@ -64,18 +77,25 @@ export default class SidebarRight extends React.Component {
this.doStrangeThings();
}
onSelectedChange(fromSearch) {
- var newState = getStateFromStores(fromSearch);
+ var newState = this.getStateFromStores(fromSearch);
newState.from_search = fromSearch;
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
onSearchChange() {
- var newState = getStateFromStores();
- if (!Utils.areStatesEqual(newState, this.state)) {
+ var newState = this.getStateFromStores();
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
+ onShowSearch() {
+ if (!this.state.search_visible) {
+ this.setState({
+ search_visible: true
+ });
+ }
+ }
render() {
var content = '';
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index 2135e3ef3..d93d146d8 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var InviteMemberModal = require('./invite_member_modal.jsx');
-var UserSettingsModal = require('./user_settings/user_settings_modal.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var TeamStore = require('../stores/team_store.jsx');
-var client = require('../utils/client.jsx');
-var utils = require('../utils/utils.jsx');
+import TeamMembersModal from './team_members_modal.jsx';
+import ToggleModalButton from './toggle_modal_button.jsx';
+import UserSettingsModal from './user_settings/user_settings_modal.jsx';
+import UserStore from '../stores/user_store.jsx';
+import * as client from '../utils/client.jsx';
+import * as EventHelpers from '../dispatcher/event_helpers.jsx';
+import * as utils from '../utils/utils.jsx';
export default class SidebarRightMenu extends React.Component {
componentDidMount() {
@@ -46,9 +47,9 @@ export default class SidebarRightMenu extends React.Component {
<li>
<a
href='#'
- onClick={InviteMemberModal.show}
+ onClick={EventHelpers.showInviteMemberModal}
>
- <i className='glyphicon glyphicon-user'></i>Invite New Member
+ <i className='fa fa-user'></i>Invite New Member
</a>
</li>
);
@@ -56,12 +57,12 @@ export default class SidebarRightMenu extends React.Component {
if (this.props.teamType === 'O') {
teamLink = (
<li>
- <a href='#'
- data-toggle='modal'
- data-target='#get_link'
- data-title='Team Invite'
- data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
- ><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a>
+ <a
+ href='#'
+ onClick={EventHelpers.showGetTeamInviteLinkModal}
+ >
+ <i className='glyphicon glyphicon-link'></i>{'Get Team Invite Link'}
+ </a>
</li>
);
}
@@ -74,28 +75,25 @@ export default class SidebarRightMenu extends React.Component {
href='#'
data-toggle='modal'
data-target='#team_settings'
- ><i className='glyphicon glyphicon-globe'></i>Team Settings</a>
+ ><i className='fa fa-globe'></i>Team Settings</a>
</li>
);
manageLink = (
<li>
- <a
- href='#'
- data-toggle='modal'
- data-target='#team_members'
- >
- <i className='glyphicon glyphicon-wrench'></i>Manage Members</a>
+ <ToggleModalButton dialogType={TeamMembersModal}>
+ <i className='fa fa-users'></i>{'Manage Members'}
+ </ToggleModalButton>
</li>
);
}
- if (isSystemAdmin) {
+ if (isSystemAdmin && !utils.isMobile()) {
consoleLink = (
<li>
<a
href={'/admin_console?' + utils.getSessionIndex()}
>
- <i className='glyphicon glyphicon-wrench'></i>System Console</a>
+ <i className='fa fa-wrench'></i>System Console</a>
</li>
);
}
@@ -125,7 +123,7 @@ export default class SidebarRightMenu extends React.Component {
href='#'
onClick={() => this.setState({showUserSettingsModal: true})}
>
- <i className='glyphicon glyphicon-cog'></i>Account Settings
+ <i className='fa fa-cog'></i>Account Settings
</a>
</li>
{teamSettingsLink}
@@ -137,18 +135,18 @@ export default class SidebarRightMenu extends React.Component {
<a
href='#'
onClick={this.handleLogoutClick}
- ><i className='glyphicon glyphicon-log-out'></i>Logout</a></li>
+ ><i className='fa fa-sign-out'></i>Logout</a></li>
<li className='divider'></li>
<li>
<a
target='_blank'
- href='/static/help/configure_links.html'
- ><i className='glyphicon glyphicon-question-sign'></i>Help</a></li>
+ href='/static/help/help.html'
+ ><i className='fa fa-question'></i>Help</a></li>
<li>
<a
target='_blank'
- href='/static/help/configure_links.html'
- ><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li>
+ href='/static/help/report_problem.html'
+ ><i className='fa fa-phone'></i>Report a Problem</a></li>
</ul>
</div>
<UserSettingsModal
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 516765a3f..0ac837326 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const ChoosePage = require('./team_signup_choose_auth.jsx');
-const EmailSignUpPage = require('./team_signup_with_email.jsx');
-const SSOSignupPage = require('./team_signup_with_sso.jsx');
-const Constants = require('../utils/constants.jsx');
+import ChoosePage from './team_signup_choose_auth.jsx';
+import EmailSignUpPage from './team_signup_with_email.jsx';
+import SSOSignupPage from './team_signup_with_sso.jsx';
+import Constants from '../utils/constants.jsx';
export default class TeamSignUp extends React.Component {
constructor(props) {
diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx
index c30132885..6c7fd57b3 100644
--- a/web/react/components/signup_team_complete.jsx
+++ b/web/react/components/signup_team_complete.jsx
@@ -1,13 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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 SendInivtesPage = require('./team_signup_send_invites_page.jsx');
-var UsernamePage = require('./team_signup_username_page.jsx');
-var PasswordPage = require('./team_signup_password_page.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
+import WelcomePage from './team_signup_welcome_page.jsx';
+import TeamDisplayNamePage from './team_signup_display_name_page.jsx';
+import TeamURLPage from './team_signup_url_page.jsx';
+import SendInivtesPage from './team_signup_send_invites_page.jsx';
+import UsernamePage from './team_signup_username_page.jsx';
+import PasswordPage from './team_signup_password_page.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
export default class SignupTeamComplete extends React.Component {
constructor(props) {
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index d70ea5065..2bde78726 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
+import * as Utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
export default class SignupUserComplete extends React.Component {
constructor(props) {
diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx
new file mode 100644
index 000000000..8c2893448
--- /dev/null
+++ b/web/react/components/suggestion/at_mention_provider.jsx
@@ -0,0 +1,100 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import UserStore from '../../stores/user_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+class AtMentionSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let username;
+ let description;
+ let icon;
+ if (item.username === 'all') {
+ username = 'all';
+ description = 'Notifies everyone in the team';
+ icon = <i className='mention-img fa fa-users fa-2x' />;
+ } else if (item.username === 'channel') {
+ username = 'channel';
+ description = 'Notifies everyone in the channel';
+ icon = <i className='mention-img fa fa-users fa-2x' />;
+ } else {
+ username = item.username;
+ description = Utils.getFullName(item);
+ icon = (
+ <img
+ className='mention-img'
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at + '&' + Utils.getSessionIndex()}
+ />
+ );
+ }
+
+ let className = 'mentions-name';
+ if (isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <div className='pull-left'>
+ {icon}
+ </div>
+ <div className='pull-left mention-align'>
+ <span>
+ {'@' + username}
+ </span>
+ <span className='mention-fullname'>
+ {description}
+ </span>
+ </div>
+ </div>
+ );
+ }
+}
+
+AtMentionSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class AtMentionProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
+ if (captured) {
+ const usernamePrefix = captured[1];
+
+ const users = UserStore.getProfiles();
+ let filtered = [];
+
+ for (const id of Object.keys(users)) {
+ const user = users[id];
+
+ if (user.username.startsWith(usernamePrefix)) {
+ filtered.push(user);
+ }
+ }
+
+ // add dummy users to represent the @all and @channel special mentions
+ if ('all'.startsWith(usernamePrefix)) {
+ filtered.push({username: 'all'});
+ }
+
+ if ('channel'.startsWith(usernamePrefix)) {
+ filtered.push({username: 'channel'});
+ }
+
+ filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
+
+ const mentions = filtered.map((user) => '@' + user.username);
+
+ SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
+ SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/command_provider.jsx b/web/react/components/suggestion/command_provider.jsx
new file mode 100644
index 000000000..91d556bb9
--- /dev/null
+++ b/web/react/components/suggestion/command_provider.jsx
@@ -0,0 +1,46 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as AsyncClient from '../../utils/async_client.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+class CommandSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'command-name';
+ if (isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <div className='command__title'>
+ <string>{item.suggestion}</string>
+ </div>
+ <div className='command__desc'>
+ {item.description}
+ </div>
+ </div>
+ );
+ }
+}
+
+CommandSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class CommandProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ if (pretext.startsWith('/')) {
+ SuggestionStore.setMatchedPretext(suggestionId, pretext);
+
+ AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/emoticon_provider.jsx b/web/react/components/suggestion/emoticon_provider.jsx
new file mode 100644
index 000000000..fd470cf21
--- /dev/null
+++ b/web/react/components/suggestion/emoticon_provider.jsx
@@ -0,0 +1,91 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import * as Emoticons from '../../utils/emoticons.jsx';
+
+const MAX_EMOTICON_SUGGESTIONS = 40;
+
+class EmoticonSuggestion extends React.Component {
+ render() {
+ const text = this.props.term;
+ const name = this.props.item;
+
+ let className = 'emoticon-suggestion';
+ if (this.props.isSelection) {
+ className += ' suggestion--selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={this.props.onClick}
+ >
+ <div className='pull-left'>
+ <img
+ alt={text}
+ className='emoticon-suggestion__image'
+ src={Emoticons.getImagePathForEmoticon(name)}
+ title={text}
+ />
+ </div>
+ <div className='pull-left'>
+ {text}
+ </div>
+ </div>
+ );
+ }
+}
+
+EmoticonSuggestion.propTypes = {
+ item: React.PropTypes.string.isRequired,
+ term: React.PropTypes.string.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class EmoticonProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(pretext);
+ if (captured) {
+ const text = captured[1];
+ const partialName = captured[2];
+
+ const names = [];
+
+ for (const emoticon of Emoticons.emoticonMap.keys()) {
+ if (emoticon.indexOf(partialName) !== -1) {
+ names.push(emoticon);
+
+ if (names.length >= MAX_EMOTICON_SUGGESTIONS) {
+ break;
+ }
+ }
+ }
+
+ // sort the emoticons so that emoticons starting with the entered text come first
+ names.sort((a, b) => {
+ const aPrefix = a.startsWith(partialName);
+ const bPrefix = b.startsWith(partialName);
+
+ if (aPrefix === bPrefix) {
+ return a.localeCompare(b);
+ } else if (aPrefix) {
+ return -1;
+ }
+
+ return 1;
+ });
+
+ const terms = names.map((name) => ':' + name + ':');
+
+ if (terms.length > 0) {
+ SuggestionStore.setMatchedPretext(suggestionId, text);
+ SuggestionStore.addSuggestions(suggestionId, terms, names, EmoticonSuggestion);
+
+ // force the selection to be cleared since the order of elements may have changed
+ SuggestionStore.clearSelection(suggestionId);
+ }
+ }
+ }
+}
diff --git a/web/react/components/suggestion/search_channel_provider.jsx b/web/react/components/suggestion/search_channel_provider.jsx
new file mode 100644
index 000000000..7547a9341
--- /dev/null
+++ b/web/react/components/suggestion/search_channel_provider.jsx
@@ -0,0 +1,69 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelStore from '../../stores/channel_store.jsx';
+import Constants from '../../utils/constants.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+class SearchChannelSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'search-autocomplete__item';
+ if (isSelection) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ onClick={onClick}
+ className={className}
+ >
+ {item.name}
+ </div>
+ );
+ }
+}
+
+SearchChannelSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class SearchChannelProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext);
+ if (captured) {
+ const channelPrefix = captured[1];
+
+ const channels = ChannelStore.getAll();
+ const publicChannels = [];
+ const privateChannels = [];
+
+ for (const id of Object.keys(channels)) {
+ const channel = channels[id];
+
+ // don't show direct channels
+ if (channel.type !== Constants.DM_CHANNEL && channel.name.startsWith(channelPrefix)) {
+ if (channel.type === Constants.OPEN_CHANNEL) {
+ publicChannels.push(channel);
+ } else {
+ privateChannels.push(channel);
+ }
+ }
+ }
+
+ publicChannels.sort((a, b) => a.name.localeCompare(b.name));
+ const publicChannelNames = publicChannels.map((channel) => channel.name);
+
+ privateChannels.sort((a, b) => a.name.localeCompare(b.name));
+ const privateChannelNames = privateChannels.map((channel) => channel.name);
+
+ SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
+
+ SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
+ SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/search_suggestion_list.jsx b/web/react/components/suggestion/search_suggestion_list.jsx
new file mode 100644
index 000000000..3378a33a0
--- /dev/null
+++ b/web/react/components/suggestion/search_suggestion_list.jsx
@@ -0,0 +1,86 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+import SuggestionList from './suggestion_list.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+export default class SearchSuggestionList extends SuggestionList {
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.items.length > 0 && prevState.items.length === 0) {
+ this.getContent().perfectScrollbar();
+ }
+ }
+
+ getContent() {
+ return $(ReactDOM.findDOMNode(this.refs.popover)).find('.popover-content');
+ }
+
+ renderChannelDivider(type) {
+ let text;
+ if (type === Constants.OPEN_CHANNEL) {
+ text = 'Public ' + Utils.getChannelTerm(type) + 's';
+ } else {
+ text = 'Private ' + Utils.getChannelTerm(type) + 's';
+ }
+
+ return (
+ <div
+ key={type + '-divider'}
+ className='search-autocomplete__divider'
+ >
+ <span>{text}</span>
+ </div>
+ );
+ }
+
+ render() {
+ if (this.state.items.length === 0) {
+ return null;
+ }
+
+ const items = [];
+ for (let i = 0; i < this.state.items.length; i++) {
+ const item = this.state.items[i];
+ const term = this.state.terms[i];
+ const isSelection = term === this.state.selection;
+
+ // ReactComponent names need to be upper case when used in JSX
+ const Component = this.state.components[i];
+
+ // temporary hack to add dividers between public and private channels in the search suggestion list
+ if (i === 0 || item.type !== this.state.items[i - 1].type) {
+ if (item.type === Constants.OPEN_CHANNEL) {
+ items.push(this.renderChannelDivider(Constants.OPEN_CHANNEL));
+ } else if (item.type === Constants.PRIVATE_CHANNEL) {
+ items.push(this.renderChannelDivider(Constants.PRIVATE_CHANNEL));
+ }
+ }
+
+ items.push(
+ <Component
+ key={term}
+ ref={term}
+ item={item}
+ isSelection={isSelection}
+ onClick={this.handleItemClick.bind(this, term)}
+ />
+ );
+ }
+
+ return (
+ <ReactBootstrap.Popover
+ ref='popover'
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
+ >
+ {items}
+ </ReactBootstrap.Popover>
+ );
+ }
+}
+
+SearchSuggestionList.propTypes = {
+ ...SuggestionList.propTypes
+};
diff --git a/web/react/components/suggestion/search_user_provider.jsx b/web/react/components/suggestion/search_user_provider.jsx
new file mode 100644
index 000000000..cf2953937
--- /dev/null
+++ b/web/react/components/suggestion/search_user_provider.jsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import UserStore from '../../stores/user_store.jsx';
+
+class SearchUserSuggestion extends React.Component {
+ render() {
+ const {item, isSelection, onClick} = this.props;
+
+ let className = 'search-autocomplete__item';
+ if (isSelection) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ className={className}
+ onClick={onClick}
+ >
+ <img
+ className='profile-img rounded'
+ src={'/api/v1/users/' + item.id + '/image?time=' + item.update_at}
+ />
+ {item.username}
+ </div>
+ );
+ }
+}
+
+SearchUserSuggestion.propTypes = {
+ item: React.PropTypes.object.isRequired,
+ isSelection: React.PropTypes.bool,
+ onClick: React.PropTypes.func
+};
+
+export default class SearchUserProvider {
+ handlePretextChanged(suggestionId, pretext) {
+ const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
+ if (captured) {
+ const usernamePrefix = captured[1];
+
+ const users = UserStore.getProfiles();
+ let filtered = [];
+
+ for (const id of Object.keys(users)) {
+ const user = users[id];
+
+ if (user.username.startsWith(usernamePrefix)) {
+ filtered.push(user);
+ }
+ }
+
+ filtered = filtered.sort((a, b) => a.username.localeCompare(b.username));
+
+ const usernames = filtered.map((user) => user.username);
+
+ SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
+ SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
+ }
+ }
+}
diff --git a/web/react/components/suggestion/suggestion_box.jsx b/web/react/components/suggestion/suggestion_box.jsx
new file mode 100644
index 000000000..57a33c24a
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_box.jsx
@@ -0,0 +1,163 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+const KeyCodes = Constants.KeyCodes;
+
+export default class SuggestionBox extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleDocumentClick = this.handleDocumentClick.bind(this);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleCompleteWord = this.handleCompleteWord.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handlePretextChanged = this.handlePretextChanged.bind(this);
+
+ this.suggestionId = Utils.generateId();
+ }
+
+ componentDidMount() {
+ SuggestionStore.registerSuggestionBox(this.suggestionId);
+ $(document).on('click', this.handleDocumentClick);
+
+ SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
+ SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged);
+ }
+
+ componentWillUnmount() {
+ SuggestionStore.removeCompleteWordListener(this.suggestionId, this.handleCompleteWord);
+ SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged);
+
+ SuggestionStore.unregisterSuggestionBox(this.suggestionId);
+ $(document).off('click', this.handleDocumentClick);
+ }
+
+ getTextbox() {
+ // this is to support old code that looks at the input/textarea DOM nodes
+ return ReactDOM.findDOMNode(this.refs.textbox);
+ }
+
+ handleDocumentClick(e) {
+ const container = $(ReactDOM.findDOMNode(this));
+ if (!(container.is(e.target) || container.has(e.target).length > 0)) {
+ // we can't just use blur for this because it fires and hides the children before
+ // their click handlers can be called
+ EventHelpers.emitClearSuggestions(this.suggestionId);
+ }
+ }
+
+ handleChange(e) {
+ const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const caret = Utils.getCaretPosition(textbox);
+ const pretext = textbox.value.substring(0, caret);
+
+ EventHelpers.emitSuggestionPretextChanged(this.suggestionId, pretext);
+
+ if (this.props.onUserInput) {
+ this.props.onUserInput(textbox.value);
+ }
+
+ if (this.props.onChange) {
+ this.props.onChange(e);
+ }
+ }
+
+ handleCompleteWord(term) {
+ const textbox = ReactDOM.findDOMNode(this.refs.textbox);
+ const caret = Utils.getCaretPosition(textbox);
+
+ const text = this.props.value;
+ const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length);
+ const suffix = text.substring(caret);
+
+ if (this.props.onUserInput) {
+ this.props.onUserInput(prefix + term + ' ' + suffix);
+ }
+
+ // set the caret position after the next rendering
+ window.requestAnimationFrame(() => {
+ Utils.setCaretPosition(textbox, prefix.length + term.length + 1);
+ });
+ }
+
+ handleKeyDown(e) {
+ if (SuggestionStore.hasSuggestions(this.suggestionId)) {
+ if (e.which === KeyCodes.UP) {
+ EventHelpers.emitSelectPreviousSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (e.which === KeyCodes.DOWN) {
+ EventHelpers.emitSelectNextSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.TAB) {
+ EventHelpers.emitCompleteWordSuggestion(this.suggestionId);
+ e.preventDefault();
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ } else if (this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ }
+
+ handlePretextChanged(pretext) {
+ for (const provider of this.props.providers) {
+ provider.handlePretextChanged(this.suggestionId, pretext);
+ }
+ }
+
+ render() {
+ const newProps = Object.assign({}, this.props, {
+ onChange: this.handleChange,
+ onKeyDown: this.handleKeyDown
+ });
+
+ let textbox = null;
+ if (this.props.type === 'input') {
+ textbox = (
+ <input
+ ref='textbox'
+ type='text'
+ {...newProps}
+ />
+ );
+ } else if (this.props.type === 'textarea') {
+ textbox = (
+ <textarea
+ ref='textbox'
+ {...newProps}
+ />
+ );
+ }
+
+ const SuggestionListComponent = this.props.listComponent;
+
+ return (
+ <div>
+ {textbox}
+ <SuggestionListComponent suggestionId={this.suggestionId} />
+ </div>
+ );
+ }
+}
+
+SuggestionBox.defaultProps = {
+ type: 'input'
+};
+
+SuggestionBox.propTypes = {
+ listComponent: React.PropTypes.func.isRequired,
+ type: React.PropTypes.oneOf(['input', 'textarea']).isRequired,
+ value: React.PropTypes.string.isRequired,
+ onUserInput: React.PropTypes.func,
+ providers: React.PropTypes.arrayOf(React.PropTypes.object),
+
+ // explicitly name any input event handlers we override and need to manually call
+ onChange: React.PropTypes.func,
+ onKeyDown: React.PropTypes.func
+};
diff --git a/web/react/components/suggestion/suggestion_list.jsx b/web/react/components/suggestion/suggestion_list.jsx
new file mode 100644
index 000000000..e3ccd0f08
--- /dev/null
+++ b/web/react/components/suggestion/suggestion_list.jsx
@@ -0,0 +1,125 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
+import SuggestionStore from '../../stores/suggestion_store.jsx';
+
+export default class SuggestionList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getContent = this.getContent.bind(this);
+
+ this.handleItemClick = this.handleItemClick.bind(this);
+ this.handleSuggestionsChanged = this.handleSuggestionsChanged.bind(this);
+
+ this.scrollToItem = this.scrollToItem.bind(this);
+
+ this.state = {
+ items: [],
+ terms: [],
+ components: [],
+ selection: ''
+ };
+ }
+
+ componentDidMount() {
+ SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
+ }
+
+ componentWillUnmount() {
+ SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
+ }
+
+ getContent() {
+ return $(ReactDOM.findDOMNode(this.refs.content));
+ }
+
+ handleItemClick(term, e) {
+ EventHelpers.emitCompleteWordSuggestion(this.props.suggestionId, term);
+
+ e.preventDefault();
+ }
+
+ handleSuggestionsChanged() {
+ const selection = SuggestionStore.getSelection(this.props.suggestionId);
+
+ this.setState({
+ items: SuggestionStore.getItems(this.props.suggestionId),
+ terms: SuggestionStore.getTerms(this.props.suggestionId),
+ components: SuggestionStore.getComponents(this.props.suggestionId),
+ selection
+ });
+
+ if (selection) {
+ window.requestAnimationFrame(() => this.scrollToItem(this.state.selection));
+ }
+ }
+
+ scrollToItem(term) {
+ const content = this.getContent();
+ const visibleContentHeight = content[0].clientHeight;
+ const actualContentHeight = content[0].scrollHeight;
+
+ if (visibleContentHeight < actualContentHeight) {
+ const contentTop = content.scrollTop();
+ const contentTopPadding = parseInt(content.css('padding-top'), 10);
+ const contentBottomPadding = parseInt(content.css('padding-top'), 10);
+
+ const item = $(ReactDOM.findDOMNode(this.refs[term]));
+ const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
+ const itemBottomMargin = parseInt(item.css('margin-bottom'), 10) + parseInt(item.css('padding-bottom'), 10);
+ const itemBottom = item[0].offsetTop + item.height() + itemBottomMargin;
+
+ if (itemTop - contentTopPadding < contentTop) {
+ // the item is off the top of the visible space
+ content.scrollTop(itemTop - contentTopPadding);
+ } else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
+ // the item has gone off the bottom of the visible space
+ content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
+ }
+ }
+ }
+
+ render() {
+ if (this.state.items.length === 0) {
+ return null;
+ }
+
+ const items = [];
+ for (let i = 0; i < this.state.items.length; i++) {
+ const item = this.state.items[i];
+ const term = this.state.terms[i];
+ const isSelection = term === this.state.selection;
+
+ // ReactComponent names need to be upper case when used in JSX
+ const Component = this.state.components[i];
+
+ items.push(
+ <Component
+ key={term}
+ ref={term}
+ item={item}
+ term={term}
+ isSelection={isSelection}
+ onClick={this.handleItemClick.bind(this, term)}
+ />
+ );
+ }
+
+ return (
+ <div className='suggestion-list suggestion-list--top'>
+ <div
+ ref='content'
+ className='suggestion-content suggestion-content--top'
+ >
+ {items}
+ </div>
+ </div>
+ );
+ }
+}
+
+SuggestionList.propTypes = {
+ suggestionId: React.PropTypes.string.isRequired
+};
diff --git a/web/react/components/team_export_tab.jsx b/web/react/components/team_export_tab.jsx
index e15e3a372..14df7fffc 100644
--- a/web/react/components/team_export_tab.jsx
+++ b/web/react/components/team_export_tab.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
+import * as Client from '../utils/client.jsx';
export default class TeamExportTab extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 587ef5ec2..03715d585 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const SettingItemMin = require('./setting_item_min.jsx');
-const SettingItemMax = require('./setting_item_max.jsx');
+import SettingItemMin from './setting_item_min.jsx';
+import SettingItemMax from './setting_item_max.jsx';
-const Client = require('../utils/client.jsx');
-const Utils = require('../utils/utils.jsx');
-const TeamStore = require('../stores/team_store.jsx');
+import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
+import TeamStore from '../stores/team_store.jsx';
export default class GeneralTab extends React.Component {
constructor(props) {
@@ -393,7 +393,7 @@ export default class GeneralTab extends React.Component {
</div>
</div>
</div>
- <div className='setting-list__hint'>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div>
+ <div className='setting-list__hint'>{'Your Invite Code is used in the URL sent to people to join your team. Regenerating your Invite Code will invalidate the URLs in previous invitations, unless "Allow anyone to sign-up from login page" is enabled.'}</div>
</div>
);
@@ -437,6 +437,7 @@ export default class GeneralTab extends React.Component {
<input
className='form-control'
type='text'
+ maxLength='22'
onChange={this.updateName}
value={this.state.name}
/>
@@ -452,6 +453,7 @@ export default class GeneralTab extends React.Component {
server_error={serverError}
client_error={clientError}
updateSection={this.onUpdateNameSection}
+ extraInfo='Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.'
/>
);
} else {
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index a80b1a472..37f8746d7 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var SettingUpload = require('./setting_upload.jsx');
+import * as utils from '../utils/utils.jsx';
+import SettingUpload from './setting_upload.jsx';
export default class TeamImportTab extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx
deleted file mode 100644
index ac1ebf52d..000000000
--- a/web/react/components/team_members.jsx
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var UserStore = require('../stores/user_store.jsx');
-var MemberListTeam = require('./member_list_team.jsx');
-var utils = require('../utils/utils.jsx');
-
-function getStateFromStores() {
- var users = UserStore.getProfiles();
- var memberList = [];
- for (var id in users) {
- if (users.hasOwnProperty(id)) {
- memberList.push(users[id]);
- }
- }
-
- memberList.sort(function sort(a, b) {
- if (a.username < b.username) {
- return -1;
- }
-
- if (a.username > b.username) {
- return 1;
- }
-
- return 0;
- });
-
- return {
- member_list: memberList
- };
-}
-
-export default class TeamMembers extends React.Component {
- constructor(props) {
- super(props);
-
- this.onChange = this.onChange.bind(this);
-
- this.state = getStateFromStores();
- }
-
- componentDidMount() {
- UserStore.addChangeListener(this.onChange);
-
- var self = this;
- $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function show() {
- self.setState({render_members: false});
- });
-
- $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function hide() {
- self.setState({render_members: true});
- });
- }
-
- componentWillUnmount() {
- UserStore.removeChangeListener(this.onChange);
- }
-
- onChange() {
- var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- this.setState(newState);
- }
- }
-
- render() {
- var serverError = null;
-
- if (this.state.server_error) {
- serverError = <label className='has-error control-label'>{this.state.server_error}</label>;
- }
-
- var renderMembers = '';
-
- if (this.state.render_members) {
- renderMembers = <MemberListTeam users={this.state.member_list} />;
- }
-
- return (
- <div
- className='modal fade more-modal'
- ref='modal'
- id='team_members'
- 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'
- aria-label='Close'
- >
- <span aria-hidden='true'>×</span>
- </button>
- <h4
- className='modal-title'
- id='myModalLabel'
- >{this.props.teamDisplayName + ' Members'}</h4>
- </div>
- <div
- ref='modalBody'
- className='modal-body'
- >
- <div className='team-member-list'>
- {renderMembers}
- </div>
- {serverError}
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >Close</button>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-TeamMembers.propTypes = {
- teamDisplayName: React.PropTypes.string
-};
diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx
new file mode 100644
index 000000000..27224c283
--- /dev/null
+++ b/web/react/components/team_members_modal.jsx
@@ -0,0 +1,71 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import MemberListTeam from './member_list_team.jsx';
+import TeamStore from '../stores/team_store.jsx';
+
+const Modal = ReactBootstrap.Modal;
+
+export default class TeamMembersModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onShow = this.onShow.bind(this);
+ }
+
+ componentDidMount() {
+ if (this.props.show) {
+ this.onShow();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.show && !prevProps.show) {
+ this.onShow();
+ }
+ }
+
+ onShow() {
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ } else {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
+ }
+ }
+
+ render() {
+ const team = TeamStore.getCurrent();
+
+ return (
+ <Modal
+ dialogClassName='team-members-modal'
+ show={this.props.show}
+ onHide={this.props.onHide}
+ >
+ <Modal.Header closeButton={true}>
+ {team.display_name + ' Members'}
+ </Modal.Header>
+ <Modal.Body ref='modalBody'>
+ <div className='team-member-list'>
+ <MemberListTeam />
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.props.onHide}
+ >
+ {'Close'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+TeamMembersModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onHide: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 09674f1ef..bbcedb5dd 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var TeamStore = require('../stores/team_store.jsx');
-var ImportTab = require('./team_import_tab.jsx');
-var ExportTab = require('./team_export_tab.jsx');
-var GeneralTab = require('./team_general_tab.jsx');
-var Utils = require('../utils/utils.jsx');
+import TeamStore from '../stores/team_store.jsx';
+import ImportTab from './team_import_tab.jsx';
+import ExportTab from './team_export_tab.jsx';
+import GeneralTab from './team_general_tab.jsx';
+import * as Utils from '../utils/utils.jsx';
export default class TeamSettings extends React.Component {
constructor(props) {
@@ -23,7 +23,7 @@ export default class TeamSettings extends React.Component {
}
onChange() {
var team = TeamStore.getCurrent();
- if (!Utils.areStatesEqual(this.state.team, team)) {
+ if (!Utils.areObjectsEqual(this.state.team, team)) {
this.setState({team});
}
}
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 4d47db2a8..dbdbde958 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const SettingsSidebar = require('./settings_sidebar.jsx');
-const TeamSettings = require('./team_settings.jsx');
+import SettingsSidebar from './settings_sidebar.jsx';
+import TeamSettings from './team_settings.jsx';
export default class TeamSettingsModal extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 2005ecc31..f4d5ea162 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
+import * as utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
export default class TeamSignupDisplayNamePage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx
index 1d2b24ed7..59c4771d7 100644
--- a/web/react/components/team_signup_email_item.jsx
+++ b/web/react/components/team_signup_email_item.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
export default class TeamSignupEmailItem extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index 67fd686bc..378c7fe2c 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
+import * as Client from '../utils/client.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
+import UserStore from '../stores/user_store.jsx';
export default class TeamSignupPasswordPage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index 7b4db8fae..a580623e4 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var EmailItem = require('./team_signup_email_item.jsx');
-var Client = require('../utils/client.jsx');
+import EmailItem from './team_signup_email_item.jsx';
+import * as Client from '../utils/client.jsx';
export default class TeamSignupSendInvitesPage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 8972fda1a..30459fc67 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
export default class TeamSignupUrlPage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index d8d0dbf2c..de239f169 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
export default class TeamSignupUsernamePage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 9448413ce..aa91a1329 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
-var BrowserStore = require('../stores/browser_store.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import BrowserStore from '../stores/browser_store.jsx';
export default class TeamSignupWelcomePage extends React.Component {
constructor(props) {
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index 021713f04..4150a0013 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
export default class EmailSignUpPage extends React.Component {
constructor() {
@@ -14,18 +14,19 @@ export default class EmailSignUpPage extends React.Component {
}
handleSubmit(e) {
e.preventDefault();
- var team = {};
- var state = {serverError: ''};
+ const team = {};
+ const state = {serverError: null};
+ let isValid = true;
team.email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!team.email || !Utils.isEmail(team.email)) {
state.emailError = 'Please enter a valid email address';
- state.inValid = true;
+ isValid = false;
} else {
- state.emailError = '';
+ state.emailError = null;
}
- if (state.inValid) {
+ if (!isValid) {
this.setState(state);
return;
}
@@ -45,11 +46,16 @@ export default class EmailSignUpPage extends React.Component {
);
}
render() {
- var serverError = null;
+ let serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
+ let emailError = null;
+ if (this.state.emailError) {
+ emailError = <div className='form-group has-error'><label className='control-label'>{this.state.emailError}</label></div>;
+ }
+
return (
<form
role='form'
@@ -65,6 +71,7 @@ export default class EmailSignUpPage extends React.Component {
maxLength='128'
spellCheck='false'
/>
+ {emailError}
</div>
<div className='form-group'>
<button
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index a0ccdf2c7..e3f16efb0 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
-var client = require('../utils/client.jsx');
-var Constants = require('../utils/constants.jsx');
+import * as utils from '../utils/utils.jsx';
+import * as client from '../utils/client.jsx';
+import Constants from '../utils/constants.jsx';
export default class SSOSignUpPage extends React.Component {
constructor(props) {
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 707033d8f..b29f304ab 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -1,46 +1,39 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const SearchStore = require('../stores/search_store.jsx');
-const CommandList = require('./command_list.jsx');
-const ErrorStore = require('../stores/error_store.jsx');
-
-const Utils = require('../utils/utils.jsx');
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
-const KeyCodes = Constants.KeyCodes;
+import AtMentionProvider from './suggestion/at_mention_provider.jsx';
+import CommandProvider from './suggestion/command_provider.jsx';
+import EmoticonProvider from './suggestion/emoticon_provider.jsx';
+import SuggestionList from './suggestion/suggestion_list.jsx';
+import SuggestionBox from './suggestion/suggestion_box.jsx';
+import ErrorStore from '../stores/error_store.jsx';
+
+import * as TextFormatting from '../utils/text_formatting.jsx';
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
export default class Textbox extends React.Component {
constructor(props) {
super(props);
this.getStateFromStores = this.getStateFromStores.bind(this);
- this.onListenerChange = this.onListenerChange.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
- this.updateMentionTab = this.updateMentionTab.bind(this);
- this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleBackspace = this.handleBackspace.bind(this);
- this.checkForNewMention = this.checkForNewMention.bind(this);
- this.addMention = this.addMention.bind(this);
- this.addCommand = this.addCommand.bind(this);
this.resize = this.resize.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
- this.handlePaste = this.handlePaste.bind(this);
+ this.showPreview = this.showPreview.bind(this);
this.state = {
- mentionText: '-1',
- mentions: [],
connection: ''
};
- this.caret = -1;
- this.addedMention = false;
- this.doProcessMentions = false;
- this.mentions = [];
+ this.suggestionProviders = [new AtMentionProvider(), new EmoticonProvider()];
+ if (props.supportsCommands) {
+ this.suggestionProviders.push(new CommandProvider());
+ }
}
getStateFromStores() {
@@ -54,24 +47,15 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
- SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
- this.updateMentionTab(null);
}
componentWillUnmount() {
- SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
- onListenerChange(id, username) {
- if (id === this.props.id) {
- this.addMention(username);
- }
- }
-
onRecievedError() {
const errorState = ErrorStore.getLastError();
@@ -83,157 +67,21 @@ export default class Textbox extends React.Component {
}
componentDidUpdate() {
- if (this.caret >= 0) {
- Utils.setCaretPosition(ReactDOM.findDOMNode(this.refs.message), this.caret);
- this.caret = -1;
- }
- if (this.doProcessMentions) {
- this.updateMentionTab(null);
- this.doProcessMentions = false;
- }
this.resize();
}
- componentWillReceiveProps(nextProps) {
- if (!this.addedMention) {
- this.checkForNewMention(nextProps.messageText);
- }
- const text = ReactDOM.findDOMNode(this.refs.message).value;
- if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) {
- this.doProcessMentions = true;
- }
- this.addedMention = false;
- this.refs.commands.getSuggestedCommands(nextProps.messageText);
- }
-
- updateMentionTab(mentionText) {
- // using setTimeout so dispatch isn't called during an in progress dispatch
- setTimeout(() => {
- AppDispatcher.handleViewAction({
- type: ActionTypes.RECIEVED_MENTION_DATA,
- id: this.props.id,
- mention_text: mentionText
- });
- }, 1);
- }
-
- handleChange() {
- this.props.onUserInput(ReactDOM.findDOMNode(this.refs.message).value);
- }
-
handleKeyPress(e) {
- const text = ReactDOM.findDOMNode(this.refs.message).value;
-
- if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) {
- this.refs.commands.addFirstCommand();
- e.preventDefault();
- return;
- }
-
- if (!this.doProcessMentions) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
- const preText = text.substring(0, caret);
- const lastSpace = preText.lastIndexOf(' ');
- const lastAt = preText.lastIndexOf('@');
-
- if (caret > lastAt && lastSpace < lastAt) {
- this.doProcessMentions = true;
- }
- }
-
this.props.onKeyPress(e);
}
handleKeyDown(e) {
- if (Utils.getSelectedText(ReactDOM.findDOMNode(this.refs.message)) !== '') {
- this.doProcessMentions = true;
- }
-
- if (e.keyCode === KeyCodes.BACKSPACE) {
- this.handleBackspace(e);
- } else if (this.props.onKeyDown) {
+ if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
- handleBackspace() {
- const text = ReactDOM.findDOMNode(this.refs.message).value;
- if (text.indexOf('/') === 0) {
- this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1));
- }
-
- if (this.doProcessMentions) {
- return;
- }
-
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
- const preText = text.substring(0, caret);
- const lastSpace = preText.lastIndexOf(' ');
- const lastAt = preText.lastIndexOf('@');
-
- if (caret > lastAt && (lastSpace > lastAt || lastSpace === -1)) {
- this.doProcessMentions = true;
- }
- }
-
- checkForNewMention(text) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
-
- const preText = text.substring(0, caret);
-
- const atIndex = preText.lastIndexOf('@');
-
- // The @ character not typed, so nothing to do.
- if (atIndex === -1) {
- this.updateMentionTab('-1');
- return;
- }
-
- const lastCharSpace = preText.lastIndexOf(String.fromCharCode(160));
- const lastSpace = preText.lastIndexOf(' ');
-
- // If there is a space after the last @, nothing to do.
- if (lastSpace > atIndex || lastCharSpace > atIndex) {
- this.updateMentionTab('-1');
- return;
- }
-
- // Get the name typed so far.
- const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
- this.updateMentionTab(name);
- }
-
- addMention(name) {
- const caret = Utils.getCaretPosition(ReactDOM.findDOMNode(this.refs.message));
-
- const text = this.props.messageText;
-
- const preText = text.substring(0, caret);
-
- const atIndex = preText.lastIndexOf('@');
-
- // The @ character not typed, so nothing to do.
- if (atIndex === -1) {
- return;
- }
-
- const prefix = text.substring(0, atIndex);
- const suffix = text.substring(caret, text.length);
- this.caret = prefix.length + name.length + 2;
- this.addedMention = true;
- this.doProcessMentions = true;
-
- this.props.onUserInput(`${prefix}@${name} ${suffix}`);
- }
-
- addCommand(cmd) {
- const elm = ReactDOM.findDOMNode(this.refs.message);
- elm.value = cmd;
- this.handleChange();
- }
-
resize() {
- const e = ReactDOM.findDOMNode(this.refs.message);
+ const e = this.refs.message.getTextbox();
const w = ReactDOM.findDOMNode(this.refs.wrapper);
const prevHeight = $(e).height();
@@ -250,10 +98,16 @@ export default class Textbox extends React.Component {
$(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod);
$(w).css({height: 'auto'}).height(e.scrollHeight + 2);
$(w).closest('.post-body__cell').removeClass('scroll');
+ if (this.state.preview) {
+ $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'auto'}).height(e.scrollHeight - mod);
+ }
} else {
- $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167);
- $(w).css({height: 'auto'}).height(167);
+ $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod);
+ $(w).css({height: 'auto'}).height(163);
$(w).closest('.post-body__cell').addClass('scroll');
+ if (this.state.preview) {
+ $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(163);
+ }
}
if (prevHeight !== $(e).height() && this.props.onHeightChange) {
@@ -262,38 +116,58 @@ export default class Textbox extends React.Component {
}
handleFocus() {
- const elm = ReactDOM.findDOMNode(this.refs.message);
+ const elm = this.refs.message.getTextbox();
if (elm.title === elm.value) {
elm.value = '';
}
}
handleBlur() {
- const elm = ReactDOM.findDOMNode(this.refs.message);
+ const elm = this.refs.message.getTextbox();
if (elm.value === '') {
elm.value = elm.title;
}
}
- handlePaste() {
- this.doProcessMentions = true;
+ showPreview(e) {
+ e.preventDefault();
+ e.target.blur();
+ this.setState({preview: !this.state.preview});
+ this.resize();
+ }
+
+ showHelp(e) {
+ e.preventDefault();
+ e.target.blur();
+
+ global.window.open('/docs/Messaging');
}
render() {
+ let previewLink = null;
+ if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
+ const previewLinkVisible = this.props.messageText.length > 0;
+ previewLink = (
+ <a
+ style={{visibility: previewLinkVisible ? 'visible' : 'hidden'}}
+ onClick={this.showPreview}
+ className='textbox-preview-link'
+ >
+ {this.state.preview ? 'Edit message' : 'Preview'}
+ </a>
+ );
+ }
+
return (
<div
ref='wrapper'
className='textarea-wrapper'
>
- <CommandList
- ref='commands'
- addCommand={this.addCommand}
- channelId={this.props.channelId}
- />
- <textarea
+ <SuggestionBox
id={this.props.id}
ref='message'
className={`form-control custom-textarea ${this.state.connection}`}
+ type='textarea'
spellCheck='true'
autoComplete='off'
autoCorrect='off'
@@ -301,19 +175,39 @@ export default class Textbox extends React.Component {
maxLength={Constants.MAX_POST_LEN}
placeholder={this.props.createMessage}
value={this.props.messageText}
- onInput={this.handleChange}
- onChange={this.handleChange}
+ onUserInput={this.props.onUserInput}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onPaste={this.handlePaste}
+ style={{visibility: this.state.preview ? 'hidden' : 'visible'}}
+ listComponent={SuggestionList}
+ providers={this.suggestionProviders}
/>
+ <div
+ ref='preview'
+ className='form-control custom-textarea textbox-preview-area'
+ style={{display: this.state.preview ? 'block' : 'none'}}
+ dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.messageText) : ''}}
+ >
+ </div>
+ {previewLink}
+ <a
+ onClick={this.showHelp}
+ className='textbox-help-link'
+ >
+ {'Help'}
+ </a>
</div>
);
}
}
+Textbox.defaultProps = {
+ supportsCommands: true
+};
+
Textbox.propTypes = {
id: React.PropTypes.string.isRequired,
channelId: React.PropTypes.string,
@@ -322,5 +216,6 @@ Textbox.propTypes = {
onKeyPress: React.PropTypes.func.isRequired,
onHeightChange: React.PropTypes.func,
createMessage: React.PropTypes.string.isRequired,
- onKeyDown: React.PropTypes.func
+ onKeyDown: React.PropTypes.func,
+ supportsCommands: React.PropTypes.bool.isRequired
};
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
index c37739b9c..cffff6ee7 100644
--- a/web/react/components/time_since.jsx
+++ b/web/react/components/time_since.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
+import * as Utils from '../utils/utils.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -34,7 +34,7 @@ export default class TimeSince extends React.Component {
placement='top'
overlay={tooltip}
>
- <time className='post-profile-time'>
+ <time className='post__time'>
{Utils.displayDateTime(this.props.eventTime)}
</time>
</OverlayTrigger>
diff --git a/web/react/components/toggle_modal_button.jsx b/web/react/components/toggle_modal_button.jsx
new file mode 100644
index 000000000..ce8ff3f60
--- /dev/null
+++ b/web/react/components/toggle_modal_button.jsx
@@ -0,0 +1,73 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ModalToggleButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.show = this.show.bind(this);
+ this.hide = this.hide.bind(this);
+
+ this.state = {
+ show: false
+ };
+ }
+
+ show() {
+ this.setState({show: true});
+ }
+
+ hide() {
+ this.setState({show: false});
+ }
+
+ render() {
+ const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-redeclare
+
+ // allow callers to provide an onClick which will be called before the modal is shown
+ let clickHandler = this.show;
+ if (onClick) {
+ clickHandler = () => {
+ onClick();
+
+ this.show();
+ };
+ }
+
+ // this assumes that all modals will have a show property and an onHide event
+ const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
+ show: this.state.show,
+ onHide: () => {
+ this.hide();
+
+ if (dialogProps.onHide) {
+ dialogProps.onHide();
+ }
+ }
+ }));
+
+ // nesting the dialog in the anchor tag looks like it shouldn't work, but it does due to how react-bootstrap
+ // renders modals at the top level of the DOM instead of where you specify in the virtual DOM
+ return (
+ <a
+ {...props}
+ href='#'
+ onClick={clickHandler}
+ >
+ {children}
+ {dialog}
+ </a>
+ );
+ }
+}
+
+ModalToggleButton.propTypes = {
+ children: React.PropTypes.node.isRequired,
+ dialogType: React.PropTypes.func.isRequired,
+ dialogProps: React.PropTypes.object,
+ onClick: React.PropTypes.func
+};
+
+ModalToggleButton.defaultProps = {
+ dialogProps: {}
+};
diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx
index 66ca556c6..9360d31f8 100644
--- a/web/react/components/tutorial/tutorial_intro_screens.jsx
+++ b/web/react/components/tutorial/tutorial_intro_screens.jsx
@@ -1,14 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const UserStore = require('../../stores/user_store.jsx');
-const ChannelStore = require('../../stores/channel_store.jsx');
-const TeamStore = require('../../stores/team_store.jsx');
-const PreferenceStore = require('../../stores/preference_store.jsx');
-const Utils = require('../../utils/utils.jsx');
-const AsyncClient = require('../../utils/async_client.jsx');
-
-const Constants = require('../../utils/constants.jsx');
+import UserStore from '../../stores/user_store.jsx';
+import ChannelStore from '../../stores/channel_store.jsx';
+import TeamStore from '../../stores/team_store.jsx';
+import PreferenceStore from '../../stores/preference_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+
+import Constants from '../../utils/constants.jsx';
const Preferences = Constants.Preferences;
const NUM_SCREENS = 3;
@@ -41,6 +41,11 @@ export default class TutorialIntroScreens extends React.Component {
componentDidMount() {
$('.tutorials__scroll').perfectScrollbar();
}
+ skipTutorial(e) {
+ e.preventDefault();
+ const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
+ AsyncClient.savePreferences([preference]);
+ }
createScreen() {
switch (this.state.currentScreen) {
case 0:
@@ -176,6 +181,13 @@ export default class TutorialIntroScreens extends React.Component {
>
{'Next'}
</button>
+ <a
+ className='tutorial-skip'
+ href='#'
+ onClick={this.skipTutorial}
+ >
+ {'Skip tutorial'}
+ </a>
</div>
</div>
</div>
diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx
index 75d73e920..d7c67cc9c 100644
--- a/web/react/components/tutorial/tutorial_tip.jsx
+++ b/web/react/components/tutorial/tutorial_tip.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const UserStore = require('../../stores/user_store.jsx');
-const PreferenceStore = require('../../stores/preference_store.jsx');
-const AsyncClient = require('../../utils/async_client.jsx');
+import UserStore from '../../stores/user_store.jsx';
+import PreferenceStore from '../../stores/preference_store.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
-const Constants = require('../../utils/constants.jsx');
+import Constants from '../../utils/constants.jsx';
const Preferences = Constants.Preferences;
const Overlay = ReactBootstrap.Overlay;
@@ -51,21 +51,22 @@ export default class TutorialTip extends React.Component {
const dots = [];
if (this.props.screens.length > 1) {
for (let i = 0; i < this.props.screens.length; i++) {
+ let className = 'circle';
if (i === this.state.currentScreen) {
- dots.push(
- <div
- className='circle active'
- key={'dotactive' + i}
- />
- );
- } else {
- dots.push(
- <div
- className='circle'
- key={'dotinactive' + i}
- />
- );
+ className += ' active';
}
+
+ dots.push(
+ <a
+ href='#'
+ key={'dotactive' + i}
+ className={className}
+ onClick={(e) => { //eslint-disable-line no-loop-func
+ e.preventDefault();
+ this.setState({currentScreen: i});
+ }}
+ />
+ );
}
}
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index eb0a8f0ca..385cd0f52 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../utils/utils.jsx');
-var UserStore = require('../stores/user_store.jsx');
+import * as Utils from '../utils/utils.jsx';
+import UserStore from '../stores/user_store.jsx';
var Popover = ReactBootstrap.Popover;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -29,7 +29,7 @@ export default class UserProfile extends React.Component {
return {profile: {id: '0', username: '...'}};
}
- return {profile: profile};
+ return {profile};
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -43,7 +43,7 @@ export default class UserProfile extends React.Component {
onChange(userId) {
if (!userId || userId === this.props.userId) {
var newState = this.getStateFromStores(this.props.userId);
- if (!Utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -54,20 +54,27 @@ export default class UserProfile extends React.Component {
}
}
render() {
- var name = this.state.profile.username;
+ var name = Utils.displayUsername(this.state.profile.id);
if (this.props.overwriteName) {
name = this.props.overwriteName;
+ } else if (!name) {
+ name = '...';
}
if (this.props.disablePopover) {
return <div>{name}</div>;
}
+ var profileImg = '/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex();
+ if (this.props.overwriteImage) {
+ profileImg = this.props.overwriteImage;
+ }
+
var dataContent = [];
dataContent.push(
<img
className='user-popover__image'
- src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()}
+ src={profileImg}
height='128'
width='128'
key='user-popover-image'
@@ -107,7 +114,7 @@ export default class UserProfile extends React.Component {
rootClose={true}
overlay={
<Popover
- title={this.state.profile.username}
+ title={name}
id='user-profile-popover'
>
{dataContent}
@@ -128,10 +135,12 @@ export default class UserProfile extends React.Component {
UserProfile.defaultProps = {
userId: '',
overwriteName: '',
+ overwriteImage: '',
disablePopover: false
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
overwriteName: React.PropTypes.string,
+ overwriteImage: React.PropTypes.string,
disablePopover: React.PropTypes.bool
};
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 3dbed72c3..35f836adb 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Constants = require('../../utils/constants.jsx');
+import Constants from '../../utils/constants.jsx';
export default class CustomThemeChooser extends React.Component {
constructor(props) {
@@ -14,7 +14,10 @@ export default class CustomThemeChooser extends React.Component {
this.state = {};
}
componentDidMount() {
- $('.color-picker').colorpicker().on('changeColor', this.onPickerChange);
+ $('.color-picker').colorpicker({
+ format: 'hex'
+ });
+ $('.color-picker').on('changeColor', this.onPickerChange);
}
onPickerChange(e) {
const theme = this.props.theme;
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
index 4d594bb1b..3df9dfedf 100644
--- a/web/react/components/user_settings/import_theme_modal.jsx
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -1,14 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const ModalStore = require('../../stores/modal_store.jsx');
-const UserStore = require('../../stores/user_store.jsx');
-const Utils = require('../../utils/utils.jsx');
-const Client = require('../../utils/client.jsx');
+import ModalStore from '../../stores/modal_store.jsx';
+import UserStore from '../../stores/user_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
+import * as Client from '../../utils/client.jsx';
const Modal = ReactBootstrap.Modal;
-const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
-const Constants = require('../../utils/constants.jsx');
+import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
+import Constants from '../../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
export default class ImportThemeModal extends React.Component {
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index 128c011ea..9ebb55646 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Utils = require('../../utils/utils.jsx');
-var Constants = require('../../utils/constants.jsx');
-var ChannelStore = require('../../stores/channel_store.jsx');
-var LoadingScreen = require('../loading_screen.jsx');
+import * as Client from '../../utils/client.jsx';
+import * as Utils from '../../utils/utils.jsx';
+import Constants from '../../utils/constants.jsx';
+import ChannelStore from '../../stores/channel_store.jsx';
+import LoadingScreen from '../loading_screen.jsx';
export default class ManageIncomingHooks extends React.Component {
constructor() {
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 7b7cf7401..9c0fe3709 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-const LoadingScreen = require('../loading_screen.jsx');
+import LoadingScreen from '../loading_screen.jsx';
-const ChannelStore = require('../../stores/channel_store.jsx');
+import ChannelStore from '../../stores/channel_store.jsx';
-const Client = require('../../utils/client.jsx');
-const Constants = require('../../utils/constants.jsx');
+import * as Client from '../../utils/client.jsx';
+import Constants from '../../utils/constants.jsx';
export default class ManageOutgoingHooks extends React.Component {
constructor() {
@@ -188,7 +188,7 @@ export default class ManageOutgoingHooks extends React.Component {
key={hook.id}
className='webhook__item'
>
- <div className='padding-top x2'>
+ <div className='padding-top x2 webhook__url'>
<strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
</div>
{channelDiv}
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index 22cfcebcd..9889bff5c 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var Utils = require('../../utils/utils.jsx');
-var Constants = require('../../utils/constants.jsx');
+import * as Utils from '../../utils/utils.jsx';
+import Constants from '../../utils/constants.jsx';
export default class PremadeThemeChooser extends React.Component {
constructor(props) {
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index e089ce973..54d98bbde 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -1,16 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
-var DisplayTab = require('./user_settings_display.jsx');
-var AdvancedTab = require('./user_settings_advanced.jsx');
+import UserStore from '../../stores/user_store.jsx';
+import * as utils from '../../utils/utils.jsx';
+import NotificationsTab from './user_settings_notifications.jsx';
+import SecurityTab from './user_settings_security.jsx';
+import GeneralTab from './user_settings_general.jsx';
+import AppearanceTab from './user_settings_appearance.jsx';
+import DeveloperTab from './user_settings_developer.jsx';
+import IntegrationsTab from './user_settings_integrations.jsx';
+import DisplayTab from './user_settings_display.jsx';
+import AdvancedTab from './user_settings_advanced.jsx';
export default class UserSettings extends React.Component {
constructor(props) {
@@ -36,7 +36,7 @@ export default class UserSettings extends React.Component {
onListenerChange() {
var user = UserStore.getCurrentUser();
- if (!utils.areStatesEqual(this.state.user, user)) {
+ if (!utils.areObjectsEqual(this.state.user, user)) {
this.setState({user});
}
}
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
index 2616981ba..c15936ccd 100644
--- a/web/react/components/user_settings/user_settings_advanced.jsx
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const Client = require('../../utils/client.jsx');
-const SettingItemMin = require('../setting_item_min.jsx');
-const SettingItemMax = require('../setting_item_max.jsx');
-const Constants = require('../../utils/constants.jsx');
-const PreferenceStore = require('../../stores/preference_store.jsx');
+import * as Client from '../../utils/client.jsx';
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+import Constants from '../../utils/constants.jsx';
+import PreferenceStore from '../../stores/preference_store.jsx';
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
export default class AdvancedSettingsDisplay extends React.Component {
constructor(props) {
@@ -13,21 +14,33 @@ export default class AdvancedSettingsDisplay extends React.Component {
this.updateSection = this.updateSection.bind(this);
this.updateSetting = this.updateSetting.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
+ this.toggleFeature = this.toggleFeature.bind(this);
+ this.saveEnabledFeatures = this.saveEnabledFeatures.bind(this);
- this.state = this.setupInitialState();
- }
+ const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
+ const advancedSettings = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
+ const settings = {
+ send_on_ctrl_enter: PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value
+ };
- setupInitialState() {
- const sendOnCtrlEnter = PreferenceStore.getPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- 'send_on_ctrl_enter',
- {value: 'false'}
- ).value;
+ let enabledFeatures = 0;
+ advancedSettings.forEach((setting) => {
+ preReleaseFeaturesKeys.forEach((key) => {
+ const feature = PreReleaseFeatures[key];
+ if (setting.name === Constants.FeatureTogglePrefix + feature.label) {
+ settings[setting.name] = setting.value;
+ if (setting.value === 'true') {
+ enabledFeatures++;
+ }
+ }
+ });
+ });
- return {
- settings: {send_on_ctrl_enter: sendOnCtrlEnter}
- };
+ this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures};
}
updateSetting(setting, value) {
@@ -36,14 +49,45 @@ export default class AdvancedSettingsDisplay extends React.Component {
this.setState(settings);
}
- handleSubmit(setting) {
- const preference = PreferenceStore.setPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- setting,
- this.state.settings[setting]
- );
+ toggleFeature(feature, checked) {
+ const settings = this.state.settings;
+ settings[Constants.FeatureTogglePrefix + feature] = String(checked);
+
+ let enabledFeatures = 0;
+ Object.keys(this.state.settings).forEach((setting) => {
+ if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0 && this.state.settings[setting] === 'true') {
+ enabledFeatures++;
+ }
+ });
+
+ this.setState({settings, enabledFeatures});
+ }
+
+ saveEnabledFeatures() {
+ const features = [];
+ Object.keys(this.state.settings).forEach((setting) => {
+ if (setting.lastIndexOf(Constants.FeatureTogglePrefix) === 0) {
+ features.push(setting);
+ }
+ });
+
+ this.handleSubmit(features);
+ }
- Client.savePreferences([preference],
+ handleSubmit(settings) {
+ const preferences = [];
+
+ (Array.isArray(settings) ? settings : [settings]).forEach((setting) => {
+ preferences.push(
+ PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ String(this.state.settings[setting])
+ )
+ );
+ });
+
+ Client.savePreferences(preferences,
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -118,6 +162,66 @@ export default class AdvancedSettingsDisplay extends React.Component {
);
}
+ let previewFeaturesSection;
+ let previewFeaturesSectionDivider;
+ if (this.state.preReleaseFeaturesKeys.length > 0) {
+ previewFeaturesSectionDivider = (
+ <div className='divider-light'/>
+ );
+
+ if (this.props.activeSection === 'advancedPreviewFeatures') {
+ const inputs = [];
+
+ this.state.preReleaseFeaturesKeys.forEach((key) => {
+ const feature = this.state.preReleaseFeatures[key];
+ inputs.push(
+ <div key={'advancedPreviewFeatures_' + feature.label}>
+ <div className='checkbox'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.settings[Constants.FeatureTogglePrefix + feature.label] === 'true'}
+ onChange={(e) => {
+ this.toggleFeature(feature.label, e.target.checked);
+ }}
+ />
+ {feature.description}
+ </label>
+ </div>
+ </div>
+ );
+ });
+
+ inputs.push(
+ <div key='advancedPreviewFeatures_helptext'>
+ <br/>
+ {'Check any pre-released features you\'d like to preview. You may also need to refresh the page before the setting will take effect.'}
+ </div>
+ );
+
+ previewFeaturesSection = (
+ <SettingItemMax
+ title='Preview pre-release features'
+ inputs={inputs}
+ submit={this.saveEnabledFeatures}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ previewFeaturesSection = (
+ <SettingItemMin
+ title='Preview pre-release features'
+ describe={this.state.enabledFeatures + (this.state.enabledFeatures === 1 ? ' Feature ' : ' Features ') + 'enabled'}
+ updateSection={() => this.props.updateSection('advancedPreviewFeatures')}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -145,6 +249,8 @@ export default class AdvancedSettingsDisplay extends React.Component {
<h3 className='tab-header'>{'Advanced Settings'}</h3>
<div className='divider-dark first'/>
{ctrlSendSection}
+ {previewFeaturesSectionDivider}
+ {previewFeaturesSection}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index d73b5f476..ad41ab771 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -1,14 +1,16 @@
// Copyright (c) 2015 Mattermost, 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');
+import CustomThemeChooser from './custom_theme_chooser.jsx';
+import PremadeThemeChooser from './premade_theme_chooser.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');
+import UserStore from '../../stores/user_store.jsx';
+
+import AppDispatcher from '../../dispatcher/app_dispatcher.jsx';
+import * as Client from '../../utils/client.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+import Constants from '../../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
export default class UserSettingsAppearance extends React.Component {
@@ -66,7 +68,7 @@ export default class UserSettingsAppearance extends React.Component {
onChange() {
const newState = this.getStateFromStores();
- if (!Utils.areStatesEqual(this.state, newState)) {
+ if (!Utils.areObjectsEqual(this.state, newState)) {
this.setState(newState);
}
diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index e6adba1d4..01e13be57 100644
--- a/web/react/components/user_settings/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -1,18 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingItemMin = require('../setting_item_min.jsx');
-var SettingItemMax = require('../setting_item_max.jsx');
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+import * as EventHelpers from '../../dispatcher/event_helpers.jsx';
export default class DeveloperTab extends React.Component {
constructor(props) {
super(props);
+ this.register = this.register.bind(this);
+
this.state = {};
}
register() {
- $('#user_settings1').modal('hide');
- $('#register_app').modal('show');
+ this.props.closeModal();
+ EventHelpers.showRegisterAppModal();
}
render() {
var appSection;
@@ -21,7 +24,10 @@ export default class DeveloperTab extends React.Component {
var inputs = [];
inputs.push(
- <div className='form-group'>
+ <div
+ key='registerbtn'
+ className='form-group'
+ >
<div className='col-sm-7'>
<a
className='btn btn-sm btn-primary'
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 43c8d33d1..c464258de 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -6,14 +6,17 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
import PreferenceStore from '../../stores/preference_store.jsx';
+import * as Utils from '../../utils/utils.jsx';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
+ const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT});
return {
militaryTime: militaryTime.value,
- nameFormat: nameFormat.value
+ nameFormat: nameFormat.value,
+ selectedFont: selectedFont.value
};
}
@@ -24,15 +27,20 @@ export default class UserSettingsDisplay extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClockRadio = this.handleClockRadio.bind(this);
this.handleNameRadio = this.handleNameRadio.bind(this);
+ this.handleFont = this.handleFont.bind(this);
this.updateSection = this.updateSection.bind(this);
this.state = getDisplayStateFromStores();
+ this.selectedFont = this.state.selectedFont;
}
handleSubmit() {
const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
+ const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont);
- savePreferences([timePreference, namePreference],
+ this.selectedFont = this.state.selectedFont;
+
+ savePreferences([timePreference, namePreference, fontPreference],
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -48,6 +56,10 @@ export default class UserSettingsDisplay extends React.Component {
handleNameRadio(nameFormat) {
this.setState({nameFormat});
}
+ handleFont(selectedFont) {
+ Utils.applyFont(selectedFont);
+ this.setState({selectedFont});
+ }
updateSection(section) {
this.setState(getDisplayStateFromStores());
this.props.updateSection(section);
@@ -56,6 +68,8 @@ export default class UserSettingsDisplay extends React.Component {
const serverError = this.state.serverError || null;
let clockSection;
let nameFormatSection;
+ let fontSection;
+
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -209,6 +223,66 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === 'font') {
+ const options = [];
+ Object.keys(Constants.FONTS).forEach((fontName, idx) => {
+ const className = Constants.FONTS[fontName];
+ options.push(
+ <option
+ key={'font_' + idx}
+ value={fontName}
+ className={className}
+ >
+ {fontName}
+ </option>
+ );
+ });
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div
+ className='dropdown'
+ >
+ <select
+ className='form-control'
+ type='text'
+ value={this.state.selectedFont}
+ onChange={(e) => this.handleFont(e.target.value)}
+ >
+ {options}
+ </select>
+ </div>
+ <div><br/>{'Select the font displayed in the Mattermost user interface.'}</div>
+ </div>
+ ];
+
+ fontSection = (
+ <SettingItemMax
+ title='Display Font'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ if (this.selectedFont !== this.state.selectedFont) {
+ this.handleFont(this.selectedFont);
+ }
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ fontSection = (
+ <SettingItemMin
+ title='Display Font'
+ describe={this.state.selectedFont}
+ updateSection={() => {
+ this.props.updateSection('font');
+ }}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -235,6 +309,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='user-settings'>
<h3 className='tab-header'>{'Display Settings'}</h3>
<div className='divider-dark first'/>
+ {fontSection}
+ <div className='divider-dark'/>
{clockSection}
<div className='divider-dark'/>
{nameFormatSection}
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 1bfae6930..962efd7a2 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -1,15 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../../stores/user_store.jsx');
-var ErrorStore = require('../../stores/error_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');
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+import SettingPicture from '../setting_picture.jsx';
+
+import UserStore from '../../stores/user_store.jsx';
+import ErrorStore from '../../stores/error_store.jsx';
+
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import * as Utils from '../../utils/utils.jsx';
export default class UserSettingsGeneralTab extends React.Component {
constructor(props) {
@@ -32,17 +33,15 @@ export default class UserSettingsGeneralTab extends React.Component {
this.updatePicture = this.updatePicture.bind(this);
this.updateSection = this.updateSection.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
-
this.state = this.setupInitialState(props);
}
submitUsername(e) {
e.preventDefault();
- var user = this.props.user;
- var username = this.state.username.trim().toLowerCase();
+ const user = Object.assign({}, this.props.user);
+ const username = this.state.username.trim().toLowerCase();
- var usernameError = utils.isValidUsername(username);
+ const usernameError = Utils.isValidUsername(username);
if (usernameError === 'Cannot use a reserved word as a username.') {
this.setState({clientError: 'This username is reserved, please choose a new one.'});
return;
@@ -52,7 +51,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}
if (user.username === username) {
- this.setState({clientError: 'You must submit a new username'});
+ this.setState({clientError: 'You must submit a new username.', emailError: '', serverError: ''});
return;
}
@@ -63,11 +62,11 @@ export default class UserSettingsGeneralTab extends React.Component {
submitNickname(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- var nickname = this.state.nickname.trim();
+ const user = Object.assign({}, this.props.user);
+ const nickname = this.state.nickname.trim();
if (user.nickname === nickname) {
- this.setState({clientError: 'You must submit a new nickname'});
+ this.setState({clientError: 'You must submit a new nickname.', emailError: '', serverError: ''});
return;
}
@@ -78,12 +77,12 @@ export default class UserSettingsGeneralTab extends React.Component {
submitName(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- var firstName = this.state.firstName.trim();
- var lastName = this.state.lastName.trim();
+ const user = Object.assign({}, this.props.user);
+ const firstName = this.state.firstName.trim();
+ const lastName = this.state.lastName.trim();
if (user.first_name === firstName && user.last_name === lastName) {
- this.setState({clientError: 'You must submit a new first or last name'});
+ this.setState({clientError: 'You must submit a new first or last name.', emailError: '', serverError: ''});
return;
}
@@ -95,21 +94,21 @@ export default class UserSettingsGeneralTab extends React.Component {
submitEmail(e) {
e.preventDefault();
- var user = UserStore.getCurrentUser();
- var email = this.state.email.trim().toLowerCase();
- var confirmEmail = this.state.confirmEmail.trim().toLowerCase();
+ const user = Object.assign({}, this.props.user);
+ const email = this.state.email.trim().toLowerCase();
+ const confirmEmail = this.state.confirmEmail.trim().toLowerCase();
if (user.email === email) {
return;
}
- if (email === '' || !utils.isEmail(email)) {
- this.setState({emailError: 'Please enter a valid email address'});
+ if (email === '' || !Utils.isEmail(email)) {
+ this.setState({emailError: 'Please enter a valid email address.', clientError: '', serverError: ''});
return;
}
if (email !== confirmEmail) {
- this.setState({emailError: 'The new emails you entered do not match'});
+ this.setState({emailError: 'The new emails you entered do not match.', clientError: '', serverError: ''});
return;
}
@@ -117,7 +116,7 @@ export default class UserSettingsGeneralTab extends React.Component {
this.submitUser(user, true);
}
submitUser(user, emailUpdated) {
- client.updateUser(user,
+ Client.updateUser(user,
() => {
this.updateSection('');
AsyncClient.getMe();
@@ -130,13 +129,13 @@ export default class UserSettingsGeneralTab extends React.Component {
}
},
(err) => {
- var state = this.setupInitialState(this.props);
+ let serverError;
if (err.message) {
- state.serverError = err.message;
+ serverError = err.message;
} else {
- state.serverError = err;
+ serverError = err;
}
- this.setState(state);
+ this.setState({serverError, emailError: '', clientError: ''});
}
);
}
@@ -151,10 +150,10 @@ export default class UserSettingsGeneralTab extends React.Component {
return;
}
- var picture = this.state.picture;
+ const picture = this.state.picture;
if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') {
- this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures'});
+ this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures.'});
return;
}
@@ -162,17 +161,17 @@ export default class UserSettingsGeneralTab extends React.Component {
formData.append('image', picture, picture.name);
this.setState({loadingPicture: true});
- client.uploadProfileImage(formData,
- function imageUploadSuccess() {
+ Client.uploadProfileImage(formData,
+ () => {
this.submitActive = false;
AsyncClient.getMe();
window.location.reload();
- }.bind(this),
- function imageUploadFailure(err) {
+ },
+ (err) => {
var state = this.setupInitialState(this.props);
state.serverError = err.message;
this.setState(state);
- }.bind(this)
+ }
);
}
updateUsername(e) {
@@ -205,34 +204,34 @@ export default class UserSettingsGeneralTab extends React.Component {
}
updateSection(section) {
const emailChangeInProgress = this.state.emailChangeInProgress;
- this.setState(assign({}, this.setupInitialState(this.props), {emailChangeInProgress: emailChangeInProgress, clientError: '', serverError: '', emailError: ''}));
+ this.setState(Object.assign({}, this.setupInitialState(this.props), {emailChangeInProgress, clientError: '', serverError: '', emailError: ''}));
this.submitActive = false;
this.props.updateSection(section);
}
setupInitialState(props) {
- var user = props.user;
+ const user = props.user;
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false};
}
render() {
- var user = this.props.user;
+ const user = this.props.user;
- var clientError = null;
+ let clientError = null;
if (this.state.clientError) {
clientError = this.state.clientError;
}
- var serverError = null;
+ let serverError = null;
if (this.state.serverError) {
serverError = this.state.serverError;
}
- var emailError = null;
+ let emailError = null;
if (this.state.emailError) {
emailError = this.state.emailError;
}
- var nameSection;
- var inputs = [];
+ let nameSection;
+ const inputs = [];
if (this.props.activeSection === 'name') {
inputs.push(
@@ -298,15 +297,15 @@ export default class UserSettingsGeneralTab extends React.Component {
submit={this.submitName}
server_error={serverError}
client_error={clientError}
- updateSection={function clearSection(e) {
+ updateSection={(e) => {
this.updateSection('');
e.preventDefault();
- }.bind(this)}
+ }}
extraInfo={extraInfo}
/>
);
} else {
- var fullName = '';
+ let fullName = '';
if (user.first_name && user.last_name) {
fullName = user.first_name + ' ' + user.last_name;
@@ -320,17 +319,17 @@ export default class UserSettingsGeneralTab extends React.Component {
<SettingItemMin
title='Full Name'
describe={fullName}
- updateSection={function updateNameSection() {
+ updateSection={() => {
this.updateSection('name');
- }.bind(this)}
+ }}
/>
);
}
- var nicknameSection;
+ let nicknameSection;
if (this.props.activeSection === 'nickname') {
let nicknameLabel = 'Nickname';
- if (utils.isMobile()) {
+ if (Utils.isMobile()) {
nicknameLabel = '';
}
@@ -364,10 +363,10 @@ export default class UserSettingsGeneralTab extends React.Component {
submit={this.submitNickname}
server_error={serverError}
client_error={clientError}
- updateSection={function clearSection(e) {
+ updateSection={(e) => {
this.updateSection('');
e.preventDefault();
- }.bind(this)}
+ }}
extraInfo={extraInfo}
/>
);
@@ -376,17 +375,17 @@ export default class UserSettingsGeneralTab extends React.Component {
<SettingItemMin
title='Nickname'
describe={UserStore.getCurrentUser().nickname}
- updateSection={function updateNicknameSection() {
+ updateSection={() => {
this.updateSection('nickname');
- }.bind(this)}
+ }}
/>
);
}
- var usernameSection;
+ let usernameSection;
if (this.props.activeSection === 'username') {
let usernameLabel = 'Username';
- if (utils.isMobile()) {
+ if (Utils.isMobile()) {
usernameLabel = '';
}
@@ -416,10 +415,10 @@ export default class UserSettingsGeneralTab extends React.Component {
submit={this.submitUsername}
server_error={serverError}
client_error={clientError}
- updateSection={function clearSection(e) {
+ updateSection={(e) => {
this.updateSection('');
e.preventDefault();
- }.bind(this)}
+ }}
extraInfo={extraInfo}
/>
);
@@ -428,22 +427,23 @@ export default class UserSettingsGeneralTab extends React.Component {
<SettingItemMin
title='Username'
describe={UserStore.getCurrentUser().username}
- updateSection={function updateUsernameSection() {
+ updateSection={() => {
this.updateSection('username');
- }.bind(this)}
+ }}
/>
);
}
- var emailSection;
+
+ let emailSection;
if (this.props.activeSection === 'email') {
const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true';
const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true';
- let helpText = 'Email is used for notifications, and requires verification if changed.';
+ let helpText = 'Email is used for sign-in, notifications, and password reset. Email requires verification if changed.';
if (!emailEnabled) {
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>;
} else if (!emailVerificationEnabled) {
- helpText = 'Email is used for notifications.';
+ helpText = 'Email is used for sign-in, notifications, and password reset.';
} else if (this.state.emailChangeInProgress) {
const newEmail = UserStore.getCurrentUser().email;
if (newEmail) {
@@ -507,10 +507,10 @@ export default class UserSettingsGeneralTab extends React.Component {
submit={submit}
server_error={serverError}
client_error={emailError}
- updateSection={function clearSection(e) {
+ updateSection={(e) => {
this.updateSection('');
e.preventDefault();
- }.bind(this)}
+ }}
/>
);
} else {
@@ -534,26 +534,26 @@ export default class UserSettingsGeneralTab extends React.Component {
<SettingItemMin
title='Email'
describe={describe}
- updateSection={function updateEmailSection() {
+ updateSection={() => {
this.updateSection('email');
- }.bind(this)}
+ }}
/>
);
}
- var pictureSection;
+ let pictureSection;
if (this.props.activeSection === 'picture') {
pictureSection = (
<SettingPicture
title='Profile Picture'
submit={this.submitPicture}
- src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + utils.getSessionIndex()}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + Utils.getSessionIndex()}
server_error={serverError}
client_error={clientError}
- updateSection={function clearSection(e) {
+ updateSection={(e) => {
this.updateSection('');
e.preventDefault();
- }.bind(this)}
+ }}
picture={this.state.picture}
pictureChange={this.updatePicture}
submitActive={this.submitActive}
@@ -561,17 +561,17 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
} else {
- var minMessage = 'Click \'Edit\' to upload an image.';
+ let minMessage = 'Click \'Edit\' to upload an image.';
if (user.last_picture_update) {
- minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update);
+ minMessage = 'Image last updated ' + Utils.displayDate(user.last_picture_update);
}
pictureSection = (
<SettingItemMin
title='Profile Picture'
describe={minMessage}
- updateSection={function updatePictureSection() {
+ updateSection={() => {
this.updateSection('picture');
- }.bind(this)}
+ }}
/>
);
}
@@ -619,10 +619,10 @@ 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,
+ user: React.PropTypes.object.isRequired,
+ updateSection: React.PropTypes.func.isRequired,
+ updateTab: React.PropTypes.func.isRequired,
+ activeSection: React.PropTypes.string.isRequired,
closeModal: React.PropTypes.func.isRequired,
collapseModal: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 744a6beea..a86510eb3 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, 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');
-var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx');
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+import ManageIncomingHooks from './manage_incoming_hooks.jsx';
+import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 4dcf32cb9..36e1aa217 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -1,15 +1,16 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const ConfirmModal = require('../confirm_modal.jsx');
+import ConfirmModal from '../confirm_modal.jsx';
const Modal = ReactBootstrap.Modal;
-const SettingsSidebar = require('../settings_sidebar.jsx');
-const UserSettings = require('./user_settings.jsx');
+import SettingsSidebar from '../settings_sidebar.jsx';
+import UserSettings from './user_settings.jsx';
export default class UserSettingsModal extends React.Component {
constructor(props) {
super(props);
+ this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleHidden = this.handleHidden.bind(this);
this.handleCollapse = this.handleCollapse.bind(this);
@@ -33,12 +34,24 @@ export default class UserSettingsModal extends React.Component {
this.requireConfirm = false;
}
+ componentDidMount() {
+ if (this.props.show) {
+ this.handleShow();
+ }
+ }
+
componentDidUpdate(prevProps) {
- if (!prevProps.show && this.props.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300);
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
- }
+ if (this.props.show && !prevProps.show) {
+ this.handleShow();
+ }
+ }
+
+ handleShow() {
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
+ } else {
+ $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
}
}
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index c6f47804f..f762405af 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -1,16 +1,18 @@
// Copyright (c) 2015 Mattermost, 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');
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+
+import UserStore from '../../stores/user_store.jsx';
+
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import * as Utils from '../../utils/utils.jsx';
function getNotificationsStateFromStores() {
var user = UserStore.getCurrentUser();
- var soundNeeded = !utils.isBrowserFirefox();
+ var soundNeeded = !Utils.isBrowserFirefox();
var sound = 'true';
if (user.notify_props && user.notify_props.desktop_sound) {
@@ -76,7 +78,9 @@ export default class NotificationsTab extends React.Component {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleCancel = this.handleCancel.bind(this);
this.updateSection = this.updateSection.bind(this);
+ this.updateState = this.updateState.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleNotifyRadio = this.handleNotifyRadio.bind(this);
this.handleEmailRadio = this.handleEmailRadio.bind(this);
@@ -116,7 +120,7 @@ export default class NotificationsTab extends React.Component {
data.all = this.state.allKey.toString();
data.channel = this.state.channelKey.toString();
- client.updateUserNotifyProps(data,
+ Client.updateUserNotifyProps(data,
function success() {
this.props.updateSection('');
AsyncClient.getMe();
@@ -126,10 +130,21 @@ export default class NotificationsTab extends React.Component {
}.bind(this)
);
}
+ handleCancel(e) {
+ this.updateState();
+ this.props.updateSection('');
+ e.preventDefault();
+ }
updateSection(section) {
- this.setState(getNotificationsStateFromStores());
+ this.updateState();
this.props.updateSection(section);
}
+ updateState() {
+ const newState = getNotificationsStateFromStores();
+ if (!Utils.areObjectsEqual(newState, this.state)) {
+ this.setState(newState);
+ }
+ }
componentDidMount() {
UserStore.addChangeListener(this.onListenerChange);
}
@@ -137,10 +152,7 @@ export default class NotificationsTab extends React.Component {
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
- var newState = getNotificationsStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.updateState();
}
handleNotifyRadio(notifyLevel) {
this.setState({notifyLevel: notifyLevel});
@@ -243,11 +255,6 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateDesktopSection = function updateDesktopSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
desktopSection = (
@@ -257,7 +264,7 @@ export default class NotificationsTab extends React.Component {
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateDesktopSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -322,11 +329,6 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateSoundSection = function updateSoundSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
soundSection = (
@@ -336,7 +338,7 @@ export default class NotificationsTab extends React.Component {
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateSoundSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -403,18 +405,13 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateEmailSection = function updateEmailSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
-
emailSection = (
<SettingItemMax
title='Email notifications'
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateEmailSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -510,7 +507,7 @@ export default class NotificationsTab extends React.Component {
}.bind(this);
inputs.push(
<div key='userNotificationAllOption'>
- <div className='checkbox'>
+ <div className='checkbox hidden'>
<label>
<input
type='checkbox'
@@ -564,17 +561,13 @@ export default class NotificationsTab extends React.Component {
</div>
);
- handleUpdateKeysSection = function updateKeysSection(e) {
- this.props.updateSection('');
- e.preventDefault();
- }.bind(this);
keysSection = (
<SettingItemMax
title='Words that trigger mentions'
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
- updateSection={handleUpdateKeysSection}
+ updateSection={this.handleCancel}
/>
);
} else {
@@ -588,9 +581,11 @@ export default class NotificationsTab extends React.Component {
if (this.state.mentionKey) {
keys.push('@' + user.username);
}
- if (this.state.allKey) {
- keys.push('@all');
- }
+
+ // if (this.state.allKey) {
+ // keys.push('@all');
+ // }
+
if (this.state.channelKey) {
keys.push('@channel');
}
@@ -649,7 +644,7 @@ export default class NotificationsTab extends React.Component {
ref='wrapper'
className='user-settings'
>
- <h3 className='tab-header'>Notifications</h3>
+ <h3 className='tab-header'>{'Notifications'}</h3>
<div className='divider-dark first'/>
{desktopSection}
<div className='divider-light'/>
diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index 61d13ed8b..fa2fecf07 100644
--- a/web/react/components/user_settings/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -1,46 +1,26 @@
// Copyright (c) 2015 Mattermost, 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 AccessHistoryModal = require('../access_history_modal.jsx');
-var ActivityLogModal = require('../activity_log_modal.jsx');
-var Client = require('../../utils/client.jsx');
-var AsyncClient = require('../../utils/async_client.jsx');
-var Constants = require('../../utils/constants.jsx');
+import SettingItemMin from '../setting_item_min.jsx';
+import SettingItemMax from '../setting_item_max.jsx';
+import AccessHistoryModal from '../access_history_modal.jsx';
+import ActivityLogModal from '../activity_log_modal.jsx';
+import ToggleModalButton from '../toggle_modal_button.jsx';
+import * as Client from '../../utils/client.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import Constants from '../../utils/constants.jsx';
export default class SecurityTab extends React.Component {
constructor(props) {
super(props);
- this.showAccessHistoryModal = this.showAccessHistoryModal.bind(this);
- this.showActivityLogModal = this.showActivityLogModal.bind(this);
- this.hideModals = this.hideModals.bind(this);
this.submitPassword = this.submitPassword.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.setupInitialState = this.setupInitialState.bind(this);
- const state = this.setupInitialState();
- state.showAccessHistoryModal = false;
- state.showActivityLogModal = false;
- this.state = state;
- }
- showAccessHistoryModal() {
- this.props.setEnforceFocus(false);
- this.setState({showAccessHistoryModal: true});
- }
- showActivityLogModal() {
- this.props.setEnforceFocus(false);
- this.setState({showActivityLogModal: true});
- }
- hideModals() {
- this.props.setEnforceFocus(true);
- this.setState({
- showAccessHistoryModal: false,
- showActivityLogModal: false
- });
+ this.state = this.setupInitialState();
}
submitPassword(e) {
e.preventDefault();
@@ -258,30 +238,20 @@ export default class SecurityTab extends React.Component {
{passwordSection}
<div className='divider-dark'/>
<br></br>
- <a
+ <ToggleModalButton
className='security-links theme'
- href='#'
- onClick={this.showAccessHistoryModal}
+ dialogType={AccessHistoryModal}
>
<i className='fa fa-clock-o'></i>View Access History
- </a>
+ </ToggleModalButton>
<b> </b>
- <a
+ <ToggleModalButton
className='security-links theme'
- href='#'
- onClick={this.showActivityLogModal}
+ dialogType={ActivityLogModal}
>
- <i className='fa fa-globe'></i>View and Logout of Active Sessions
- </a>
+ <i className='fa fa-clock-o'></i>{'View and Logout of Active Sessions'}
+ </ToggleModalButton>
</div>
- <AccessHistoryModal
- show={this.state.showAccessHistoryModal}
- onModalDismissed={this.hideModals}
- />
- <ActivityLogModal
- show={this.state.showActivityLogModal}
- onModalDismissed={this.hideModals}
- />
</div>
);
}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 92d7cd835..820f8fd8e 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-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');
+import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
+import Constants from '../utils/constants.jsx';
+import ViewImagePopoverBar from './view_image_popover_bar.jsx';
const Modal = ReactBootstrap.Modal;
const KeyCodes = Constants.KeyCodes;
@@ -423,24 +423,29 @@ export default class ViewImageModal extends React.Component {
onClick={this.props.onModalDismissed}
>
<div
- className={'image-wrapper ' + bgClass}
- onMouseEnter={this.onMouseEnterImage}
- onMouseLeave={this.onMouseLeaveImage}
- onClick={(e) => e.stopPropagation()}
+ className={'image-wrapper'}
+ onClick={this.props.onModalDismissed}
>
<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}
- getPublicLink={this.getPublicLink}
- />
+ className={bgClass}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <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}
+ getPublicLink={this.getPublicLink}
+ />
+ </div>
</div>
{leftArrow}
{rightArrow}