summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/activity_log_modal.jsx20
-rw-r--r--webapp/components/admin_console/admin_settings.jsx5
-rw-r--r--webapp/components/admin_console/admin_sidebar_header.jsx2
-rw-r--r--webapp/components/admin_console/admin_team_members_dropdown.jsx18
-rw-r--r--webapp/components/admin_console/brand_image_setting.jsx3
-rw-r--r--webapp/components/admin_console/cluster_table_container.jsx10
-rw-r--r--webapp/components/admin_console/compliance_reports.jsx3
-rw-r--r--webapp/components/admin_console/email_connection_test.jsx5
-rw-r--r--webapp/components/admin_console/ldap_test_button.jsx5
-rw-r--r--webapp/components/admin_console/license_settings.jsx8
-rw-r--r--webapp/components/admin_console/logs.jsx3
-rw-r--r--webapp/components/admin_console/policy_settings.jsx49
-rw-r--r--webapp/components/admin_console/post_edit_setting.jsx99
-rw-r--r--webapp/components/admin_console/purge_caches.jsx10
-rw-r--r--webapp/components/admin_console/radio_setting.jsx63
-rw-r--r--webapp/components/admin_console/recycle_db.jsx5
-rw-r--r--webapp/components/admin_console/reload_config.jsx8
-rw-r--r--webapp/components/admin_console/reset_password_modal.jsx5
-rw-r--r--webapp/components/admin_console/saml_settings.jsx9
-rw-r--r--webapp/components/admin_console/sync_now_button.jsx5
-rw-r--r--webapp/components/admin_console/team_users.jsx8
-rw-r--r--webapp/components/analytics/system_analytics.jsx34
-rw-r--r--webapp/components/authorize.jsx7
-rw-r--r--webapp/components/channel_header.jsx21
-rw-r--r--webapp/components/channel_invite_modal.jsx8
-rw-r--r--webapp/components/channel_members_dropdown.jsx246
-rw-r--r--webapp/components/channel_members_modal.jsx153
-rw-r--r--webapp/components/channel_select.jsx14
-rw-r--r--webapp/components/channel_switch_modal.jsx13
-rw-r--r--webapp/components/claim/components/email_to_ldap.jsx4
-rw-r--r--webapp/components/claim/components/email_to_oauth.jsx4
-rw-r--r--webapp/components/claim/components/oauth_to_email.jsx12
-rw-r--r--webapp/components/create_comment.jsx7
-rw-r--r--webapp/components/create_post.jsx3
-rw-r--r--webapp/components/delete_channel_modal.jsx14
-rw-r--r--webapp/components/delete_post_modal.jsx24
-rw-r--r--webapp/components/do_verify_email.jsx15
-rw-r--r--webapp/components/edit_channel_header_modal.jsx12
-rw-r--r--webapp/components/edit_channel_purpose_modal.jsx7
-rw-r--r--webapp/components/edit_post_modal.jsx27
-rw-r--r--webapp/components/file_attachment_list.jsx2
-rw-r--r--webapp/components/file_preview.jsx5
-rw-r--r--webapp/components/file_upload.jsx40
-rw-r--r--webapp/components/integrations/components/installed_oauth_app.jsx4
-rw-r--r--webapp/components/invite_member_modal.jsx4
-rw-r--r--webapp/components/logged_in.jsx25
-rw-r--r--webapp/components/login/login_controller.jsx77
-rw-r--r--webapp/components/member_list_channel.jsx179
-rw-r--r--webapp/components/member_list_team.jsx16
-rw-r--r--webapp/components/mfa/mfa_controller.jsx23
-rw-r--r--webapp/components/more_channels.jsx9
-rw-r--r--webapp/components/navbar.jsx22
-rw-r--r--webapp/components/new_channel_modal.jsx4
-rw-r--r--webapp/components/password_reset_form.jsx7
-rw-r--r--webapp/components/popover_list_members.jsx94
-rw-r--r--webapp/components/post_view/components/post.jsx16
-rw-r--r--webapp/components/post_view/components/post_attachment_oembed.jsx108
-rw-r--r--webapp/components/post_view/components/post_attachment_opengraph.jsx212
-rw-r--r--webapp/components/post_view/components/post_body.jsx4
-rw-r--r--webapp/components/post_view/components/post_body_additional_content.jsx93
-rw-r--r--webapp/components/post_view/components/post_image.jsx6
-rw-r--r--webapp/components/post_view/components/post_info.jsx40
-rw-r--r--webapp/components/post_view/components/post_list.jsx23
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx2
-rw-r--r--webapp/components/post_view/components/post_message_view.jsx38
-rw-r--r--webapp/components/post_view/components/post_time.jsx5
-rw-r--r--webapp/components/post_view/components/providers.json376
-rw-r--r--webapp/components/post_view/post_view_controller.jsx10
-rw-r--r--webapp/components/profile_popover.jsx35
-rw-r--r--webapp/components/rhs_comment.jsx28
-rw-r--r--webapp/components/rhs_root_post.jsx28
-rw-r--r--webapp/components/root.jsx27
-rw-r--r--webapp/components/search_bar.jsx31
-rw-r--r--webapp/components/search_results.jsx6
-rw-r--r--webapp/components/search_results_item.jsx4
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/setting_picture.jsx4
-rw-r--r--webapp/components/should_verify_email.jsx5
-rw-r--r--webapp/components/sidebar_header.jsx13
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx5
-rw-r--r--webapp/components/signup/components/signup_email.jsx29
-rw-r--r--webapp/components/signup/components/signup_ldap.jsx19
-rw-r--r--webapp/components/signup/signup_controller.jsx14
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx5
-rw-r--r--webapp/components/suggestion/channel_mention_provider.jsx114
-rw-r--r--webapp/components/suggestion/emoticon_provider.jsx23
-rw-r--r--webapp/components/suggestion/search_user_provider.jsx2
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx6
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx2
-rw-r--r--webapp/components/team_general_tab.jsx108
-rw-r--r--webapp/components/user_list_row.jsx2
-rw-r--r--webapp/components/user_profile.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx10
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx6
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx7
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx62
-rw-r--r--webapp/components/webrtc/components/webrtc_notification.jsx2
-rw-r--r--webapp/components/webrtc/webrtc_controller.jsx8
99 files changed, 1773 insertions, 1252 deletions
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index b907668f0..cd369f742 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -5,7 +5,6 @@ import LoadingScreen from './loading_screen.jsx';
import UserStore from 'stores/user_store.jsx';
-import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -14,6 +13,8 @@ import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
+import {revokeSession} from 'actions/admin_actions.jsx';
+
export default class ActivityLogModal extends React.Component {
constructor(props) {
super(props);
@@ -46,10 +47,8 @@ export default class ActivityLogModal extends React.Component {
setTimeout(() => {
modalContent.removeClass('animation--highlight');
}, 1500);
- Client.revokeSession(altId,
- () => {
- AsyncClient.getSessions();
- },
+ revokeSession(altId,
+ null,
(err) => {
const state = this.getStateFromStores();
state.serverError = err;
@@ -134,6 +133,17 @@ export default class ActivityLogModal extends React.Component {
} else {
devicePicture = 'fa fa-linux';
}
+ } else if (currentSession.props.os.indexOf('Linux') !== -1) {
+ devicePicture = 'fa fa-linux';
+ }
+
+ if (currentSession.props.browser.indexOf('Desktop App') !== -1) {
+ devicePlatform = (
+ <FormattedMessage
+ id='activity_log_modal.desktop'
+ defaultMessage='Native Desktop App'
+ />
+ );
}
let moreInfo;
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
index 9975a3975..b9883d7d8 100644
--- a/webapp/components/admin_console/admin_settings.jsx
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -4,11 +4,12 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import FormError from 'components/form_error.jsx';
import SaveButton from 'components/admin_console/save_button.jsx';
+import {saveConfig} from 'actions/admin_actions.jsx';
+
export default class AdminSettings extends React.Component {
static get propTypes() {
return {
@@ -53,7 +54,7 @@ export default class AdminSettings extends React.Component {
let config = JSON.parse(JSON.stringify(this.props.config));
config = this.getConfigFromState(config);
- Client.saveConfig(
+ saveConfig(
config,
() => {
AsyncClient.getConfig((savedConfig) => {
diff --git a/webapp/components/admin_console/admin_sidebar_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx
index 86c2c6b0f..5725551bf 100644
--- a/webapp/components/admin_console/admin_sidebar_header.jsx
+++ b/webapp/components/admin_console/admin_sidebar_header.jsx
@@ -42,7 +42,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
diff --git a/webapp/components/admin_console/admin_team_members_dropdown.jsx b/webapp/components/admin_console/admin_team_members_dropdown.jsx
index ee9e53f6c..01e94db16 100644
--- a/webapp/components/admin_console/admin_team_members_dropdown.jsx
+++ b/webapp/components/admin_console/admin_team_members_dropdown.jsx
@@ -6,12 +6,10 @@ import ConfirmModal from '../confirm_modal.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import {updateUserRoles, updateActive} from 'actions/user_actions.jsx';
-import {updateTeamMemberRoles} from 'actions/team_actions.jsx';
+import {updateTeamMemberRoles, removeUserFromTeam, adminResetMfa} from 'actions/team_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -75,14 +73,10 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
handleRemoveFromTeam() {
- Client.removeUserFromTeam(
+ removeUserFromTeam(
this.props.teamMember.team_id,
this.props.user.id,
- () => {
- AsyncClient.getTeamStats(this.props.teamMember.team_id);
- UserStore.removeProfileFromTeam(this.props.teamMember.team_id, this.props.user.id);
- UserStore.emitInTeamChange();
- },
+ null,
(err) => {
this.setState({serverError: err.message});
}
@@ -150,10 +144,8 @@ export default class AdminTeamMembersDropdown extends React.Component {
handleResetMfa(e) {
e.preventDefault();
- Client.adminResetMfa(this.props.user.id,
- () => {
- AsyncClient.getUser(this.props.user.id);
- },
+ adminResetMfa(this.props.user.id,
+ null,
(err) => {
this.setState({serverError: err.message});
}
diff --git a/webapp/components/admin_console/brand_image_setting.jsx b/webapp/components/admin_console/brand_image_setting.jsx
index 653073200..b58c0159c 100644
--- a/webapp/components/admin_console/brand_image_setting.jsx
+++ b/webapp/components/admin_console/brand_image_setting.jsx
@@ -7,6 +7,7 @@ import ReactDOM from 'react-dom';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import {uploadBrandImage} from 'actions/admin_actions.jsx';
import FormError from 'components/form_error.jsx';
import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
@@ -81,7 +82,7 @@ export default class BrandImageSetting extends React.Component {
error: ''
});
- Client.uploadBrandImage(
+ uploadBrandImage(
this.state.brandImage,
() => {
$(ReactDOM.findDOMNode(this.refs.upload)).button('complete');
diff --git a/webapp/components/admin_console/cluster_table_container.jsx b/webapp/components/admin_console/cluster_table_container.jsx
index aad5753b7..8dba80cce 100644
--- a/webapp/components/admin_console/cluster_table_container.jsx
+++ b/webapp/components/admin_console/cluster_table_container.jsx
@@ -4,8 +4,8 @@
import React from 'react';
import ClusterTable from './cluster_table.jsx';
import LoadingScreen from '../loading_screen.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
+
+import {getClusterStatus} from 'actions/admin_actions.jsx';
export default class ClusterTableContainer extends React.Component {
constructor(props) {
@@ -19,15 +19,13 @@ export default class ClusterTableContainer extends React.Component {
}
load() {
- Client.getClusterStatus(
+ getClusterStatus(
(data) => {
this.setState({
clusterInfos: data
});
},
- (err) => {
- AsyncClient.dispatchError(err, 'getClusterStatus');
- }
+ null
);
}
diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx
index aac09c0de..7274e6774 100644
--- a/webapp/components/admin_console/compliance_reports.jsx
+++ b/webapp/components/admin_console/compliance_reports.jsx
@@ -9,6 +9,7 @@ import UserStore from '../../stores/user_store.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
+import {saveComplianceReports} from 'actions/admin_actions.jsx';
import {FormattedMessage, FormattedDate, FormattedTime} from 'react-intl';
@@ -72,7 +73,7 @@ export default class ComplianceReports extends React.Component {
job.start_at = Date.parse(ReactDOM.findDOMNode(this.refs.from).value);
job.end_at = Date.parse(ReactDOM.findDOMNode(this.refs.to).value);
- Client.saveComplianceReports(
+ saveComplianceReports(
job,
() => {
ReactDOM.findDOMNode(this.refs.emails).value = '';
diff --git a/webapp/components/admin_console/email_connection_test.jsx b/webapp/components/admin_console/email_connection_test.jsx
index 8e11a0bb4..b99633eec 100644
--- a/webapp/components/admin_console/email_connection_test.jsx
+++ b/webapp/components/admin_console/email_connection_test.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
+import {testEmail} from 'actions/admin_actions.jsx';
+
export default class EmailConnectionTestButton extends React.Component {
static get propTypes() {
return {
@@ -41,7 +42,7 @@ export default class EmailConnectionTestButton extends React.Component {
const config = JSON.parse(JSON.stringify(this.props.config));
this.props.getConfigFromState(config);
- Client.testEmail(
+ testEmail(
config,
() => {
this.setState({
diff --git a/webapp/components/admin_console/ldap_test_button.jsx b/webapp/components/admin_console/ldap_test_button.jsx
index e077aec5f..a564fa42a 100644
--- a/webapp/components/admin_console/ldap_test_button.jsx
+++ b/webapp/components/admin_console/ldap_test_button.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {ldapTest} from 'actions/admin_actions.jsx';
+
export default class LdapTestButton extends React.Component {
static get propTypes() {
return {
@@ -38,7 +39,7 @@ export default class LdapTestButton extends React.Component {
});
const doRequest = () => { //eslint-disable-line func-style
- Client.ldapTest(
+ ldapTest(
() => {
this.setState({
buisy: false,
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index d98309f80..6c14394b7 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -4,7 +4,8 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
+
+import {uploadLicenseFile, removeLicenseFile} from 'actions/admin_actions.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -54,7 +55,8 @@ class LicenseSettings extends React.Component {
$('#upload-button').button('loading');
- Client.uploadLicenseFile(file,
+ uploadLicenseFile(
+ file,
() => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
@@ -74,7 +76,7 @@ class LicenseSettings extends React.Component {
$('#remove-button').button('loading');
- Client.removeLicenseFile(
+ removeLicenseFile(
() => {
$('#remove-button').button('reset');
this.setState({fileSelected: false, fileName: null, serverError: null});
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index 8dc0c1e2e..5846c91db 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -24,12 +24,14 @@ export default class Logs extends React.Component {
componentDidMount() {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
+ this.refs.logPanel.focus();
}
componentDidUpdate() {
// Scroll Down to get the latest logs
var node = this.refs.logPanel;
node.scrollTop = node.scrollHeight;
+ node.focus();
}
componentWillUnmount() {
@@ -100,6 +102,7 @@ export default class Logs extends React.Component {
/>
</button>
<div
+ tabIndex='-1'
ref='logPanel'
className='log__panel'
>
diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx
index 0e224af73..391726a93 100644
--- a/webapp/components/admin_console/policy_settings.jsx
+++ b/webapp/components/admin_console/policy_settings.jsx
@@ -6,6 +6,8 @@ import React from 'react';
import AdminSettings from './admin_settings.jsx';
import SettingsGroup from './settings_group.jsx';
import DropdownSetting from './dropdown_setting.jsx';
+import RadioSetting from './radio_setting.jsx';
+import PostEditSetting from './post_edit_setting.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -22,6 +24,9 @@ export default class PolicySettings extends AdminSettings {
}
getConfigFromState(config) {
+ config.ServiceSettings.RestrictPostDelete = this.state.restrictPostDelete;
+ config.ServiceSettings.AllowEditPost = this.state.allowEditPost;
+ config.ServiceSettings.PostEditTimeLimit = this.parseIntNonZero(this.state.postEditTimeLimit, Constants.DEFAULT_POST_EDIT_TIME_LIMIT);
config.TeamSettings.RestrictTeamInvite = this.state.restrictTeamInvite;
config.TeamSettings.RestrictPublicChannelCreation = this.state.restrictPublicChannelCreation;
config.TeamSettings.RestrictPrivateChannelCreation = this.state.restrictPrivateChannelCreation;
@@ -35,6 +40,9 @@ export default class PolicySettings extends AdminSettings {
getStateFromConfig(config) {
return {
+ restrictPostDelete: config.ServiceSettings.RestrictPostDelete,
+ allowEditPost: config.ServiceSettings.AllowEditPost,
+ postEditTimeLimit: config.ServiceSettings.PostEditTimeLimit,
restrictTeamInvite: config.TeamSettings.RestrictTeamInvite,
restrictPublicChannelCreation: config.TeamSettings.RestrictPublicChannelCreation,
restrictPrivateChannelCreation: config.TeamSettings.RestrictPrivateChannelCreation,
@@ -241,6 +249,47 @@ export default class PolicySettings extends AdminSettings {
/>
}
/>
+ <RadioSetting
+ id='restrictPostDelete'
+ values={[
+ {value: Constants.PERMISSIONS_DELETE_POST_ALL, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostAll', 'Message authors can delete their own messages, and Administrators can delete any message')},
+ {value: Constants.PERMISSIONS_DELETE_POST_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostAdmin', 'Team Admins and System Admins')},
+ {value: Constants.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostSystemAdmin', 'System Admins')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.general.policy.restrictPostDeleteTitle'
+ defaultMessage='Allow which users to delete messages:'
+ />
+ }
+ value={this.state.restrictPostDelete}
+ onChange={this.handleChange}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.general.policy.restrictPostDeleteDescription'
+ defaultMessage='Set policy on who has permission to delete messages.'
+ />
+ }
+ />
+ <PostEditSetting
+ id='allowEditPost'
+ timeLimitId='postEditTimeLimit'
+ label={
+ <FormattedMessage
+ id='admin.general.policy.allowEditPostTitle'
+ defaultMessage='Allow users to edit their messages:'
+ />
+ }
+ value={this.state.allowEditPost}
+ timeLimitValue={this.state.postEditTimeLimit}
+ onChange={this.handleChange}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.general.policy.allowEditPostDescription'
+ defaultMessage='Set policy on the length of time authors have to edit their messages after posting.'
+ />
+ }
+ />
</SettingsGroup>
);
}
diff --git a/webapp/components/admin_console/post_edit_setting.jsx b/webapp/components/admin_console/post_edit_setting.jsx
new file mode 100644
index 000000000..282a1b6c5
--- /dev/null
+++ b/webapp/components/admin_console/post_edit_setting.jsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class PostEditSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleTimeLimitChange = this.handleTimeLimitChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ handleTimeLimitChange(e) {
+ this.props.onChange(this.props.timeLimitId, e.target.value);
+ }
+
+ render() {
+ return (
+ <Setting
+ label={this.props.label}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
+ >
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_ALWAYS}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_ALWAYS}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {Utils.localizeMessage('admin.general.policy.allowEditPostAlways', 'Any time')}
+ </label>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_NEVER}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_NEVER}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {Utils.localizeMessage('admin.general.policy.allowEditPostNever', 'Never')}
+ </label>
+ </div>
+ <div className='radio form-inline'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ <input
+ type='text'
+ value={this.props.timeLimitValue}
+ className='form-control'
+ name={this.props.timeLimitId}
+ onChange={this.handleTimeLimitChange}
+ disabled={this.props.disabled || this.props.value !== Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ />
+ <span> {Utils.localizeMessage('admin.general.policy.allowEditPostTimeLimit', 'seconds after posting')}</span>
+ </label>
+ </div>
+ </Setting>
+ );
+ }
+}
+
+PostEditSetting.defaultProps = {
+ isDisabled: false
+};
+
+PostEditSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
+ timeLimitId: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ value: React.PropTypes.string.isRequired,
+ timeLimitValue: React.PropTypes.number.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
+};
diff --git a/webapp/components/admin_console/purge_caches.jsx b/webapp/components/admin_console/purge_caches.jsx
index a999f090e..9f52433d5 100644
--- a/webapp/components/admin_console/purge_caches.jsx
+++ b/webapp/components/admin_console/purge_caches.jsx
@@ -3,10 +3,10 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
-
import {FormattedMessage} from 'react-intl';
+import {invalidateAllCaches} from 'actions/admin_actions.jsx';
+
export default class PurgeCachesButton extends React.Component {
constructor(props) {
super(props);
@@ -27,7 +27,7 @@ export default class PurgeCachesButton extends React.Component {
fail: null
});
- Client.invalidateAllCaches(
+ invalidateAllCaches(
() => {
this.setState({
loading: false
@@ -43,10 +43,6 @@ export default class PurgeCachesButton extends React.Component {
}
render() {
- if (global.window.mm_license.IsLicensed !== 'true') {
- return <div/>;
- }
-
let testMessage = null;
if (this.state.fail) {
testMessage = (
diff --git a/webapp/components/admin_console/radio_setting.jsx b/webapp/components/admin_console/radio_setting.jsx
new file mode 100644
index 000000000..dd45a5a26
--- /dev/null
+++ b/webapp/components/admin_console/radio_setting.jsx
@@ -0,0 +1,63 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+export default class RadioSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ render() {
+ const options = [];
+ for (const {value, text} of this.props.values) {
+ options.push(
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={value}
+ name={this.props.id}
+ checked={value === this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {text}
+ </label>
+ </div>
+ );
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
+ >
+ {options}
+ </Setting>
+ );
+ }
+}
+
+RadioSetting.defaultProps = {
+ isDisabled: false
+};
+
+RadioSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
+ values: React.PropTypes.array.isRequired,
+ label: React.PropTypes.node.isRequired,
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
+};
diff --git a/webapp/components/admin_console/recycle_db.jsx b/webapp/components/admin_console/recycle_db.jsx
index 53e8e7436..5683f97e2 100644
--- a/webapp/components/admin_console/recycle_db.jsx
+++ b/webapp/components/admin_console/recycle_db.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {recycleDatabaseConnection} from 'actions/admin_actions.jsx';
+
export default class RecycleDbButton extends React.Component {
constructor(props) {
super(props);
@@ -28,7 +29,7 @@ export default class RecycleDbButton extends React.Component {
fail: null
});
- Client.recycleDatabaseConnection(
+ recycleDatabaseConnection(
() => {
this.setState({
loading: false
diff --git a/webapp/components/admin_console/reload_config.jsx b/webapp/components/admin_console/reload_config.jsx
index 0b50d5803..25e9463d3 100644
--- a/webapp/components/admin_console/reload_config.jsx
+++ b/webapp/components/admin_console/reload_config.jsx
@@ -3,13 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
-import {getConfig} from 'utils/async_client.jsx';
-
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {reloadConfig} from 'actions/admin_actions.jsx';
+
export default class ReloadConfigButton extends React.Component {
constructor(props) {
super(props);
@@ -30,9 +29,8 @@ export default class ReloadConfigButton extends React.Component {
fail: null
});
- Client.reloadConfig(
+ reloadConfig(
() => {
- getConfig();
this.setState({
loading: false
});
diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
index e3fd2bf00..757f85517 100644
--- a/webapp/components/admin_console/reset_password_modal.jsx
+++ b/webapp/components/admin_console/reset_password_modal.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {Modal} from 'react-bootstrap';
import {injectIntl, intlShape, FormattedMessage} from 'react-intl';
+import {adminResetPassword} from 'actions/admin_actions.jsx';
+
import React from 'react';
class ResetPasswordModal extends React.Component {
@@ -32,7 +33,7 @@ class ResetPasswordModal extends React.Component {
}
this.setState({serverError: null});
- Client.adminResetPassword(
+ adminResetPassword(
this.props.user.id,
password,
() => {
diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx
index ad7a82553..7b9ed38b8 100644
--- a/webapp/components/admin_console/saml_settings.jsx
+++ b/webapp/components/admin_console/saml_settings.jsx
@@ -12,9 +12,10 @@ import RemoveFileSetting from './remove_file_setting.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import SettingsGroup from './settings_group.jsx';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import {samlCertificateStatus, uploadCertificateFile, removeCertificateFile} from 'actions/admin_actions.jsx';
+
export default class SamlSettings extends AdminSettings {
constructor(props) {
super(props);
@@ -73,7 +74,7 @@ export default class SamlSettings extends AdminSettings {
}
componentWillMount() {
- Client.samlCertificateStatus(
+ samlCertificateStatus(
(data) => {
const files = {};
if (!data.IdpCertificateFile) {
@@ -93,7 +94,7 @@ export default class SamlSettings extends AdminSettings {
}
uploadCertificate(id, file, callback) {
- Client.uploadCertificateFile(
+ uploadCertificateFile(
file,
() => {
const fileName = file.name;
@@ -112,7 +113,7 @@ export default class SamlSettings extends AdminSettings {
}
removeCertificate(id, callback) {
- Client.removeCertificateFile(
+ removeCertificateFile(
this.state[id],
() => {
this.handleChange(id, '');
diff --git a/webapp/components/admin_console/sync_now_button.jsx b/webapp/components/admin_console/sync_now_button.jsx
index 95d126291..f1197b216 100644
--- a/webapp/components/admin_console/sync_now_button.jsx
+++ b/webapp/components/admin_console/sync_now_button.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {ldapSyncNow} from 'actions/admin_actions.jsx';
+
export default class SyncNowButton extends React.Component {
static get propTypes() {
return {
@@ -33,7 +34,7 @@ export default class SyncNowButton extends React.Component {
fail: null
});
- Client.ldapSyncNow(
+ ldapSyncNow(
() => {
this.setState({
buisy: false
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 4517e241b..547002a5b 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -158,13 +158,17 @@ export default class UserList extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
this.props.params.team,
options,
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete);
}
@@ -172,6 +176,8 @@ export default class UserList extends React.Component {
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
index dd7b90260..89cc98f0b 100644
--- a/webapp/components/analytics/system_analytics.jsx
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -358,6 +358,32 @@ class SystemAnalytics extends React.Component {
/>
);
+ const dailyActiveUsers = (
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.dailyActiveUsers'
+ defaultMessage='Daily Active Users'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.DAILY_ACTIVE_USERS]}
+ />
+ );
+
+ const monthlyActiveUsers = (
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.monthlyActiveUsers'
+ defaultMessage='Monthly Active Users'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.MONTHLY_ACTIVE_USERS]}
+ />
+ );
+
let firstRow;
let secondRow;
if (isLicensed && skippedIntensiveQueries) {
@@ -406,6 +432,13 @@ class SystemAnalytics extends React.Component {
);
}
+ const thirdRow = (
+ <div className='row'>
+ {dailyActiveUsers}
+ {monthlyActiveUsers}
+ </div>
+ );
+
return (
<div className='wrapper--fixed team_statistics'>
<h3>
@@ -417,6 +450,7 @@ class SystemAnalytics extends React.Component {
{banner}
{firstRow}
{secondRow}
+ {thirdRow}
{advancedStats}
{advancedGraphs}
{postTotalGraph}
diff --git a/webapp/components/authorize.jsx b/webapp/components/authorize.jsx
index 684bae589..f3f5770de 100644
--- a/webapp/components/authorize.jsx
+++ b/webapp/components/authorize.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'client/web_client.jsx';
import FormError from 'components/form_error.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -9,6 +8,8 @@ import React from 'react';
import icon50 from 'images/icon50x50.png';
+import {getOAuthAppInfo, allowOAuth2} from 'actions/admin_actions.jsx';
+
export default class Authorize extends React.Component {
static get propTypes() {
return {
@@ -27,7 +28,7 @@ export default class Authorize extends React.Component {
}
componentWillMount() {
- Client.getOAuthAppInfo(
+ getOAuthAppInfo(
this.props.location.query.client_id,
(app) => {
this.setState({app});
@@ -46,7 +47,7 @@ export default class Authorize extends React.Component {
handleAllow() {
const params = this.props.location.query;
- Client.allowOAuth2(params.response_type, params.client_id, params.redirect_uri, params.state, params.scope,
+ allowOAuth2(params,
(data) => {
if (data.redirect) {
window.location.href = data.redirect;
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index fc0ec132e..e83d493f4 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -29,15 +29,12 @@ import * as ChannelActions from 'actions/channel_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import {getFlaggedPosts} from 'actions/post_actions.jsx';
import {Constants, Preferences, UserStatuses} from 'utils/constants.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap';
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
@@ -131,21 +128,7 @@ export default class ChannelHeader extends React.Component {
}
handleLeave() {
- Client.leaveChannel(this.state.channel.id,
- () => {
- const channelId = this.state.channel.id;
-
- if (this.state.isFavorite) {
- ChannelActions.unmarkFavorite(channelId);
- }
-
- const townsquare = ChannelStore.getByName('town-square');
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleLeave');
- }
- );
+ ChannelActions.leaveChannel(this.state.channel.id);
}
toggleFavorite = (e) => {
@@ -438,7 +421,7 @@ export default class ChannelHeader extends React.Component {
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
- id='chanel_header.addMembers'
+ id='channel_header.addMembers'
defaultMessage='Add Members'
/>
</ToggleModalButton>
diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx
index 355d23d53..5deec0794 100644
--- a/webapp/components/channel_invite_modal.jsx
+++ b/webapp/components/channel_invite_modal.jsx
@@ -117,19 +117,25 @@ export default class ChannelInviteModal extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
TeamStore.getCurrentId(),
{not_in_channel_id: this.props.channel.id},
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
this.setState({search: true, users});
}
);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/channel_members_dropdown.jsx b/webapp/components/channel_members_dropdown.jsx
new file mode 100644
index 000000000..a7b3259af
--- /dev/null
+++ b/webapp/components/channel_members_dropdown.jsx
@@ -0,0 +1,246 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelStore from 'stores/channel_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {removeUserFromChannel, makeUserChannelAdmin, makeUserChannelMember} from 'actions/channel_actions.jsx';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+export default class ChannelMembersDropdown extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleRemoveFromChannel = this.handleRemoveFromChannel.bind(this);
+ this.handleMakeChannelMember = this.handleMakeChannelMember.bind(this);
+ this.handleMakeChannelAdmin = this.handleMakeChannelAdmin.bind(this);
+
+ this.state = {
+ serverError: null,
+ user: null,
+ role: null
+ };
+ }
+
+ handleRemoveFromChannel() {
+ removeUserFromChannel(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeChannelMember() {
+ makeUserChannelMember(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeChannelAdmin() {
+ makeUserChannelAdmin(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ // Checks if the user this menu is for is a channel admin or not.
+ isChannelAdmin() {
+ if (Utils.isChannelAdmin(this.props.channelMember.roles)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Checks if the current user has the power to change the roles of this member.
+ canChangeMemberRoles() {
+ if (UserStore.isSystemAdminForCurrentUser()) {
+ return true;
+ } else if (TeamStore.isTeamAdminForCurrentTeam()) {
+ return true;
+ } else if (ChannelStore.isChannelAdminForCurrentChannel()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Checks if the current user has the power to remove this member from the channel.
+ canRemoveMember() {
+ // TODO: This will be implemented as part of PLT-5047.
+ return true;
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ if (this.props.user.id === UserStore.getCurrentId()) {
+ return null;
+ }
+
+ if (this.canChangeMemberRoles()) {
+ let role = (
+ <FormattedMessage
+ id='channel_members_dropdown.channel_member'
+ defaultMessage='Channel Member'
+ />
+ );
+
+ if (this.isChannelAdmin()) {
+ role = (
+ <FormattedMessage
+ id='channel_members_dropdown.channel_admin'
+ defaultMessage='Channel Admin'
+ />
+ );
+ }
+
+ let removeFromChannel = null;
+ if (this.canRemoveMember()) {
+ removeFromChannel = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleRemoveFromChannel}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.remove_from_channel'
+ defaultMessage='Remove From Channel'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeChannelMember = null;
+ if (this.isChannelAdmin()) {
+ makeChannelMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeChannelMember}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.make_channel_member'
+ defaultMessage='Make Channel Member'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeChannelAdmin = null;
+ if (!this.isChannelAdmin()) {
+ makeChannelAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeChannelAdmin}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.make_channel_admin'
+ defaultMessage='Make Channel Admin'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{role} </span>
+ <span className='fa fa-chevron-down'/>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ >
+ {makeChannelMember}
+ {makeChannelAdmin}
+ {removeFromChannel}
+ </ul>
+ {serverError}
+ </div>
+ );
+ } else if (this.canRemoveMember()) {
+ return (
+ <button
+ type='button'
+ className='btn btn-danger btn-message'
+ onClick={this.handleRemoveFromChannel}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.remove_member'
+ defaultMessage='Remove Member'
+ />
+ </button>
+ );
+ } else if (this.isChannelAdmin()) {
+ return (
+ <div>
+ <FormattedMessage
+ id='channel_members_dropdown.channel_admin'
+ defaultMessage='Channel Admin'
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <FormattedMessage
+ id='channel_members_dropdown.channel_member'
+ defaultMessage='Channel Member'
+ />
+ </div>
+ );
+ }
+}
+
+ChannelMembersDropdown.propTypes = {
+ channel: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object.isRequired,
+ teamMember: React.PropTypes.object.isRequired,
+ channelMember: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx
index 351efed96..96d90e5cc 100644
--- a/webapp/components/channel_members_modal.jsx
+++ b/webapp/components/channel_members_modal.jsx
@@ -1,170 +1,29 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SearchableUserList from './searchable_user_list.jsx';
-import LoadingScreen from './loading_screen.jsx';
-
-import UserStore from 'stores/user_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
-
-import {searchUsers} from 'actions/user_actions.jsx';
-import {removeUserFromChannel} from 'actions/channel_actions.jsx';
-
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as UserAgent from 'utils/user_agent.jsx';
-import Constants from 'utils/constants.jsx';
+import MemberListChannel from './member_list_channel.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
-const USERS_PER_PAGE = 50;
-
export default class ChannelMembersModal extends React.Component {
constructor(props) {
super(props);
- this.onChange = this.onChange.bind(this);
- this.onStatusChange = this.onStatusChange.bind(this);
this.onHide = this.onHide.bind(this);
- this.handleRemove = this.handleRemove.bind(this);
- this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
- this.search = this.search.bind(this);
- this.nextPage = this.nextPage.bind(this);
-
- this.term = '';
- this.searchTimeoutId = 0;
-
- const stats = ChannelStore.getStats(props.channel.id);
this.state = {
- users: [],
- total: stats.member_count,
- show: true,
- search: false,
- statusChange: false
+ channel: this.props.channel,
+ show: true
};
}
- componentDidMount() {
- ChannelStore.addStatsChangeListener(this.onChange);
- UserStore.addInChannelChangeListener(this.onChange);
- UserStore.addStatusesChangeListener(this.onStatusChange);
-
- AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
- }
-
- componentWillUnmount() {
- ChannelStore.removeStatsChangeListener(this.onChange);
- UserStore.removeInChannelChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onStatusChange);
- }
-
- onChange(force) {
- if (this.state.search && !force) {
- this.search(this.term);
- return;
- }
-
- const stats = ChannelStore.getStats(this.props.channel.id);
- this.setState({
- users: UserStore.getProfileListInChannel(this.props.channel.id),
- total: stats.member_count
- });
- }
-
- onStatusChange() {
- // Initiate a render to pick up on new statuses
- this.setState({
- statusChange: !this.state.statusChange
- });
- }
-
onHide() {
this.setState({show: false});
}
- handleRemove(user) {
- const userId = user.id;
-
- removeUserFromChannel(
- this.props.channel.id,
- userId,
- null,
- (err) => {
- this.setState({inviteError: err.message});
- }
- );
- }
-
- createRemoveMemberButton({user}) {
- if (user.id === UserStore.getCurrentId()) {
- return null;
- }
-
- return (
- <button
- type='button'
- className='btn btn-primary btn-message'
- onClick={this.handleRemove.bind(this, user)}
- >
- <FormattedMessage
- id='channel_members_modal.remove'
- defaultMessage='Remove'
- />
- </button>
- );
- }
-
- nextPage(page) {
- AsyncClient.getProfilesInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
- }
-
- search(term) {
- this.term = term;
-
- if (term === '') {
- this.onChange(true);
- this.setState({search: false});
- return;
- }
-
- clearTimeout(this.searchTimeoutId);
-
- this.searchTimeoutId = setTimeout(
- () => {
- searchUsers(
- term,
- TeamStore.getCurrentId(),
- {in_channel_id: this.props.channel.id},
- (users) => {
- this.setState({search: true, users});
- }
- );
- },
- Constants.SEARCH_TIMEOUT_MILLISECONDS
- );
- }
-
render() {
- let content;
- if (this.state.loading) {
- content = (<LoadingScreen/>);
- } else {
- content = (
- <SearchableUserList
- users={this.state.users}
- usersPerPage={USERS_PER_PAGE}
- total={this.state.total}
- nextPage={this.nextPage}
- search={this.search}
- actions={[this.createRemoveMemberButton]}
- focusOnMount={!UserAgent.isMobile()}
- />
- );
- }
-
return (
<div>
<Modal
@@ -177,7 +36,7 @@ export default class ChannelMembersModal extends React.Component {
<Modal.Title>
<span className='name'>{this.props.channel.display_name}</span>
<FormattedMessage
- id='channel_memebers_modal.members'
+ id='channel_members_modal.members'
defaultMessage=' Members'
/>
</Modal.Title>
@@ -198,7 +57,9 @@ export default class ChannelMembersModal extends React.Component {
<Modal.Body
ref='modalBody'
>
- {content}
+ <MemberListChannel
+ channel={this.props.channel}
+ />
</Modal.Body>
</Modal>
</div>
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx
index 59bf2f15a..194de3874 100644
--- a/webapp/components/channel_select.jsx
+++ b/webapp/components/channel_select.jsx
@@ -31,12 +31,13 @@ export default class ChannelSelect extends React.Component {
super(props);
this.handleChannelChange = this.handleChannelChange.bind(this);
+ this.filterChannels = this.filterChannels.bind(this);
this.compareByDisplayName = this.compareByDisplayName.bind(this);
AsyncClient.getMoreChannels(true);
this.state = {
- channels: ChannelStore.getAll().sort(this.compareByDisplayName)
+ channels: ChannelStore.getAll().filter(this.filterChannels).sort(this.compareByDisplayName)
};
}
@@ -50,10 +51,19 @@ export default class ChannelSelect extends React.Component {
handleChannelChange() {
this.setState({
- channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).sort(this.compareByDisplayName)
+ channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).
+ filter(this.filterChannels).sort(this.compareByDisplayName)
});
}
+ filterChannels(channel) {
+ if (channel.display_name) {
+ return true;
+ }
+
+ return false;
+ }
+
compareByDisplayName(channelA, channelB) {
return channelA.display_name.localeCompare(channelB.display_name);
}
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
index 2f8595c78..fc66e06b1 100644
--- a/webapp/components/channel_switch_modal.jsx
+++ b/webapp/components/channel_switch_modal.jsx
@@ -64,6 +64,7 @@ export default class SwitchChannelModal extends React.Component {
}
onExited() {
+ this.selected = null;
setTimeout(() => {
$('#post_textbox').get(0).focus();
});
@@ -71,6 +72,7 @@ export default class SwitchChannelModal extends React.Component {
onChange(e) {
this.setState({text: e.target.value});
+ this.selected = null;
}
onItemSelected(item) {
@@ -89,6 +91,15 @@ export default class SwitchChannelModal extends React.Component {
handleSubmit() {
let channel = null;
+ if (!this.selected) {
+ if (this.state.text !== '') {
+ this.setState({
+ error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
+ });
+ }
+ return;
+ }
+
if (this.selected.type === Constants.DM_CHANNEL) {
const user = UserStore.getProfileByUsername(this.selected.name);
@@ -117,7 +128,7 @@ export default class SwitchChannelModal extends React.Component {
this.onHide();
} else if (this.state.text !== '') {
this.setState({
- error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
+ error: Utils.localizeMessage('channel_switch_modal.failed_to_open', 'Failed to open channel.')
});
}
}
diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx
index 890512803..7d062a957 100644
--- a/webapp/components/claim/components/email_to_ldap.jsx
+++ b/webapp/components/claim/components/email_to_ldap.jsx
@@ -4,9 +4,9 @@
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
+import {emailToLdap} from 'actions/admin_actions.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
@@ -79,7 +79,7 @@ export default class EmailToLDAP extends React.Component {
}
submit(loginId, password, token, ldapId, ldapPassword) {
- Client.emailToLdap(
+ emailToLdap(
loginId,
password,
token,
diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx
index 3cede15a3..bc5a7bdaa 100644
--- a/webapp/components/claim/components/email_to_oauth.jsx
+++ b/webapp/components/claim/components/email_to_oauth.jsx
@@ -4,10 +4,10 @@
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
+import {emailToOAuth} from 'actions/admin_actions.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -55,7 +55,7 @@ export default class EmailToOAuth extends React.Component {
}
submit(loginId, password, token) {
- Client.emailToOAuth(
+ emailToOAuth(
loginId,
password,
token,
diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx
index ed604583d..ffba1c331 100644
--- a/webapp/components/claim/components/oauth_to_email.jsx
+++ b/webapp/components/claim/components/oauth_to_email.jsx
@@ -2,13 +2,13 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
+
+import {oauthToEmail} from 'actions/admin_actions.jsx';
export default class OAuthToEmail extends React.Component {
constructor(props) {
@@ -48,14 +48,10 @@ export default class OAuthToEmail extends React.Component {
state.error = null;
this.setState(state);
- Client.oauthToEmail(
+ oauthToEmail(
this.props.email,
password,
- (data) => {
- if (data.follow_link) {
- browserHistory.push(data.follow_link);
- }
- },
+ null,
(err) => {
this.setState({error: err.message});
}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 0e9d2a41a..d9d66c8fa 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -331,6 +331,9 @@ export default class CreateComment extends React.Component {
draft.fileInfos = draft.fileInfos.concat(fileInfos);
PostStore.storeCommentDraft(this.props.rootId, draft);
+ // Focus on preview if needed
+ this.refs.preview.refs.container.scrollIntoViewIfNeeded();
+
this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos});
}
@@ -355,6 +358,9 @@ export default class CreateComment extends React.Component {
const fileInfos = this.state.fileInfos;
const uploadsInProgress = this.state.uploadsInProgress;
+ // Clear previous errors
+ this.handleUploadError(null);
+
// id can either be the id of an uploaded file or the client id of an in progress upload
let index = fileInfos.findIndex((info) => info.id === id);
if (index === -1) {
@@ -432,6 +438,7 @@ export default class CreateComment extends React.Component {
fileInfos={this.state.fileInfos}
onRemove={this.removePreview}
uploadsInProgress={this.state.uploadsInProgress}
+ ref='preview'
/>
);
}
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index e1b2ca059..9269633ff 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -297,6 +297,9 @@ export default class CreatePost extends React.Component {
const fileInfos = Object.assign([], this.state.fileInfos);
const uploadsInProgress = this.state.uploadsInProgress;
+ // Clear previous errors
+ this.handleUploadError(null);
+
// id can either be the id of an uploaded file or the client id of an in progress upload
let index = fileInfos.findIndex((info) => info.id === id);
if (index === -1) {
diff --git a/webapp/components/delete_channel_modal.jsx b/webapp/components/delete_channel_modal.jsx
index 1b642861a..a6577a4a9 100644
--- a/webapp/components/delete_channel_modal.jsx
+++ b/webapp/components/delete_channel_modal.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import {Modal} from 'react-bootstrap';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
@@ -13,7 +11,7 @@ import {browserHistory} from 'react-router/es6';
import React from 'react';
-import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
+import {deleteChannel} from 'actions/channel_actions.jsx';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -31,15 +29,7 @@ export default class DeleteChannelModal extends React.Component {
}
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/town-square');
- Client.deleteChannel(
- this.props.channel.id,
- () => {
- loadChannelsForCurrentUser();
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleDelete');
- }
- );
+ deleteChannel(this.props.channel.id);
}
onHide() {
diff --git a/webapp/components/delete_post_modal.jsx b/webapp/components/delete_post_modal.jsx
index 84eef4671..39d4f41f9 100644
--- a/webapp/components/delete_post_modal.jsx
+++ b/webapp/components/delete_post_modal.jsx
@@ -3,13 +3,9 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ModalStore from 'stores/modal_store.jsx';
import {Modal} from 'react-bootstrap';
-import * as AsyncClient from 'utils/async_client.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import {removePostFromStore} from 'actions/post_actions.jsx';
+import ModalStore from 'stores/modal_store.jsx';
+import {deletePost} from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
@@ -51,24 +47,16 @@ export default class DeletePostModal extends React.Component {
}
handleDelete() {
- Client.deletePost(
+ deletePost(
this.state.post.channel_id,
- this.state.post.id,
+ this.state.post,
() => {
- removePostFromStore(this.state.post);
- if (this.state.post.id === PostStore.getSelectedPostId()) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST_SELECTED,
- postId: null
- });
- }
+ this.handleHide();
},
(err) => {
- AsyncClient.dispatchError(err, 'deletePost');
+ this.setState({error: err.message});
}
);
-
- this.handleHide();
}
handleToggle(value, args) {
diff --git a/webapp/components/do_verify_email.jsx b/webapp/components/do_verify_email.jsx
index e0ac3218e..9b6a9ccad 100644
--- a/webapp/components/do_verify_email.jsx
+++ b/webapp/components/do_verify_email.jsx
@@ -2,11 +2,12 @@
// See License.txt for license information.
import {FormattedMessage} from 'react-intl';
-import Client from 'client/web_client.jsx';
import LoadingScreen from './loading_screen.jsx';
import {browserHistory, Link} from 'react-router/es6';
+import {verifyEmail} from 'actions/user_actions.jsx';
+
import React from 'react';
export default class DoVerifyEmail extends React.Component {
@@ -19,15 +20,11 @@ export default class DoVerifyEmail extends React.Component {
};
}
componentWillMount() {
- const uid = this.props.location.query.uid;
- const hid = this.props.location.query.hid;
- const email = this.props.location.query.email;
-
- Client.verifyEmail(
- uid,
- hid,
+ verifyEmail(
+ this.props.location.query.uid,
+ this.props.location.query.hid,
() => {
- browserHistory.push('/login?extra=verified&email=' + email);
+ browserHistory.push('/login?extra=verified&email=' + this.props.location.query.email);
},
(err) => {
this.setState({verifyStatus: 'failure', serverError: err.message});
diff --git a/webapp/components/edit_channel_header_modal.jsx b/webapp/components/edit_channel_header_modal.jsx
index 490b9fb31..0d8eb8acb 100644
--- a/webapp/components/edit_channel_header_modal.jsx
+++ b/webapp/components/edit_channel_header_modal.jsx
@@ -2,13 +2,12 @@
// See License.txt for license information.
import ReactDOM from 'react-dom';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {updateChannelHeader} from 'actions/channel_actions.jsx';
import {Modal} from 'react-bootstrap';
@@ -64,17 +63,12 @@ class EditChannelHeaderModal extends React.Component {
handleSubmit() {
this.setState({submitted: true});
- Client.updateChannelHeader(
+ updateChannelHeader(
this.props.channel.id,
this.state.header,
- (channel) => {
+ () => {
this.setState({serverError: ''});
this.onHide();
-
- AppDispatcher.handleServerAction({
- type: Constants.ActionTypes.RECEIVED_CHANNEL,
- channel
- });
},
(err) => {
if (err.id === 'api.context.invalid_param.app_error') {
diff --git a/webapp/components/edit_channel_purpose_modal.jsx b/webapp/components/edit_channel_purpose_modal.jsx
index 7ba2eff2c..4bb876460 100644
--- a/webapp/components/edit_channel_purpose_modal.jsx
+++ b/webapp/components/edit_channel_purpose_modal.jsx
@@ -3,14 +3,13 @@
import PreferenceStore from 'stores/preference_store.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
+import {updateChannelPurpose} from 'actions/channel_actions.jsx';
export default class EditChannelPurposeModal extends React.Component {
constructor(props) {
@@ -64,12 +63,10 @@ export default class EditChannelPurposeModal extends React.Component {
this.setState({submitted: true});
- Client.updateChannelPurpose(
+ updateChannelPurpose(
this.props.channel.id,
this.refs.purpose.value.trim(),
() => {
- AsyncClient.getChannel(this.props.channel.id);
-
this.handleHide();
},
(err) => {
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index 2108ec3d1..b2b607428 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -9,11 +9,9 @@ import MessageHistoryStore from 'stores/message_history_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import {loadPosts} from 'actions/post_actions.jsx';
+import {updatePost} from 'actions/post_actions.jsx';
-import Client from 'client/web_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
@@ -91,15 +89,12 @@ export default class EditPostModal extends React.Component {
return;
}
- Client.updatePost(
+ updatePost(
updatedPost,
() => {
- loadPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
},
- (err) => {
- AsyncClient.dispatchError(err, 'updatePost');
- }
+ Boolean(PostStore.getFocusedPostId()) // If there is focused post we need to update that post's store too.
);
$('#edit_post').modal('hide');
@@ -125,6 +120,17 @@ export default class EditPostModal extends React.Component {
}
handleEditPostEvent(options) {
+ var post = PostStore.getPost(options.channelId, options.postId);
+ if (global.window.mm_license.IsLicensed === 'true') {
+ if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
+ return;
+ }
+ if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) {
+ if ((post.create_at + (global.window.mm_config.PostEditTimeLimit * 1000)) < Utils.getTimestamp()) {
+ return;
+ }
+ }
+ }
this.setState({
editText: options.message || '',
originalText: options.message || '',
@@ -180,7 +186,10 @@ export default class EditPostModal extends React.Component {
onModalHide() {
if (this.state.refocusId !== '') {
setTimeout(() => {
- $(this.state.refocusId).get(0).focus();
+ const element = $(this.state.refocusId).get(0);
+ if (element) {
+ element.focus();
+ }
});
}
}
diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list.jsx
index 3d39d8709..472cd2686 100644
--- a/webapp/components/file_attachment_list.jsx
+++ b/webapp/components/file_attachment_list.jsx
@@ -50,7 +50,7 @@ export default class FileAttachmentList extends React.Component {
return (
<div>
- <div className='post-image__columns'>
+ <div className='post-image__columns clearfix'>
{postFiles}
</div>
<ViewImageModal
diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx
index 53cec7f7b..624bfaf44 100644
--- a/webapp/components/file_preview.jsx
+++ b/webapp/components/file_preview.jsx
@@ -84,7 +84,10 @@ export default class FilePreview extends React.Component {
});
return (
- <div className='file-preview__container'>
+ <div
+ className='file-preview__container'
+ ref='container'
+ >
{previews}
</div>
);
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 9eff25ab5..a821fedab 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -4,7 +4,6 @@
import $ from 'jquery';
import 'jquery-dragster/jquery.dragster.js';
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
@@ -13,6 +12,8 @@ import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages} from 'react-intl';
+import {uploadFile} from 'actions/file_actions.jsx';
+
const holders = defineMessages({
limited: {
id: 'file_upload.limited',
@@ -47,6 +48,7 @@ class FileUpload extends React.Component {
this.cancelUpload = this.cancelUpload.bind(this);
this.pasteUpload = this.pasteUpload.bind(this);
this.keyUpload = this.keyUpload.bind(this);
+ this.handleMaxUploadReached = this.handleMaxUploadReached.bind(this);
this.state = {
requests: {}
@@ -88,13 +90,14 @@ class FileUpload extends React.Component {
// generate a unique id that can be used by other components to refer back to this upload
const clientId = Utils.generateId();
- const request = Client.uploadFile(files[i],
- files[i].name,
- channelId,
- clientId,
- this.fileUploadSuccess.bind(this, channelId),
- this.fileUploadFail.bind(this, clientId, channelId)
- );
+ const request = uploadFile(
+ files[i],
+ files[i].name,
+ channelId,
+ clientId,
+ this.fileUploadSuccess.bind(this, channelId),
+ this.fileUploadFail.bind(this, clientId)
+ );
const requests = this.state.requests;
requests[clientId] = request;
@@ -270,7 +273,8 @@ class FileUpload extends React.Component {
const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext;
- const request = Client.uploadFile(file,
+ const request = uploadFile(
+ file,
name,
channelId,
clientId,
@@ -309,6 +313,16 @@ class FileUpload extends React.Component {
}
}
+ handleMaxUploadReached(e) {
+ e.preventDefault();
+
+ const {formatMessage} = this.props.intl;
+
+ this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
+
+ return false;
+ }
+
render() {
let multiple = true;
if (UserAgent.isMobileApp()) {
@@ -322,10 +336,14 @@ class FileUpload extends React.Component {
accept = 'image/*';
}
+ const channelId = this.props.channelId || ChannelStore.getCurrentId();
+
+ const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId);
+
return (
<span
ref='input'
- className='btn btn-file'
+ className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')}
>
<span
className='icon'
@@ -335,7 +353,7 @@ class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
- onClick={this.props.onClick}
+ onClick={uploadsRemaining > 0 ? this.props.onClick : this.handleMaxUploadReached}
multiple={multiple}
accept={accept}
/>
diff --git a/webapp/components/integrations/components/installed_oauth_app.jsx b/webapp/components/integrations/components/installed_oauth_app.jsx
index 15a79ed4c..a6dea65bf 100644
--- a/webapp/components/integrations/components/installed_oauth_app.jsx
+++ b/webapp/components/integrations/components/installed_oauth_app.jsx
@@ -5,10 +5,10 @@ import React from 'react';
import FormError from 'components/form_error.jsx';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {regenerateOAuthAppSecret} from 'actions/admin_actions.jsx';
const FAKE_SECRET = '***************';
@@ -49,7 +49,7 @@ export default class InstalledOAuthApp extends React.Component {
handleRegenerate(e) {
e.preventDefault();
- Client.regenerateOAuthAppSecret(
+ regenerateOAuthAppSecret(
this.props.oauthApp.id,
(data) => {
this.props.oauthApp.client_secret = data.client_secret;
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index f4fd1d712..563c1aba9 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -5,13 +5,13 @@ import ReactDOM from 'react-dom';
import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import Client from 'client/web_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import ConfirmModal from './confirm_modal.jsx';
+import {inviteMembers} from 'actions/team_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -142,7 +142,7 @@ class InviteMemberModal extends React.Component {
this.setState({isSendingEmails: true});
- Client.inviteMembers(
+ inviteMembers(
data,
() => {
this.handleHide(false);
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 841061d48..9282e74ca 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -4,7 +4,6 @@
import LoadingScreen from 'components/loading_screen.jsx';
import UserStore from 'stores/user_store.jsx';
-import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
@@ -29,30 +28,6 @@ export default class LoggedIn extends React.Component {
this.onUserChanged = this.onUserChanged.bind(this);
this.setupUser = this.setupUser.bind(this);
- // Force logout of all tabs if one tab is logged out
- $(window).bind('storage', (e) => {
- // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
- if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected logout from a different tab'); //eslint-disable-line no-console
- GlobalActions.emitUserLoggedOutEvent('/', false);
- }
-
- if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected login from a different tab'); //eslint-disable-line no-console
- location.reload();
- }
- });
-
// Because current CSS requires the root tag to have specific stuff
$('#root').attr('class', 'channel-view');
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 6dc7af883..535cdfd12 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -6,6 +6,8 @@ import ErrorBar from 'components/error_bar.jsx';
import FormError from 'components/form_error.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
+import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
+import {checkMfa, webLogin} from 'actions/user_actions.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -47,7 +49,8 @@ export default class LoginController extends React.Component {
samlEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true',
loginId: '', // the browser will set a default for this
password: '',
- showMfa: false
+ showMfa: false,
+ loading: false
};
}
@@ -117,40 +120,38 @@ export default class LoginController extends React.Component {
return;
}
- if (global.window.mm_config.EnableMultifactorAuthentication === 'true') {
- Client.checkMfa(
- loginId,
- (data) => {
- if (data.mfa_required === 'true') {
- this.setState({showMfa: true});
- } else {
- this.submit(loginId, password, '');
- }
- },
- (err) => {
- this.setState({serverError: err.message});
+ checkMfa(
+ loginId,
+ (data) => {
+ if (data && data.mfa_required === 'true') {
+ this.setState({showMfa: true});
+ } else {
+ this.submit(loginId, password, '');
}
- );
- } else {
- this.submit(loginId, password, '');
- }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
}
submit(loginId, password, token) {
- this.setState({serverError: null});
+ this.setState({serverError: null, loading: true});
- Client.webLogin(
+ webLogin(
loginId,
password,
token,
() => {
// check for query params brought over from signup_user_complete
- const query = this.props.location.query;
- if (query.id || query.h) {
- Client.addUserToTeamFromInvite(
- query.d,
- query.h,
- query.id,
+ const hash = this.props.location.query.h;
+ const data = this.props.location.query.d;
+ const inviteId = this.props.location.query.id;
+ if (inviteId || hash) {
+ addUserToTeamFromInvite(
+ data,
+ hash,
+ inviteId,
(team) => {
this.finishSignin(team);
},
@@ -172,6 +173,7 @@ export default class LoginController extends React.Component {
err.id === 'ent.ldap.do_login.user_not_registered.app_error') {
this.setState({
showMfa: false,
+ loading: false,
serverError: (
<FormattedMessage
id='login.userNotFound'
@@ -182,6 +184,7 @@ export default class LoginController extends React.Component {
} else if (err.id === 'api.user.check_user_password.invalid.app_error' || err.id === 'ent.ldap.do_login.invalid_password.app_error') {
this.setState({
showMfa: false,
+ loading: false,
serverError: (
<FormattedMessage
id='login.invalidPassword'
@@ -190,7 +193,7 @@ export default class LoginController extends React.Component {
)
});
} else {
- this.setState({showMfa: false, serverError: err.message});
+ this.setState({showMfa: false, serverError: err.message, loading: false});
}
}
);
@@ -348,6 +351,23 @@ export default class LoginController extends React.Component {
errorClass = ' has-error';
}
+ let loginButton =
+ (<FormattedMessage
+ id='login.signIn'
+ defaultMessage='Sign in'
+ />);
+
+ if (this.state.loading) {
+ loginButton =
+ (<span>
+ <span className='fa fa-refresh icon--rotate'/>
+ <FormattedMessage
+ id='login.signInLoading'
+ defaultMessage='Signing in...'
+ />
+ </span>);
+ }
+
loginControls.push(
<form
key='loginBoxes'
@@ -387,10 +407,7 @@ export default class LoginController extends React.Component {
type='submit'
className='btn btn-primary'
>
- <FormattedMessage
- id='login.signIn'
- defaultMessage='Sign in'
- />
+ { loginButton }
</button>
</div>
</div>
diff --git a/webapp/components/member_list_channel.jsx b/webapp/components/member_list_channel.jsx
new file mode 100644
index 000000000..6f8a266ad
--- /dev/null
+++ b/webapp/components/member_list_channel.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx';
+import SearchableUserList from 'components/searchable_user_list.jsx';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
+import {searchUsers, loadProfilesAndTeamMembersAndChannelMembers, loadTeamMembersAndChannelMembersForProfilesList} from 'actions/user_actions.jsx';
+import {getChannelStats} from 'utils/async_client.jsx';
+
+import Constants from 'utils/constants.jsx';
+
+import * as UserAgent from 'utils/user_agent.jsx';
+
+import React from 'react';
+
+const USERS_PER_PAGE = 50;
+
+export default class MemberListChannel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.onStatsChange = this.onStatsChange.bind(this);
+ this.search = this.search.bind(this);
+ this.loadComplete = this.loadComplete.bind(this);
+
+ this.searchTimeoutId = 0;
+
+ const stats = ChannelStore.getCurrentStats();
+
+ this.state = {
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign({}, ChannelStore.getMembersInChannel()),
+ total: stats.member_count,
+ search: false,
+ term: '',
+ loading: true
+ };
+ }
+
+ componentDidMount() {
+ UserStore.addInTeamChangeListener(this.onChange);
+ UserStore.addStatusesChangeListener(this.onChange);
+ TeamStore.addChangeListener(this.onChange);
+ ChannelStore.addChangeListener(this.onChange);
+ ChannelStore.addStatsChangeListener(this.onStatsChange);
+
+ loadProfilesAndTeamMembersAndChannelMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
+ getChannelStats(ChannelStore.getCurrentId());
+ }
+
+ componentWillUnmount() {
+ UserStore.removeInTeamChangeListener(this.onChange);
+ UserStore.removeStatusesChangeListener(this.onChange);
+ TeamStore.removeChangeListener(this.onChange);
+ ChannelStore.removeChangeListener(this.onChange);
+ ChannelStore.removeStatsChangeListener(this.onStatsChange);
+ }
+
+ loadComplete() {
+ this.setState({loading: false});
+ }
+
+ onChange(force) {
+ if (this.state.search && !force) {
+ return;
+ } else if (this.state.search) {
+ this.search(this.state.term);
+ return;
+ }
+
+ this.setState({
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign({}, ChannelStore.getMembersInChannel())
+ });
+ }
+
+ onStatsChange() {
+ const stats = ChannelStore.getCurrentStats();
+ this.setState({total: stats.member_count});
+ }
+
+ nextPage(page) {
+ loadProfilesAndTeamMembersAndChannelMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
+ }
+
+ search(term) {
+ if (term === '') {
+ this.setState({
+ search: false,
+ term,
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
+ });
+ return;
+ }
+
+ clearTimeout(this.searchTimeoutId);
+
+ const searchTimeoutId = setTimeout(
+ () => {
+ searchUsers(
+ term,
+ TeamStore.getCurrentId(),
+ {},
+ (users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: true,
+ search: true,
+ users,
+ term,
+ teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
+ });
+ loadTeamMembersAndChannelMembersForProfilesList(users, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
+ }
+ );
+ },
+ Constants.SEARCH_TIMEOUT_MILLISECONDS
+ );
+
+ this.searchTimeoutId = searchTimeoutId;
+ }
+
+ render() {
+ const teamMembers = this.state.teamMembers;
+ const channelMembers = this.state.channelMembers;
+ const users = this.state.users;
+ const actionUserProps = {};
+
+ let usersToDisplay;
+ if (this.state.loading) {
+ usersToDisplay = null;
+ } else {
+ usersToDisplay = [];
+
+ for (let i = 0; i < users.length; i++) {
+ const user = users[i];
+
+ if (teamMembers[user.id] && channelMembers[user.id]) {
+ usersToDisplay.push(user);
+ actionUserProps[user.id] = {
+ channel: this.props.channel,
+ teamMember: teamMembers[user.id],
+ channelMember: channelMembers[user.id]
+ };
+ }
+ }
+ }
+
+ return (
+ <SearchableUserList
+ users={usersToDisplay}
+ usersPerPage={USERS_PER_PAGE}
+ total={this.state.total}
+ nextPage={this.nextPage}
+ search={this.search}
+ actions={[ChannelMembersDropdown]}
+ actionUserProps={actionUserProps}
+ focusOnMount={!UserAgent.isMobile()}
+ />
+ );
+ }
+}
+
+MemberListChannel.propTypes = {
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx
index a9db0e734..df17d7df2 100644
--- a/webapp/components/member_list_team.jsx
+++ b/webapp/components/member_list_team.jsx
@@ -23,6 +23,7 @@ export default class MemberListTeam extends React.Component {
super(props);
this.onChange = this.onChange.bind(this);
+ this.onTeamChange = this.onTeamChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
@@ -44,7 +45,7 @@ export default class MemberListTeam extends React.Component {
componentDidMount() {
UserStore.addInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
- TeamStore.addChangeListener(this.onChange.bind(null, true));
+ TeamStore.addChangeListener(this.onTeamChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), this.loadComplete);
@@ -54,7 +55,7 @@ export default class MemberListTeam extends React.Component {
componentWillUnmount() {
UserStore.removeInTeamChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
- TeamStore.removeChangeListener(this.onChange);
+ TeamStore.removeChangeListener(this.onTeamChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
@@ -62,6 +63,10 @@ export default class MemberListTeam extends React.Component {
this.setState({loading: false});
}
+ onTeamChange() {
+ this.onChange(true);
+ }
+
onChange(force) {
if (this.state.search && !force) {
return;
@@ -90,13 +95,16 @@ export default class MemberListTeam extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
TeamStore.getCurrentId(),
{},
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
this.setState({loading: true, search: true, users, term, teamMembers: Object.assign([], TeamStore.getMembersInTeam())});
loadTeamMembersForProfilesList(users, TeamStore.getCurrentId(), this.loadComplete);
}
@@ -104,6 +112,8 @@ export default class MemberListTeam extends React.Component {
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/mfa/mfa_controller.jsx b/webapp/components/mfa/mfa_controller.jsx
index 21b9737f8..cd9497985 100644
--- a/webapp/components/mfa/mfa_controller.jsx
+++ b/webapp/components/mfa/mfa_controller.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
+
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
@@ -16,13 +18,32 @@ export default class MFAController extends React.Component {
render() {
let backButton;
- if (window.mm_config.EnforceMultifactorAuthentication !== 'true') {
+ if (window.mm_config.EnforceMultifactorAuthentication === 'true') {
+ backButton = (
+ <div className='signup-header'>
+ <a
+ href='#'
+ onClick={(e) => {
+ e.preventDefault();
+ emitUserLoggedOutEvent('/login');
+ }}
+ >
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.logout'
+ defaultMessage='Logout'
+ />
+ </a>
+ </div>
+ );
+ } else {
backButton = (
<div className='signup-header'>
<Link to='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
+ defaultMessage='Back'
/>
</Link>
</div>
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index e4cff451d..d0b5f5399 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -107,17 +107,22 @@ export default class MoreChannels extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchMoreChannels(
term,
(channels) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
this.setState({search: true, channels});
}
);
},
SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
@@ -196,4 +201,4 @@ export default class MoreChannels extends React.Component {
MoreChannels.propTypes = {
onModalDismissed: React.PropTypes.func,
handleNewChannel: React.PropTypes.func
-}; \ No newline at end of file
+};
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 338d4edd1..b54b8701e 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -22,8 +22,6 @@ import PreferenceStore from 'stores/preference_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
@@ -37,7 +35,7 @@ import {FormattedMessage} from 'react-intl';
import {Popover, OverlayTrigger} from 'react-bootstrap';
-import {Link, browserHistory} from 'react-router/es6';
+import {Link} from 'react-router/es6';
import React from 'react';
@@ -111,23 +109,7 @@ export default class Navbar extends React.Component {
}
handleLeave() {
- var channelId = this.state.channel.id;
-
- Client.leaveChannel(channelId,
- () => {
- ChannelActions.loadChannelsForCurrentUser();
-
- if (this.state.isFavorite) {
- ChannelActions.unmarkFavorite(channelId);
- }
-
- const townsquare = ChannelStore.getByName('town-square');
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleLeave');
- }
- );
+ ChannelActions.leaveChannel(this.state.channel.id);
}
hideSidebars(e) {
diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index fc9fd0295..be97532dc 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -272,7 +272,7 @@ export default class NewChannelModal extends React.Component {
className='form-control no-resize'
ref='channel_purpose'
rows='4'
- placeholder={Utils.localizeMessage('channel_modal.purpose', 'Purpose')}
+ placeholder={Utils.localizeMessage('channel_modal.purposeEx', 'E.g.: "A channel to file bugs and improvements"')}
maxLength='250'
value={this.props.channelData.purpose}
onChange={this.handleChange}
@@ -309,7 +309,7 @@ export default class NewChannelModal extends React.Component {
className='form-control no-resize'
ref='channel_header'
rows='4'
- placeholder={Utils.localizeMessage('channel_modal.header', 'Header')}
+ placeholder={Utils.localizeMessage('channel_modal.headerEx', 'E.g.: "[Link Title](http://example.com)"')}
maxLength='128'
value={this.props.channelData.header}
onChange={this.handleChange}
diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx
index b37e07f2d..c6fe2525f 100644
--- a/webapp/components/password_reset_form.jsx
+++ b/webapp/components/password_reset_form.jsx
@@ -2,12 +2,12 @@
// See License.txt for license information.
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
+
+import {resetPassword} from 'actions/user_actions.jsx';
import React from 'react';
@@ -42,12 +42,11 @@ class PasswordResetForm extends React.Component {
error: null
});
- Client.resetPassword(
+ resetPassword(
this.props.location.query.code,
password,
() => {
this.setState({error: null});
- browserHistory.push('/login?extra=' + Constants.PASSWORD_CHANGE);
},
(err) => {
this.setState({error: err.message});
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 9cea3922a..5ffcb687a 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -5,6 +5,11 @@ import ProfilePicture from 'components/profile_picture.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+
+import TeamMembersModal from './team_members_modal.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
+import ChannelInviteModal from './channel_invite_modal.jsx';
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
@@ -22,10 +27,17 @@ export default class PopoverListMembers extends React.Component {
constructor(props) {
super(props);
+ this.showMembersModal = this.showMembersModal.bind(this);
+
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.closePopover = this.closePopover.bind(this);
- this.state = {showPopover: false};
+ this.state = {
+ showPopover: false,
+ showTeamMembersModal: false,
+ showChannelMembersModal: false,
+ showChannelInviteModal: false
+ };
}
componentDidUpdate() {
@@ -53,12 +65,31 @@ export default class PopoverListMembers extends React.Component {
this.setState({showPopover: false});
}
+ showMembersModal(e) {
+ e.preventDefault();
+
+ if (ChannelStore.isDefault(this.props.channel)) {
+ this.setState({
+ showPopover: false,
+ showTeamMembersModal: true
+ });
+ } else {
+ this.setState({
+ showPopover: false,
+ showChannelMembersModal: true
+ });
+ }
+ }
+
render() {
const popoverHtml = [];
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
+ let isAdmin = false;
const currentUserId = UserStore.getCurrentId();
+ isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+
if (members && teamMembers) {
members.sort((a, b) => {
const aName = Utils.displayUsername(a.id);
@@ -96,7 +127,7 @@ export default class PopoverListMembers extends React.Component {
key={'popover-member-' + i}
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`}
+ src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.last_picture_update}`}
width='26'
height='26'
/>
@@ -117,17 +148,37 @@ export default class PopoverListMembers extends React.Component {
}
});
+ let membersName = (
+ <FormattedMessage
+ id='members_popover.manageMembers'
+ defaultMessage='Manage Members'
+ />
+ );
+ if (!isAdmin && ChannelStore.isDefault(this.props.channel)) {
+ membersName = (
+ <FormattedMessage
+ id='members_popover.viewMembers'
+ defaultMessage='View Members'
+ />
+ );
+ }
+
popoverHtml.push(
<div
className='more-modal__row'
key={'popover-member-more'}
>
- <div className='col-sm-5'/>
+ <div className='col-sm-3'/>
<div className='more-modal__details'>
<div
className='more-modal__name'
>
- {'...'}
+ <a
+ href='#'
+ onClick={this.showMembersModal}
+ >
+ {membersName}
+ </a>
</div>
</div>
</div>
@@ -146,6 +197,38 @@ export default class PopoverListMembers extends React.Component {
defaultMessage='Members'
/>
);
+
+ let channelMembersModal;
+ if (this.state.showChannelMembersModal) {
+ channelMembersModal = (
+ <ChannelMembersModal
+ onModalDismissed={() => this.setState({showChannelMembersModal: false})}
+ showInviteModal={() => this.setState({showChannelInviteModal: true})}
+ channel={this.props.channel}
+ />
+ );
+ }
+
+ let teamMembersModal;
+ if (this.state.showTeamMembersModal) {
+ teamMembersModal = (
+ <TeamMembersModal
+ onHide={() => this.setState({showTeamMembersModal: false})}
+ isAdmin={isAdmin}
+ />
+ );
+ }
+
+ let channelInviteModal;
+ if (this.state.showChannelInviteModal) {
+ channelInviteModal = (
+ <ChannelInviteModal
+ onHide={() => this.setState({showChannelInviteModal: false})}
+ channel={this.props.channel}
+ />
+ );
+ }
+
return (
<div>
<div
@@ -181,6 +264,9 @@ export default class PopoverListMembers extends React.Component {
<div className='more-modal__list'>{popoverHtml}</div>
</Popover>
</Overlay>
+ {channelMembersModal}
+ {teamMembersModal}
+ {channelInviteModal}
</div>
);
}
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index f052ac4ae..896002a6c 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -150,10 +150,10 @@ export default class Post extends React.Component {
}
let timestamp = 0;
- if (!this.props.user || this.props.user.update_at == null) {
- timestamp = this.props.currentUser.update_at;
+ if (!this.props.user || this.props.user.last_picture_update == null) {
+ timestamp = this.props.currentUser.last_picture_update;
} else {
- timestamp = this.props.user.update_at;
+ timestamp = this.props.user.last_picture_update;
}
let sameUserClass = '';
@@ -250,7 +250,11 @@ export default class Post extends React.Component {
}
return (
- <div>
+ <div
+ ref={(div) => {
+ this.domNode = div;
+ }}
+ >
<div
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass + ' ' + hideControls + ' ' + dropdownOpenedClass}
@@ -285,6 +289,7 @@ export default class Post extends React.Component {
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
isCommentMention={this.props.isCommentMention}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
</div>
</div>
@@ -313,5 +318,6 @@ Post.propTypes = {
useMilitaryTime: React.PropTypes.bool.isRequired,
isFlagged: React.PropTypes.bool,
status: React.PropTypes.string,
- isBusy: React.PropTypes.bool
+ isBusy: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_attachment_oembed.jsx b/webapp/components/post_view/components/post_attachment_oembed.jsx
deleted file mode 100644
index 359c7cc35..000000000
--- a/webapp/components/post_view/components/post_attachment_oembed.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import React from 'react';
-
-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: {}});
- }
- });
- }
- return null;
- }
-
- 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'
- rel='noopener noreferrer'
- >
- {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/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx
new file mode 100644
index 000000000..20beaed51
--- /dev/null
+++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx
@@ -0,0 +1,212 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import OpenGraphStore from 'stores/opengraph_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+import * as CommonUtils from 'utils/commons.jsx';
+import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
+
+export default class PostAttachmentOpenGraph extends React.Component {
+ constructor(props) {
+ super(props);
+ this.imageDimentions = { // Image dimentions in pixels.
+ height: 150,
+ width: 150
+ };
+ this.maxDescriptionLength = 300;
+ this.descriptionEllipsis = '...';
+ this.fetchData = this.fetchData.bind(this);
+ this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
+ this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
+ this.onImageLoad = this.onImageLoad.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({
+ data: {},
+ imageLoaded: false,
+ imageVisible: this.props.previewCollapsed.startsWith('false')
+ });
+ this.fetchData(this.props.link);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({imageVisible: nextProps.previewCollapsed.startsWith('false')});
+ if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
+ this.fetchData(nextProps.link);
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (nextState.imageVisible !== this.state.imageVisible) {
+ return true;
+ }
+ if (nextState.imageLoaded !== this.state.imageLoaded) {
+ return true;
+ }
+ if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
+ return true;
+ }
+ return false;
+ }
+
+ componentDidMount() {
+ OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ componentDidUpdate() {
+ if (this.props.childComponentDidUpdateFunction) {
+ this.props.childComponentDidUpdateFunction();
+ }
+ }
+
+ componentWillUnmount() {
+ OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ onOpenGraphMetadataChange(url) {
+ if (url === this.props.link) {
+ this.fetchData(url);
+ }
+ }
+
+ fetchData(url) {
+ const data = OpenGraphStore.getOgInfo(url);
+ this.setState({data, imageLoaded: false});
+ if (Utils.isEmptyObject(data)) {
+ requestOpenGraphMetadata(url);
+ }
+ }
+
+ getBestImageUrl() {
+ const nearestPointData = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
+
+ const bestImage = nearestPointData.nearestPoint;
+ const bestImageLte = nearestPointData.nearestPointLte; // Best image <= 150px height and width
+
+ let finalBestImage;
+
+ if (
+ !Utils.isEmptyObject(bestImageLte) &&
+ bestImageLte.height <= this.imageDimentions.height &&
+ bestImageLte.width <= this.imageDimentions.width
+ ) {
+ finalBestImage = bestImageLte;
+ } else {
+ finalBestImage = bestImage;
+ }
+
+ return finalBestImage.secure_url || finalBestImage.url;
+ }
+
+ toggleImageVisibility() {
+ this.setState({imageVisible: !this.state.imageVisible});
+ }
+
+ onImageLoad() {
+ this.setState({imageLoaded: true});
+ }
+
+ loadImage(src) {
+ const img = new Image();
+ img.onload = this.onImageLoad;
+ img.src = src;
+ }
+
+ imageToggleAnchoreTag(imageUrl) {
+ if (imageUrl) {
+ return (
+ <a
+ className={'post__embed-visibility'}
+ data-expanded={this.state.imageVisible}
+ aria-label='Toggle Embed Visibility'
+ onClick={this.toggleImageVisibility}
+ />
+ );
+ }
+ return null;
+ }
+
+ imageTag(imageUrl) {
+ if (imageUrl && this.state.imageVisible) {
+ return (
+ <img
+ className={this.state.imageLoaded ? 'attachment__image' : 'attachment__image loading'}
+ src={this.state.imageLoaded ? imageUrl : null}
+ />
+ );
+ }
+ return null;
+ }
+
+ render() {
+ if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
+ return null;
+ }
+
+ const data = this.state.data;
+ const imageUrl = this.getBestImageUrl();
+ var description = data.description;
+
+ if (description.length > this.maxDescriptionLength) {
+ description = description.substring(0, this.maxDescriptionLength - this.descriptionEllipsis.length) + this.descriptionEllipsis;
+ }
+
+ if (imageUrl && this.state.imageVisible) {
+ this.loadImage(imageUrl);
+ }
+
+ return (
+ <div
+ className='attachment attachment--oembed'
+ ref='attachment'
+ >
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <span className='sitename'>{data.site_name}</span>
+ <h1
+ className='attachment__title has-link'
+ >
+ <a
+ className='attachment__title-link'
+ href={data.url || this.props.link}
+ target='_blank'
+ rel='noopener noreferrer'
+ title={data.title || data.url || this.props.link}
+ >
+ {data.title || data.url || this.props.link}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ <div>
+ <div>
+ {description} &nbsp;
+ {this.imageToggleAnchoreTag(imageUrl)}
+ </div>
+ {this.imageTag(imageUrl)}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+PostAttachmentOpenGraph.defaultProps = {
+ previewCollapsed: 'false'
+};
+
+PostAttachmentOpenGraph.propTypes = {
+ link: React.PropTypes.string.isRequired,
+ childComponentDidUpdateFunction: React.PropTypes.func,
+ previewCollapsed: React.PropTypes.string
+};
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx
index 60e682e8d..10c24aab2 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/components/post_body.jsx
@@ -188,6 +188,7 @@ export default class PostBody extends React.Component {
message={messageWrapper}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
);
}
@@ -221,5 +222,6 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string,
- isCommentMention: React.PropTypes.bool
+ isCommentMention: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx
index a65b608d7..cad618de0 100644
--- a/webapp/components/post_view/components/post_body_additional_content.jsx
+++ b/webapp/components/post_view/components/post_body_additional_content.jsx
@@ -2,12 +2,11 @@
// See License.txt for license information.
import PostAttachmentList from './post_attachment_list.jsx';
-import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
+import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
import PostImage from './post_image.jsx';
import YoutubeVideo from 'components/youtube_video.jsx';
import Constants from 'utils/constants.jsx';
-import OEmbedProviders from './providers.json';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
@@ -17,22 +16,24 @@ export default class PostBodyAdditionalContent extends React.Component {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
- this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
this.generateToggleableEmbed = this.generateToggleableEmbed.bind(this);
this.generateStaticEmbed = this.generateStaticEmbed.bind(this);
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
this.isLinkToggleable = this.isLinkToggleable.bind(this);
+ this.handleLinkLoadError = this.handleLinkLoadError.bind(this);
this.state = {
embedVisible: props.previewCollapsed.startsWith('false'),
- link: Utils.extractFirstLink(props.post.message)
+ link: Utils.extractFirstLink(props.post.message),
+ linkLoadError: false
};
}
componentWillReceiveProps(nextProps) {
this.setState({
embedVisible: nextProps.previewCollapsed.startsWith('false'),
- link: Utils.extractFirstLink(nextProps.post.message)
+ link: Utils.extractFirstLink(nextProps.post.message),
+ linkLoadError: false
});
}
@@ -46,6 +47,9 @@ export default class PostBodyAdditionalContent extends React.Component {
if (nextState.embedVisible !== this.state.embedVisible) {
return true;
}
+ if (nextState.linkLoadError !== this.state.linkLoadError) {
+ return true;
+ }
return false;
}
@@ -66,25 +70,11 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
- getOEmbedProvider(link) {
- for (let i = 0; i < OEmbedProviders.length; i++) {
- for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
- if (link.match(OEmbedProviders[i].patterns[j])) {
- return OEmbedProviders[i];
- }
- }
- }
-
- return null;
- }
-
isLinkImage(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 true;
- }
+ const regex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i;
+ const match = link.match(regex);
+ if (match && match[1]) {
+ return true;
}
return false;
@@ -107,6 +97,12 @@ export default class PostBodyAdditionalContent extends React.Component {
return false;
}
+ handleLinkLoadError() {
+ this.setState({
+ linkLoadError: true
+ });
+ }
+
generateToggleableEmbed() {
const link = this.state.link;
if (!link) {
@@ -128,6 +124,7 @@ export default class PostBodyAdditionalContent extends React.Component {
<PostImage
channelId={this.props.post.channel_id}
link={link}
+ onLinkLoadError={this.handleLinkLoadError}
/>
);
}
@@ -141,39 +138,21 @@ export default class PostBodyAdditionalContent extends React.Component {
}
const link = Utils.extractFirstLink(this.props.post.message);
- if (!link) {
- return null;
- }
-
- if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
- const provider = this.getOEmbedProvider(link);
-
- if (provider) {
- return (
- <PostAttachmentOEmbed
- provider={provider}
- link={link}
- />
- );
- }
+ if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
+ return (
+ <PostAttachmentOpenGraph
+ link={link}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
+ previewCollapsed={this.props.previewCollapsed}
+ />
+ );
}
return null;
}
render() {
- const staticEmbed = this.generateStaticEmbed();
-
- if (staticEmbed) {
- return (
- <div>
- {this.props.message}
- {staticEmbed}
- </div>
- );
- }
-
- if (this.isLinkToggleable()) {
+ if (this.isLinkToggleable() && !this.state.linkLoadError) {
const messageWithToggle = [];
// if message has only one line and starts with a link place toggle in this only line
@@ -213,6 +192,17 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
+ const staticEmbed = this.generateStaticEmbed();
+
+ if (staticEmbed) {
+ return (
+ <div>
+ {this.props.message}
+ {staticEmbed}
+ </div>
+ );
+ }
+
return this.props.message;
}
}
@@ -224,5 +214,6 @@ PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired,
message: React.PropTypes.element.isRequired,
compactDisplay: React.PropTypes.bool,
- previewCollapsed: React.PropTypes.string
+ previewCollapsed: React.PropTypes.string,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_image.jsx b/webapp/components/post_view/components/post_image.jsx
index d1d1a6c7a..9a761bfca 100644
--- a/webapp/components/post_view/components/post_image.jsx
+++ b/webapp/components/post_view/components/post_image.jsx
@@ -53,6 +53,9 @@ export default class PostImageEmbed extends React.Component {
errored: true,
loaded: true
});
+ if (this.props.onLinkLoadError) {
+ this.props.onLinkLoadError();
+ }
}
render() {
@@ -79,5 +82,6 @@ export default class PostImageEmbed extends React.Component {
}
PostImageEmbed.propTypes = {
- link: React.PropTypes.string.isRequired
+ link: React.PropTypes.string.isRequired,
+ onLinkLoadError: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx
index aa204add1..3f38bdffe 100644
--- a/webapp/components/post_view/components/post_info.jsx
+++ b/webapp/components/post_view/components/post_info.jsx
@@ -8,12 +8,10 @@ import PostTime from './post_time.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as PostActions from 'actions/post_actions.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import React from 'react';
@@ -23,31 +21,42 @@ export default class PostInfo extends React.Component {
constructor(props) {
super(props);
- this.handleDropdownClick = this.handleDropdownClick.bind(this);
+ this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
this.handlePermalink = this.handlePermalink.bind(this);
this.removePost = this.removePost.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
}
- handleDropdownClick(e) {
- var position = $('#post-list').height() - $(e.target).offset().top;
- var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu');
+ handleDropdownOpened() {
+ this.props.handleDropdownOpened(true);
+
+ const position = $('#post-list').height() - $(this.refs.dropdownToggle).offset().top;
+ const dropdown = $(this.refs.dropdown);
+
if (position < dropdown.height()) {
dropdown.addClass('bottom');
}
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
componentDidMount() {
- $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', () => this.props.handleDropdownOpened(true));
+ $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', this.handleDropdownOpened);
$('#post_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false));
}
createDropdown() {
var post = this.props.post;
- var isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
- const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
+
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING) {
return '';
@@ -139,7 +148,7 @@ export default class PostInfo extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
key='deletePost'
@@ -162,12 +171,12 @@ export default class PostInfo extends React.Component {
);
}
- if (isOwner && !isSystemMessage) {
+ if (this.canEdit) {
dropdownContents.push(
<li
key='editPost'
role='presentation'
- className='dropdown-submenu'
+ className={this.canEdit ? 'dropdown-submenu' : 'dropdown-submenu hide'}
>
<a
href='#'
@@ -199,15 +208,16 @@ export default class PostInfo extends React.Component {
id={'post_dropdown' + this.props.post.id}
>
<a
+ ref='dropdownToggle'
href='#'
className='dropdown-toggle post__dropdown theme'
type='button'
data-toggle='dropdown'
aria-expanded='false'
- onClick={this.handleDropdownClick}
/>
<div className='dropdown-menu__content'>
<ul
+ ref='dropdown'
className='dropdown-menu'
role='menu'
>
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index 29358122b..7550db348 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -45,6 +45,7 @@ export default class PostList extends React.Component {
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -159,7 +160,7 @@ export default class PostList extends React.Component {
const id = this.props.postList.order[i];
const element = this.refs[id];
- if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) {
+ if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
// this post is off the top of the screen so the last one is at the top of the screen
let topPostId;
@@ -347,6 +348,7 @@ export default class PostList extends React.Component {
isFlagged={isFlagged}
status={status}
isBusy={this.props.isBusy}
+ childComponentDidUpdateFunction={this.childComponentDidUpdate}
/>
);
@@ -421,6 +423,11 @@ export default class PostList extends React.Component {
this.scrollToBottom();
}
});
+
+ // This avoids the scroll jumping from top to bottom after the page has rendered (PLT-5025).
+ if (!this.refs.newMessageSeparator) {
+ this.scrollToBottom();
+ }
} else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => {
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
@@ -487,6 +494,12 @@ export default class PostList extends React.Component {
);
}
+ checkAndUpdateScrolling() {
+ if (this.props.postList != null && this.refs.postlist) {
+ this.updateScrolling();
+ }
+ }
+
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -504,9 +517,11 @@ export default class PostList extends React.Component {
}
componentDidUpdate() {
- if (this.props.postList != null && this.refs.postlist) {
- this.updateScrolling();
- }
+ this.checkAndUpdateScrolling();
+ }
+
+ childComponentDidUpdate() {
+ this.checkAndUpdateScrolling();
}
render() {
diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx
index 2d17e74c4..4e27cd29a 100644
--- a/webapp/components/post_view/components/post_message_container.jsx
+++ b/webapp/components/post_view/components/post_message_container.jsx
@@ -89,7 +89,7 @@ export default class PostMessageContainer extends React.Component {
return (
<PostMessageView
options={this.props.options}
- message={this.props.post.message}
+ post={this.props.post}
emojis={this.state.emojis}
enableFormatting={this.state.enableFormatting}
mentionKeys={this.state.mentionKeys}
diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx
index 24f96a8d9..eff791aec 100644
--- a/webapp/components/post_view/components/post_message_view.jsx
+++ b/webapp/components/post_view/components/post_message_view.jsx
@@ -2,14 +2,16 @@
// See License.txt for license information.
import React from 'react';
+import {FormattedMessage} from 'react-intl';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as PostUtils from 'utils/post_utils.jsx';
export default class PostMessageView extends React.Component {
static propTypes = {
options: React.PropTypes.object.isRequired,
- message: React.PropTypes.string.isRequired,
+ post: React.PropTypes.object.isRequired,
emojis: React.PropTypes.object.isRequired,
enableFormatting: React.PropTypes.bool.isRequired,
mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
@@ -23,7 +25,7 @@ export default class PostMessageView extends React.Component {
return true;
}
- if (nextProps.message !== this.props.message) {
+ if (nextProps.post.message !== this.props.post.message) {
return true;
}
@@ -47,9 +49,28 @@ export default class PostMessageView extends React.Component {
return false;
}
+ editedIndicator() {
+ return (
+ PostUtils.isEdited(this.props.post) ?
+ <span className='edited'>
+ <FormattedMessage
+ id='post_message_view.edited'
+ defaultMessage='(edited)'
+ />
+ </span> :
+ ''
+ );
+ }
+
render() {
if (!this.props.enableFormatting) {
- return <span>{this.props.message}</span>;
+ return (
+ <span>
+ {this.props.post.message}
+ &nbsp;
+ {this.editedIndicator()}
+ </span>
+ );
}
const options = Object.assign({}, this.props.options, {
@@ -62,10 +83,13 @@ export default class PostMessageView extends React.Component {
});
return (
- <span
- onClick={Utils.handleFormattedTextClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}}
- />
+ <div>
+ <span
+ onClick={Utils.handleFormattedTextClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}}
+ />
+ {this.editedIndicator()}
+ </div>
);
}
}
diff --git a/webapp/components/post_view/components/post_time.jsx b/webapp/components/post_view/components/post_time.jsx
index c8e57f6a9..caad12d4a 100644
--- a/webapp/components/post_view/components/post_time.jsx
+++ b/webapp/components/post_view/components/post_time.jsx
@@ -27,7 +27,10 @@ export default class PostTime extends React.Component {
render() {
return (
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={getDateForUnixTicks(this.props.eventTime).toISOString()}
+ >
{getDateForUnixTicks(this.props.eventTime).toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})}
</time>
);
diff --git a/webapp/components/post_view/components/providers.json b/webapp/components/post_view/components/providers.json
deleted file mode 100644
index b5899c225..000000000
--- a/webapp/components/post_view/components/providers.json
+++ /dev/null
@@ -1,376 +0,0 @@
-[
- {
- "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/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
index a18a73b86..a18d0ac38 100644
--- a/webapp/components/post_view/post_view_controller.jsx
+++ b/webapp/components/post_view/post_view_controller.jsx
@@ -229,8 +229,16 @@ export default class PostViewController extends React.Component {
onPostListScroll(atBottom) {
if (atBottom) {
+ let lastViewedBottom;
const lastPost = PostStore.getLatestPost(this.state.channel.id);
- this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom: lastPost.create_at || new Date().getTime()});
+
+ if (lastPost && lastPost.create_at) {
+ lastViewedBottom = lastPost.create_at;
+ } else {
+ lastViewedBottom = new Date().getTime();
+ }
+
+ this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom});
} else {
this.setState({scrollType: ScrollTypes.FREE});
}
diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx
index 7cb2f7261..22cf60004 100644
--- a/webapp/components/profile_popover.jsx
+++ b/webapp/components/profile_popover.jsx
@@ -83,6 +83,9 @@ export default class ProfilePopover extends React.Component {
openDirectChannelToUser(
user,
(channel) => {
+ if (Utils.isMobile()) {
+ GlobalActions.emitCloseRightHandSide();
+ }
this.setState({loadingDMChannel: -1});
if (this.props.hide) {
this.props.hide();
@@ -185,34 +188,34 @@ export default class ProfilePopover extends React.Component {
const fullname = Utils.getFullName(this.props.user);
if (fullname) {
dataContent.push(
- <div
- data-toggle='tooltip'
- title={fullname}
- key='user-popover-fullname'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{fullname}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
if (this.props.user.position) {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
dataContent.push(
- <div
- data-toggle='tooltip'
- title={position}
- key='user-popover-position'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='positionTooltip'>{position}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{position}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 8b7642fd8..26659c7a1 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -9,9 +9,6 @@ import ProfilePicture from 'components/profile_picture.jsx';
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
@@ -19,6 +16,7 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
@@ -36,6 +34,10 @@ export default class RhsComment extends React.Component {
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
+
this.state = {};
}
@@ -44,6 +46,10 @@ export default class RhsComment extends React.Component {
GlobalActions.showGetPostLinkModal(this.props.post);
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
removePost() {
GlobalActions.emitRemovePost(this.props.post);
}
@@ -110,8 +116,8 @@ export default class RhsComment extends React.Component {
return '';
}
- const isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
var dropdownContents = [];
@@ -170,7 +176,7 @@ export default class RhsComment extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
role='presentation'
@@ -193,11 +199,12 @@ export default class RhsComment extends React.Component {
);
}
- if (isOwner) {
+ if (this.canEdit) {
dropdownContents.push(
<li
role='presentation'
key='edit-button'
+ className={this.canEdit ? '' : 'hide'}
>
<a
href='#'
@@ -239,7 +246,7 @@ export default class RhsComment extends React.Component {
currentUserCss = 'current--user';
}
- var timestamp = this.props.currentUser.update_at;
+ var timestamp = this.props.currentUser.last_picture_update;
let status = this.props.status;
if (post.props && post.props.from_webhook === 'true') {
@@ -471,7 +478,10 @@ export default class RhsComment extends React.Component {
</li>
{botIndicator}
<li className='col'>
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={Utils.getDateForUnixTicks(post.create_at).toISOString()}
+ >
{Utils.getDateForUnixTicks(post.create_at).toLocaleString('en', timeOptions)}
</time>
{flagTrigger}
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 95f5fc1ac..7d00e2322 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -11,7 +11,6 @@ import RhsDropdown from 'components/rhs_dropdown.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
@@ -20,6 +19,7 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
@@ -34,6 +34,10 @@ export default class RhsRootPost extends React.Component {
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
+
this.state = {};
}
@@ -42,6 +46,10 @@ export default class RhsRootPost extends React.Component {
GlobalActions.showGetPostLinkModal(this.props.post);
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
shouldComponentUpdate(nextProps) {
if (nextProps.status !== this.props.status) {
return true;
@@ -96,13 +104,13 @@ export default class RhsRootPost extends React.Component {
const post = this.props.post;
const user = this.props.user;
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
- var isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
- const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
- var timestamp = user ? user.update_at : 0;
+ var timestamp = user ? user.last_picture_update : 0;
var channel = ChannelStore.get(post.channel_id);
const flagIcon = Constants.FLAG_ICON_SVG;
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
+
var type = 'Post';
if (post.root_id.length > 0) {
type = 'Comment';
@@ -189,7 +197,7 @@ export default class RhsRootPost extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
key='rhs-root-delete'
@@ -209,11 +217,12 @@ export default class RhsRootPost extends React.Component {
);
}
- if (isOwner && !isSystemMessage) {
+ if (this.canEdit) {
dropdownContents.push(
<li
key='rhs-root-edit'
role='presentation'
+ className={this.canEdit ? '' : 'hide'}
>
<a
href='#'
@@ -408,7 +417,10 @@ export default class RhsRootPost extends React.Component {
<li className='col__name'>{userProfile}</li>
{botIndicator}
<li className='col'>
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={Utils.getDateForUnixTicks(post.create_at).toISOString()}
+ >
{Utils.getDateForUnixTicks(post.create_at).toLocaleString('en', timeOptions)}
</time>
<OverlayTrigger
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index be50c7d48..465df5d79 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -8,11 +8,12 @@ import Client from 'client/web_client.jsx';
import {IntlProvider} from 'react-intl';
import React from 'react';
-
import FastClick from 'fastclick';
+import $ from 'jquery';
import {browserHistory} from 'react-router/es6';
import UserStore from 'stores/user_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
export default class Root extends React.Component {
constructor(props) {
@@ -35,6 +36,30 @@ export default class Root extends React.Component {
}
/*eslint-enable */
+ // Force logout of all tabs if one tab is logged out
+ $(window).bind('storage', (e) => {
+ // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
+ if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected logout from a different tab'); //eslint-disable-line no-console
+ GlobalActions.emitUserLoggedOutEvent('/', false);
+ }
+
+ if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected login from a different tab'); //eslint-disable-line no-console
+ location.reload();
+ }
+ });
+
// Fastclick
FastClick.attach(document.body);
}
diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx
index a7e9bfcac..c5fcd4697 100644
--- a/webapp/components/search_bar.jsx
+++ b/webapp/components/search_bar.jsx
@@ -2,9 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import SearchStore from 'stores/search_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -15,7 +12,7 @@ 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';
-import {loadProfilesForPosts, getFlaggedPosts} from 'actions/post_actions.jsx';
+import {getFlaggedPosts, performSearch} from 'actions/post_actions.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -119,26 +116,18 @@ export default class SearchBar extends React.Component {
if (terms.length) {
this.setState({isSearching: true});
- Client.search(
+ performSearch(
terms,
isMentionSearch,
- (data) => {
+ () => {
this.setState({isSearching: false});
- if (Utils.isMobile()) {
- ReactDOM.findDOMNode(this.refs.search).value = '';
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_SEARCH,
- results: data,
- is_mention_search: isMentionSearch
- });
- loadProfilesForPosts(data.posts);
+ if (Utils.isMobile() && this.search) {
+ this.search.value = '';
+ }
},
- (err) => {
+ () => {
this.setState({isSearching: false});
- AsyncClient.dispatchError(err, 'search');
}
);
}
@@ -147,7 +136,7 @@ export default class SearchBar extends React.Component {
handleSubmit(e) {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
- $(ReactDOM.findDOMNode(this.refs.search)).find('input').blur();
+ $(this.search).find('input').blur();
this.clearFocus();
}
@@ -276,7 +265,9 @@ export default class SearchBar extends React.Component {
>
<span className='fa fa-search sidebar__search-icon'/>
<SuggestionBox
- ref='search'
+ ref={(search) => {
+ this.search = search;
+ }}
className='form-control search-bar'
placeholder={Utils.localizeMessage('search_bar.search', 'Search')}
value={this.state.searchTerm}
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index a0245b7e4..86d1bac1d 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -124,6 +124,12 @@ export default class SearchResults extends React.Component {
window.removeEventListener('resize', this.handleResize);
}
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.searchTerm !== prevState.searchTerm) {
+ this.resize();
+ }
+ }
+
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 76681959e..be62653c0 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -62,7 +62,7 @@ export default class SearchResultsItem extends React.Component {
render() {
let channelName = null;
const channel = this.props.channel;
- const timestamp = UserStore.getCurrentUser().update_at;
+ const timestamp = UserStore.getCurrentUser().last_picture_update;
const user = this.props.user || {};
const post = this.props.post;
const flagIcon = Constants.FLAG_ICON_SVG;
@@ -285,7 +285,7 @@ export default class SearchResultsItem extends React.Component {
</li>
{rhsControls}
</ul>
- <div className='search-item-snippet'>
+ <div className='search-item-snippet post__body'>
{message}
</div>
</div>
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index 904e6c8d1..5971ce584 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -49,7 +49,7 @@ export default class SettingItemMax extends React.Component {
submit = (
<input
type='submit'
- className='btn btn-sm btn-primary'
+ className='btn btn-sm btn-primary pull-right'
href='#'
onClick={this.props.submit}
value={Utils.localizeMessage('setting_item_max.save', 'Save')}
@@ -88,7 +88,7 @@ export default class SettingItemMax extends React.Component {
{clientError}
{submit}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm pull-right'
href='#'
onClick={this.props.updateSection}
>
diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx
index b74ee8eb7..d1ff60c6a 100644
--- a/webapp/components/setting_picture.jsx
+++ b/webapp/components/setting_picture.jsx
@@ -73,7 +73,7 @@ export default class SettingPicture extends React.Component {
/>
);
} else {
- var confirmButtonClass = 'btn btn-sm';
+ var confirmButtonClass = 'btn btn-sm pull-right';
if (this.props.submitActive) {
confirmButtonClass += ' btn-primary';
} else {
@@ -132,7 +132,7 @@ export default class SettingPicture extends React.Component {
</span>
{confirmButton}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm theme pull-right'
href='#'
onClick={self.props.updateSection}
>
diff --git a/webapp/components/should_verify_email.jsx b/webapp/components/should_verify_email.jsx
index 5ac67e383..61edf9422 100644
--- a/webapp/components/should_verify_email.jsx
+++ b/webapp/components/should_verify_email.jsx
@@ -2,11 +2,12 @@
// See License.txt for license information.
import {FormattedMessage} from 'react-intl';
-import Client from 'client/web_client.jsx';
import React from 'react';
import {Link} from 'react-router/es6';
+import {resendVerification} from 'actions/user_actions.jsx';
+
export default class ShouldVerifyEmail extends React.Component {
constructor(props) {
super(props);
@@ -22,7 +23,7 @@ export default class ShouldVerifyEmail extends React.Component {
this.setState({resendStatus: 'sending'});
- Client.resendVerification(
+ resendVerification(
email,
() => {
this.setState({resendStatus: 'success'});
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index a5fbd2659..9bc4a5639 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -10,7 +10,7 @@ import * as Utils from 'utils/utils.jsx';
import SidebarHeaderDropdown from './sidebar_header_dropdown.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
-import {Preferences, TutorialSteps} from 'utils/constants.jsx';
+import {Preferences, TutorialSteps, Constants} from 'utils/constants.jsx';
import {createMenuTip} from 'components/tutorial/tutorial_tip.jsx';
export default class SidebarHeader extends React.Component {
@@ -59,7 +59,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
@@ -78,7 +78,7 @@ export default class SidebarHeader extends React.Component {
teamNameWithToolTip = (
<OverlayTrigger
trigger={['hover', 'focus']}
- delayShow={1000}
+ delayShow={Constants.OVERLAY_TIME_DELAY}
placement='bottom'
overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDescription}</Tooltip>}
ref='descriptionOverlay'
@@ -91,16 +91,13 @@ export default class SidebarHeader extends React.Component {
return (
<div className='team__header theme'>
{tutorialTip}
- <a
- href='#'
- onClick={this.toggleDropdown}
- >
+ <div>
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
{teamNameWithToolTip}
</div>
- </a>
+ </div>
<SidebarHeaderDropdown
ref='dropdown'
teamType={this.props.teamType}
diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx
index 826d9a342..86432e3ab 100644
--- a/webapp/components/sidebar_header_dropdown.jsx
+++ b/webapp/components/sidebar_header_dropdown.jsx
@@ -351,7 +351,10 @@ export default class SidebarHeaderDropdown extends React.Component {
if (moreTeams) {
teams.push(
<li key='joinTeam_li'>
- <Link to='/select_team'>
+ <Link
+ onClick={this.handleClick}
+ to='/select_team'
+ >
<FormattedMessage
id='navbar_dropdown.join'
defaultMessage='Join Another Team'
diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx
index aa3493c96..9ed10b94c 100644
--- a/webapp/components/signup/components/signup_email.jsx
+++ b/webapp/components/signup/components/signup_email.jsx
@@ -5,11 +5,10 @@ import LoadingScreen from 'components/loading_screen.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {track} from 'actions/analytics_actions.jsx';
-
-import BrowserStore from 'stores/browser_store.jsx';
+import {getInviteInfo} from 'actions/team_actions.jsx';
+import {loginById, createUserWithInvite} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
@@ -58,7 +57,7 @@ export default class SignupEmail extends React.Component {
loading = false;
} else if (inviteId && inviteId.length > 0) {
loading = true;
- Client.getInviteInfo(
+ getInviteInfo(
inviteId,
(inviteData) => {
if (!inviteData) {
@@ -118,26 +117,12 @@ export default class SignupEmail extends React.Component {
handleSignupSuccess(user, data) {
track('signup', 'signup_user_02_complete');
- Client.loginById(
+ loginById(
data.id,
user.password,
'',
- () => {
- if (this.state.hash > 0) {
- BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
- }
-
- GlobalActions.emitInitialLoad(
- () => {
- const query = this.props.location.query;
- if (query.redirect_to) {
- browserHistory.push(query.redirect_to);
- } else {
- GlobalActions.redirectUserToDefaultTeam();
- }
- }
- );
- },
+ this.state.hash,
+ null,
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
@@ -241,7 +226,7 @@ export default class SignupEmail extends React.Component {
allow_marketing: true
};
- Client.createUserWithInvite(user,
+ createUserWithInvite(user,
this.state.data,
this.state.hash,
this.state.inviteId,
diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx
index d80b27159..4c9afc8d6 100644
--- a/webapp/components/signup/components/signup_ldap.jsx
+++ b/webapp/components/signup/components/signup_ldap.jsx
@@ -5,9 +5,10 @@ import FormError from 'components/form_error.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {track} from 'actions/analytics_actions.jsx';
+import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
+import {webLoginByLdap} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -55,7 +56,7 @@ export default class SignupLdap extends React.Component {
this.setState({ldapError: ''});
- Client.webLoginByLdap(
+ webLoginByLdap(
this.state.ldapId,
this.state.ldapPassword,
null,
@@ -69,11 +70,15 @@ export default class SignupLdap extends React.Component {
}
handleLdapSignupSuccess() {
- if (this.props.location.query.id || this.props.location.query.h) {
- Client.addUserToTeamFromInvite(
- this.props.location.query.d,
- this.props.location.query.h,
- this.props.location.query.id,
+ const hash = this.props.location.query.h;
+ const data = this.props.location.query.d;
+ const inviteId = this.props.location.query.id;
+
+ if (inviteId || hash) {
+ addUserToTeamFromInvite(
+ data,
+ hash,
+ inviteId,
() => {
this.finishSignup();
},
diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx
index 9bf5936be..737431926 100644
--- a/webapp/components/signup/signup_controller.jsx
+++ b/webapp/components/signup/signup_controller.jsx
@@ -12,6 +12,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
+import {addUserToTeamFromInvite, getInviteInfo} from 'actions/team_actions.jsx';
import logoImage from 'images/logo.png';
import ErrorBar from 'components/error_bar.jsx';
@@ -68,7 +69,7 @@ export default class SignupController extends React.Component {
const userLoggedIn = UserStore.getCurrentUser() != null;
if ((inviteId || hash) && userLoggedIn) {
- Client.addUserToTeamFromInvite(
+ addUserToTeamFromInvite(
data,
hash,
inviteId,
@@ -79,11 +80,16 @@ export default class SignupController extends React.Component {
}
);
},
- (e) => {
+ () => {
this.setState({ // eslint-disable-line react/no-did-mount-set-state
noOpenServerError: true,
loading: false,
- serverError: e.message
+ serverError: (
+ <FormattedMessage
+ id='signup_user_completed.invalid_invite'
+ defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.'
+ />
+ )
});
}
);
@@ -92,7 +98,7 @@ export default class SignupController extends React.Component {
}
if (inviteId) {
- Client.getInviteInfo(
+ getInviteInfo(
inviteId,
(inviteData) => {
if (!inviteData) {
diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx
index 9263c6e50..5f79e08ae 100644
--- a/webapp/components/suggestion/at_mention_provider.jsx
+++ b/webapp/components/suggestion/at_mention_provider.jsx
@@ -6,6 +6,7 @@ import Provider from './provider.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
+import SuggestionStore from 'stores/suggestion_store.jsx';
import {autocompleteUsersInChannel} from 'actions/user_actions.jsx';
@@ -70,7 +71,7 @@ class AtMentionSuggestion extends Suggestion {
icon = (
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.update_at}
+ src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.last_picture_update}
/>
);
}
@@ -161,6 +162,8 @@ export default class AtMentionProvider extends Provider {
});
}
);
+ } else {
+ SuggestionStore.clearSuggestions(suggestionId);
}
}
}
diff --git a/webapp/components/suggestion/channel_mention_provider.jsx b/webapp/components/suggestion/channel_mention_provider.jsx
index 63e6944ac..f1d6d9e76 100644
--- a/webapp/components/suggestion/channel_mention_provider.jsx
+++ b/webapp/components/suggestion/channel_mention_provider.jsx
@@ -51,61 +51,83 @@ class ChannelMentionSuggestion extends Suggestion {
}
export default class ChannelMentionProvider extends Provider {
+ constructor() {
+ super();
+
+ this.lastCompletedWord = '';
+ }
+
handlePretextChanged(suggestionId, pretext) {
- const captured = (/(^|\s)(~([^~]*))$/i).exec(pretext.toLowerCase());
- if (captured) {
- const prefix = captured[3];
+ const captured = (/(^|\s)(~([^~\r\n]*))$/i).exec(pretext.toLowerCase());
- this.startNewRequest(prefix);
+ if (!captured) {
+ // Not a channel mention
+ return;
+ }
- autocompleteChannels(
- prefix,
- (data) => {
- if (this.shouldCancelDispatch(prefix)) {
- return;
- }
+ if (this.lastCompletedWord && captured[0].startsWith(this.lastCompletedWord)) {
+ // It appears we're still matching a channel handle that we already completed
+ return;
+ }
+
+ // Clear the last completed word since we've started to match new text
+ this.lastCompletedWord = '';
+
+ const prefix = captured[3];
+
+ this.startNewRequest(prefix);
+
+ autocompleteChannels(
+ prefix,
+ (data) => {
+ if (this.shouldCancelDispatch(prefix)) {
+ return;
+ }
+
+ const channels = data;
- const channels = data;
-
- // Wrap channels in an outer object to avoid overwriting the 'type' property.
- const wrappedChannels = [];
- const wrappedMoreChannels = [];
- const moreChannels = [];
- channels.forEach((item) => {
- if (ChannelStore.get(item.id)) {
- wrappedChannels.push({
- type: Constants.MENTION_CHANNELS,
- channel: item
- });
- return;
- }
-
- wrappedMoreChannels.push({
- type: Constants.MENTION_MORE_CHANNELS,
+ // Wrap channels in an outer object to avoid overwriting the 'type' property.
+ const wrappedChannels = [];
+ const wrappedMoreChannels = [];
+ const moreChannels = [];
+ channels.forEach((item) => {
+ if (ChannelStore.get(item.id)) {
+ wrappedChannels.push({
+ type: Constants.MENTION_CHANNELS,
channel: item
});
+ return;
+ }
- moreChannels.push(item);
+ wrappedMoreChannels.push({
+ type: Constants.MENTION_MORE_CHANNELS,
+ channel: item
});
- const wrapped = wrappedChannels.concat(wrappedMoreChannels);
- const mentions = wrapped.map((item) => '~' + item.channel.name);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_MORE_CHANNELS,
- channels: moreChannels
- });
+ moreChannels.push(item);
+ });
+
+ const wrapped = wrappedChannels.concat(wrappedMoreChannels);
+ const mentions = wrapped.map((item) => '~' + item.channel.name);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MORE_CHANNELS,
+ channels: moreChannels
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
+ id: suggestionId,
+ matchedPretext: captured[2],
+ terms: mentions,
+ items: wrapped,
+ component: ChannelMentionSuggestion
+ });
+ }
+ );
+ }
- AppDispatcher.handleServerAction({
- type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
- id: suggestionId,
- matchedPretext: captured[2],
- terms: mentions,
- items: wrapped,
- component: ChannelMentionSuggestion
- });
- }
- );
- }
+ handleCompleteWord(term) {
+ this.lastCompletedWord = term;
}
}
diff --git a/webapp/components/suggestion/emoticon_provider.jsx b/webapp/components/suggestion/emoticon_provider.jsx
index 6bb0aee3b..6a4332e2f 100644
--- a/webapp/components/suggestion/emoticon_provider.jsx
+++ b/webapp/components/suggestion/emoticon_provider.jsx
@@ -14,7 +14,7 @@ const MIN_EMOTICON_LENGTH = 2;
class EmoticonSuggestion extends Suggestion {
render() {
const text = this.props.term;
- const emoticon = this.props.item;
+ const emoji = this.props.item.emoji;
let className = 'emoticon-suggestion';
if (this.props.isSelection) {
@@ -30,7 +30,7 @@ class EmoticonSuggestion extends Suggestion {
<img
alt={text}
className='emoticon-suggestion__image'
- src={EmojiStore.getEmojiImageUrl(emoticon)}
+ src={EmojiStore.getEmojiImageUrl(emoji)}
title={text}
/>
</div>
@@ -73,15 +73,24 @@ export default class EmoticonProvider {
// check for named emoji
for (const [name, emoji] of EmojiStore.getEmojis()) {
- if (name.indexOf(partialName) !== -1) {
- matched.push(emoji);
+ if (emoji.aliases) {
+ // This is a system emoji so it may have multiple names
+ for (const alias of emoji.aliases) {
+ if (alias.indexOf(partialName) !== -1) {
+ matched.push({name: alias, emoji});
+ break;
+ }
+ }
+ } else if (name.indexOf(partialName) !== -1) {
+ // This is a custom emoji so it only has one name
+ matched.push({name, emoji});
}
}
// sort the emoticons so that emoticons starting with the entered text come first
matched.sort((a, b) => {
- const aName = a.name || a.aliases[0];
- const bName = b.name || b.aliases[0];
+ const aName = a.name;
+ const bName = b.name;
const aPrefix = aName.startsWith(partialName);
const bPrefix = bName.startsWith(partialName);
@@ -95,7 +104,7 @@ export default class EmoticonProvider {
return 1;
});
- const terms = matched.map((emoticon) => ':' + (emoticon.name || emoticon.aliases[0]) + ':');
+ const terms = matched.map((item) => ':' + item.name + ':');
SuggestionStore.clearSuggestions(suggestionId);
if (terms.length > 0) {
diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx
index bff59ace8..70808ca26 100644
--- a/webapp/components/suggestion/search_user_provider.jsx
+++ b/webapp/components/suggestion/search_user_provider.jsx
@@ -41,7 +41,7 @@ class SearchUserSuggestion extends Suggestion {
<i className='fa fa fa-plus-square'/>
<img
className='profile-img rounded'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
<div className='mention--align'>
<span>
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index e9f7c3699..29b9b2d8b 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -153,6 +153,12 @@ export default class SuggestionBox extends React.Component {
window.requestAnimationFrame(() => {
Utils.setCaretPosition(textbox, prefix.length + term.length + 1);
});
+
+ for (const provider of this.props.providers) {
+ if (provider.handleCompleteWord) {
+ provider.handleCompleteWord(term, matchedPretext);
+ }
+ }
}
handleKeyDown(e) {
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index 301974b9a..0bc30a79f 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -35,7 +35,7 @@ class SwitchChannelSuggestion extends Suggestion {
<div className='pull-left'>
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
</div>
);
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 955a71ac5..0100cad64 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -8,60 +8,9 @@ import SettingItemMax from './setting_item_max.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
import {updateTeam} from 'actions/team_actions.jsx';
-const holders = defineMessages({
- dirDisabled: {
- id: 'general_tab.dirDisabled',
- defaultMessage: 'Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.'
- },
- required: {
- id: 'general_tab.required',
- defaultMessage: 'This field is required'
- },
- chooseName: {
- id: 'general_tab.chooseName',
- defaultMessage: 'Please choose a new name for your team'
- },
- includeDirTitle: {
- id: 'general_tab.includeDirTitle',
- defaultMessage: 'Include this team in the Team Directory'
- },
- yes: {
- id: 'general_tab.yes',
- defaultMessage: 'Yes'
- },
- no: {
- id: 'general_tab.no',
- defaultMessage: 'No'
- },
- dirOff: {
- id: 'general_tab.dirOff',
- defaultMessage: 'Team directory is turned off for this system.'
- },
- openInviteTitle: {
- id: 'general_tab.openInviteTitle',
- defaultMessage: 'Allow any user with an account on this server to join this team'
- },
- codeTitle: {
- id: 'general_tab.codeTitle',
- defaultMessage: 'Invite Code'
- },
- codeDesc: {
- id: 'general_tab.codeDesc',
- defaultMessage: "Click 'Edit' to regenerate Invite Code."
- },
- teamNameInfo: {
- id: 'general_tab.teamNameInfo',
- defaultMessage: 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.'
- },
- teamDescriptionInfo: {
- id: 'general_tab.teamDescriptionInfo',
- defaultMessage: 'Team description provides additional information to help users select the right team. Maximum of 50 characters.'
- }
-});
-
import React from 'react';
class GeneralTab extends React.Component {
@@ -156,13 +105,12 @@ class GeneralTab extends React.Component {
var state = {serverError: '', clientError: ''};
let valid = true;
- const {formatMessage} = this.props.intl;
const name = this.state.name.trim();
if (!name) {
- state.clientError = formatMessage(holders.required);
+ state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required');
valid = false;
} else if (name === this.props.team.display_name) {
- state.clientError = formatMessage(holders.chooseName);
+ state.clientError = Utils.localizeMessage('general_tab.chooseName', 'Please choose a new name for your team');
valid = false;
} else {
state.clientError = '';
@@ -197,7 +145,7 @@ class GeneralTab extends React.Component {
if (inviteId) {
state.clientError = '';
} else {
- state.clientError = this.props.intl.fromatMessage(holders.required);
+ state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required');
valid = false;
}
@@ -230,10 +178,9 @@ class GeneralTab extends React.Component {
var state = {serverError: '', clientError: ''};
let valid = true;
- const {formatMessage} = this.props.intl;
const description = this.state.description.trim();
if (description === this.props.team.description) {
- state.clientError = formatMessage(holders.chooseName);
+ state.clientError = Utils.localizeMessage('general_tab.chooseDescription', 'Please choose a new description for your team');
valid = false;
} else {
state.clientError = '';
@@ -324,8 +271,6 @@ class GeneralTab extends React.Component {
serverError = this.state.serverError;
}
- const {formatMessage} = this.props.intl;
-
let openInviteSection;
if (this.props.activeSection === 'open_invite') {
const inputs = [
@@ -372,7 +317,7 @@ class GeneralTab extends React.Component {
openInviteSection = (
<SettingItemMax
- title={formatMessage(holders.openInviteTitle)}
+ title={Utils.localizeMessage('general_tab.openInviteTitle', 'Allow any user with an account on this server to join this team')}
inputs={inputs}
submit={this.handleOpenInviteSubmit}
server_error={serverError}
@@ -382,14 +327,14 @@ class GeneralTab extends React.Component {
} else {
let describe = '';
if (this.state.allow_open_invite === true) {
- describe = formatMessage(holders.yes);
+ describe = Utils.localizeMessage('general_tab.yes', 'Yes');
} else {
- describe = formatMessage(holders.no);
+ describe = Utils.localizeMessage('general_tab.no', 'No');
}
openInviteSection = (
<SettingItemMin
- title={formatMessage(holders.openInviteTitle)}
+ title={Utils.localizeMessage('general_tab.openInviteTitle', 'Allow any user with an account on this server to join this team')}
describe={describe}
updateSection={this.onUpdateOpenInviteSection}
/>
@@ -427,9 +372,19 @@ class GeneralTab extends React.Component {
</div>
</div>
<div className='setting-list__hint'>
- <FormattedHTMLMessage
+ <FormattedMessage
id='general_tab.codeLongDesc'
- defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by {getTeamInviteLink} in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ values={{
+ getTeamInviteLink: (
+ <strong>
+ <FormattedMessage
+ id='general_tab.getTeamInviteLink'
+ defaultMessage='Get Team Invite Link'
+ />
+ </strong>
+ )
+ }}
/>
</div>
</div>
@@ -437,7 +392,7 @@ class GeneralTab extends React.Component {
inviteSection = (
<SettingItemMax
- title={formatMessage(holders.codeTitle)}
+ title={Utils.localizeMessage('general_tab.codeTitle', 'Invite Code')}
inputs={inputs}
submit={this.handleInviteIdSubmit}
server_error={serverError}
@@ -448,8 +403,8 @@ class GeneralTab extends React.Component {
} else {
inviteSection = (
<SettingItemMin
- title={formatMessage(holders.codeTitle)}
- describe={formatMessage(holders.codeDesc)}
+ title={Utils.localizeMessage('general_tab.codeTitle', 'Invite Code')}
+ describe={Utils.localizeMessage('general_tab.codeDesc', "Click 'Edit' to regenerate Invite Code.")}
updateSection={this.onUpdateInviteIdSection}
/>
);
@@ -488,11 +443,11 @@ class GeneralTab extends React.Component {
</div>
);
- const nameExtraInfo = <span>{formatMessage(holders.teamNameInfo)}</span>;
+ const nameExtraInfo = <span>{Utils.localizeMessage('general_tab.teamNameInfo', 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.')}</span>;
nameSection = (
<SettingItemMax
- title={formatMessage({id: 'general_tab.teamName'})}
+ title={Utils.localizeMessage('general_tab.teamName', 'Team Name')}
inputs={inputs}
submit={this.handleNameSubmit}
server_error={serverError}
@@ -506,7 +461,7 @@ class GeneralTab extends React.Component {
nameSection = (
<SettingItemMin
- title={formatMessage({id: 'general_tab.teamName'})}
+ title={Utils.localizeMessage('general_tab.teamName', 'Team Name')}
describe={describe}
updateSection={this.onUpdateNameSection}
/>
@@ -546,11 +501,11 @@ class GeneralTab extends React.Component {
</div>
);
- const descriptionExtraInfo = <span>{formatMessage(holders.teamDescriptionInfo)}</span>;
+ const descriptionExtraInfo = <span>{Utils.localizeMessage('general_tab.teamDescriptionInfo', 'Team description provides additional information to help users select the right team. Maximum of 50 characters.')}</span>;
descriptionSection = (
<SettingItemMax
- title={formatMessage({id: 'general_tab.teamDescription'})}
+ title={Utils.localizeMessage('general_tab.teamDescription', 'Team Description')}
inputs={inputs}
submit={this.handleDescriptionSubmit}
server_error={serverError}
@@ -574,7 +529,7 @@ class GeneralTab extends React.Component {
descriptionSection = (
<SettingItemMin
- title={formatMessage({id: 'general_tab.teamDescription'})}
+ title={Utils.localizeMessage('general_tab.teamDescription', 'Team Description')}
describe={describemsg}
updateSection={this.onUpdateDescriptionSection}
/>
@@ -633,10 +588,9 @@ class GeneralTab extends React.Component {
}
GeneralTab.propTypes = {
- intl: intlShape.isRequired,
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
activeSection: React.PropTypes.string.isRequired
};
-export default injectIntl(GeneralTab);
+export default GeneralTab;
diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx
index ff381a30b..3a13ccb66 100644
--- a/webapp/components/user_list_row.jsx
+++ b/webapp/components/user_list_row.jsx
@@ -64,7 +64,7 @@ export default function UserListRow({user, extraInfo, actions, actionProps, acti
className='more-modal__row'
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`}
+ src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.last_picture_update}`}
status={status}
width='32'
height='32'
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index d0267c0d8..d9bd5c378 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -56,7 +56,7 @@ export default class UserProfile extends React.Component {
let profileImg = '';
if (this.props.user) {
name = Utils.displayUsername(this.props.user.id);
- profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.update_at;
+ profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.last_picture_update;
}
if (this.props.overwriteName) {
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 70306d871..dc6f4ac0c 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -332,7 +332,7 @@ export default class AdvancedSettingsDisplay extends React.Component {
return (
<FormattedMessage
id='user.settings.advance.embed_preview'
- defaultMessage='Show experimental previews of link content, when available'
+ defaultMessage='For the first web link in a message, display a preview of website content below the message, if available'
/>
);
case 'WEBRTC_PREVIEW':
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index 9ffc4f721..f51128b6f 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -191,7 +191,7 @@ export default class UserSettingsDisplay extends React.Component {
<br/>
<FormattedMessage
id='user.settings.display.collapseDesc'
- defaultMessage='Expand links to show a preview of content, when available.'
+ defaultMessage='Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.'
/>
</div>
</div>
@@ -202,7 +202,7 @@ export default class UserSettingsDisplay extends React.Component {
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
- defaultMessage='Link previews'
+ defaultMessage='Default appearance of image link previews'
/>
}
inputs={inputs}
@@ -218,14 +218,14 @@ export default class UserSettingsDisplay extends React.Component {
describe = (
<FormattedMessage
id='user.settings.display.collapseOn'
- defaultMessage='On'
+ defaultMessage='Expanded'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.display.collapseOff'
- defaultMessage='Off'
+ defaultMessage='Collapsed'
/>
);
}
@@ -239,7 +239,7 @@ export default class UserSettingsDisplay extends React.Component {
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
- defaultMessage='Link previews'
+ defaultMessage='Default appearance of image link previews'
/>
}
describe={describe}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index 06fe31a9e..d9551dccc 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -15,7 +15,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
-import {updateUser} from 'actions/user_actions.jsx';
+import {updateUser, uploadProfileImage} from 'actions/user_actions.jsx';
const holders = defineMessages({
usernameReserved: {
@@ -241,11 +241,11 @@ class UserSettingsGeneralTab extends React.Component {
this.setState({loadingPicture: true});
- Client.uploadProfileImage(picture,
+ uploadProfileImage(
+ picture,
() => {
this.updateSection('');
this.submitActive = false;
- AsyncClient.getMe();
},
(err) => {
var state = this.setupInitialState(this.props);
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index 2ee33c092..672f8d6b7 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -8,10 +8,9 @@ import DesktopNotificationSettings from './desktop_notification_settings.jsx';
import UserStore from 'stores/user_store.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+import {updateUserNotifyProps} from 'actions/user_actions.jsx';
import EmailNotificationSetting from './email_notification_setting.jsx';
import {FormattedMessage} from 'react-intl';
@@ -143,10 +142,10 @@ export default class NotificationsTab extends React.Component {
data.first_name = this.state.firstNameKey.toString();
data.channel = this.state.channelKey.toString();
- Client.updateUserNotifyProps(data,
+ updateUserNotifyProps(
+ data,
() => {
this.props.updateSection('');
- AsyncClient.getMe();
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 3484b8183..210e455b7 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -9,11 +9,12 @@ import ToggleModalButton from '../toggle_modal_button.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx';
+
import $ from 'jquery';
import React from 'react';
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
@@ -27,7 +28,7 @@ export default class SecurityTab extends React.Component {
this.submitPassword = this.submitPassword.bind(this);
this.setupMfa = this.setupMfa.bind(this);
- this.deactivateMfa = this.deactivateMfa.bind(this);
+ this.removeMfa = this.removeMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
@@ -53,7 +54,7 @@ export default class SecurityTab extends React.Component {
componentDidMount() {
if (global.mm_config.EnableOAuthServiceProvider === 'true') {
- Client.getAuthorizedApps(
+ getAuthorizedApps(
(authorizedApps) => {
this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state
},
@@ -91,7 +92,7 @@ export default class SecurityTab extends React.Component {
return;
}
- Client.updatePassword(
+ updatePassword(
user.id,
currentPassword,
newPassword,
@@ -118,10 +119,8 @@ export default class SecurityTab extends React.Component {
browserHistory.push('/mfa/setup');
}
- deactivateMfa() {
- Client.updateMfa(
- '',
- false,
+ removeMfa() {
+ deactivateMfa(
() => {
if (global.window.mm_license.MFA === 'true' &&
global.window.mm_config.EnableMultifactorAuthentication === 'true' &&
@@ -131,7 +130,6 @@ export default class SecurityTab extends React.Component {
}
this.props.updateSection('');
- AsyncClient.getMe();
this.setState(this.getDefaultState());
},
(err) => {
@@ -161,7 +159,7 @@ export default class SecurityTab extends React.Component {
deauthorizeApp(e) {
e.preventDefault();
const appId = e.currentTarget.getAttribute('data-app');
- Client.deauthorizeOAuthApp(
+ deauthorizeOAuthApp(
appId,
() => {
const authorizedApps = this.state.authorizedApps.filter((app) => {
@@ -221,7 +219,7 @@ export default class SecurityTab extends React.Component {
<a
className='btn btn-primary'
href='#'
- onClick={this.deactivateMfa}
+ onClick={this.removeMfa}
>
{mfaButtonText}
</a>
@@ -425,6 +423,34 @@ export default class SecurityTab extends React.Component {
</div>
</div>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordGoogleCantUpdate'
+ defaultMessage='Login occurs through Google Apps. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordOffice365CantUpdate'
+ defaultMessage='Login occurs through Office 365. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
}
updateSectionStatus = function resetSection(e) {
@@ -502,6 +528,20 @@ export default class SecurityTab extends React.Component {
defaultMessage='Login done through SAML'
/>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginGoogle'
+ defaultMessage='Login done through Google Apps'
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginOffice365'
+ defaultMessage='Login done through Office 365'
+ />
+ );
}
updateSectionStatus = function updateSection() {
diff --git a/webapp/components/webrtc/components/webrtc_notification.jsx b/webapp/components/webrtc/components/webrtc_notification.jsx
index 5456d6cb8..f69e731f8 100644
--- a/webapp/components/webrtc/components/webrtc_notification.jsx
+++ b/webapp/components/webrtc/components/webrtc_notification.jsx
@@ -197,7 +197,7 @@ export default class WebrtcNotification extends React.Component {
const user = this.state.userCalling;
if (user) {
const username = Utils.displayUsername(user.id);
- const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.update_at || new Date().getTime());
+ const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.last_picture_update || new Date().getTime());
const profileImg = (
<img
className='user-popover__image'
diff --git a/webapp/components/webrtc/webrtc_controller.jsx b/webapp/components/webrtc/webrtc_controller.jsx
index 94e5b3475..b8d3d4db6 100644
--- a/webapp/components/webrtc/webrtc_controller.jsx
+++ b/webapp/components/webrtc/webrtc_controller.jsx
@@ -81,14 +81,14 @@ export default class WebrtcController extends React.Component {
const currentUser = UserStore.getCurrentUser();
const remoteUser = UserStore.getProfile(props.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.state = {
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight(),
channelId: ChannelStore.getCurrentId(),
currentUser,
- currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.update_at,
+ currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.last_picture_update,
remoteUserImage,
localMediaLoaded: false,
isPaused: false,
@@ -130,7 +130,7 @@ export default class WebrtcController extends React.Component {
(nextProps.userId !== this.props.userId) ||
(nextProps.isCaller !== this.props.isCaller)) {
const remoteUser = UserStore.getProfile(nextProps.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.setState({
error: null,
remoteUserImage
@@ -644,7 +644,7 @@ export default class WebrtcController extends React.Component {
}
onConnectCall() {
- Client.webrtcToken(
+ WebrtcActions.webrtcToken(
(info) => {
const connectingMsg = (
<FormattedMessage