summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/access_history_modal.jsx3
-rw-r--r--web/react/components/admin_console/admin_controller.jsx2
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/audits.jsx5
-rw-r--r--web/react/components/admin_console/license_settings.jsx18
-rw-r--r--web/react/components/audit_table.jsx655
-rw-r--r--web/react/components/create_comment.jsx15
-rw-r--r--web/react/components/create_post.jsx55
-rw-r--r--web/react/components/delete_post_modal.jsx2
-rw-r--r--web/react/components/file_upload.jsx2
-rw-r--r--web/react/components/post_body.jsx30
-rw-r--r--web/react/components/post_info.jsx25
-rw-r--r--web/react/components/textbox.jsx12
-rw-r--r--web/react/dispatcher/event_helpers.jsx25
-rw-r--r--web/react/stores/post_store.jsx92
-rw-r--r--web/react/stores/socket_store.jsx27
-rw-r--r--web/react/utils/constants.jsx5
-rw-r--r--web/react/utils/utils.jsx4
-rw-r--r--web/sass-files/sass/partials/_post.scss9
-rw-r--r--web/static/i18n/en.json109
-rw-r--r--web/static/i18n/es.json6
21 files changed, 571 insertions, 534 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 98b1d7cc1..af4d3fb0f 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -73,7 +73,8 @@ class AccessHistoryModal extends React.Component {
content = (
<AuditTable
audits={this.state.audits}
- moreInfo={this.state.moreInfo}
+ showIp={true}
+ showSession={true}
/>
);
}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 360ae3ef3..695e2083a 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -160,7 +160,7 @@ export default class AdminController extends React.Component {
} else if (this.state.selected === 'ldap_settings') {
tab = <LdapSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'license') {
- tab = <LicenseSettingsTab />;
+ tab = <LicenseSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_users') {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 642bfe9d7..eadd8d412 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -225,7 +225,7 @@ export default class AdminSidebar extends React.Component {
>
<FormattedMessage
id='admin.sidebar.audits'
- defaultMessage='Audits'
+ defaultMessage='Compliance and Auditing'
/>
</a>
</li>
@@ -454,6 +454,7 @@ export default class AdminSidebar extends React.Component {
</ul>
<ul className='nav nav__sub-menu padded'>
{licenseSettings}
+ {audits}
<li>
<a
href='#'
@@ -466,7 +467,6 @@ export default class AdminSidebar extends React.Component {
/>
</a>
</li>
- {audits}
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx
index 866539b3d..173e63b45 100644
--- a/web/react/components/admin_console/audits.jsx
+++ b/web/react/components/admin_console/audits.jsx
@@ -60,8 +60,9 @@ export default class Audits extends React.Component {
<div style={{margin: '10px'}}>
<AuditTable
audits={this.state.audits}
- oneLine={true}
showUserId={true}
+ showIp={true}
+ showSession={true}
/>
</div>
);
@@ -72,7 +73,7 @@ export default class Audits extends React.Component {
<h3>
<FormattedMessage
id='admin.audits.title'
- defaultMessage='Server Audits'
+ defaultMessage='User Activity'
/>
</h3>
<button
diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx
index 539acd869..3332f37ef 100644
--- a/web/react/components/admin_console/license_settings.jsx
+++ b/web/react/components/admin_console/license_settings.jsx
@@ -109,7 +109,17 @@ class LicenseSettings extends React.Component {
);
licenseType = (
<FormattedHTMLMessage
- id='admin.license.entrepriseType'
+ id='admin.license.enterpriseType'
+ values={{
+ terms: global.window.mm_config.TermsOfServiceLink,
+ name: global.window.mm_license.Name,
+ company: global.window.mm_license.Company,
+ users: global.window.mm_license.Users,
+ issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
+ start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
+ expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
+ ldap: global.window.mm_license.LDAP
+ }}
defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a>
from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
<p>Your subscription details are as follows:</p>
@@ -126,6 +136,7 @@ class LicenseSettings extends React.Component {
licenseKey = (
<div className='col-sm-8'>
<button
+ disabled={this.props.config.LdapSettings.Enable}
className='btn btn-danger'
onClick={this.handleRemove}
id='remove-button'
@@ -256,7 +267,8 @@ class LicenseSettings extends React.Component {
}
LicenseSettings.propTypes = {
- intl: intlShape.isRequired
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
};
-export default injectIntl(LicenseSettings); \ No newline at end of file
+export default injectIntl(LicenseSettings);
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
index cdca7e8d6..49892ff98 100644
--- a/web/react/components/audit_table.jsx
+++ b/web/react/components/audit_table.jsx
@@ -183,389 +183,388 @@ const holders = defineMessages({
loginFailure: {
id: 'audit_table.loginFailure',
defaultMessage: ' (Login failure)'
- },
- userId: {
- id: 'audit_table.userId',
- defaultMessage: 'User ID'
}
});
class AuditTable extends React.Component {
constructor(props) {
super(props);
-
- this.handleMoreInfo = this.handleMoreInfo.bind(this);
- this.formatAuditInfo = this.formatAuditInfo.bind(this);
- this.handleRevokedSession = this.handleRevokedSession.bind(this);
-
- this.state = {moreInfo: []};
- }
- handleMoreInfo(index) {
- var newMoreInfo = this.state.moreInfo;
- newMoreInfo[index] = true;
- this.setState({moreInfo: newMoreInfo});
}
- handleRevokedSession(sessionId) {
- return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
- }
- formatAuditInfo(currentAudit) {
- const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
+ render() {
+ var accessList = [];
const {formatMessage} = this.props.intl;
- let currentAuditDesc = '';
-
- if (currentActionURL.indexOf('/channels') === 0) {
- const channelInfo = currentAudit.extra_info.split(' ');
- const channelNameField = channelInfo[0].split('=');
-
- let channelURL = '';
- let channelObj;
- let channelName = '';
- if (channelNameField.indexOf('name') >= 0) {
- channelURL = channelNameField[channelNameField.indexOf('name') + 1];
- channelObj = ChannelStore.getByName(channelURL);
- if (channelObj) {
- channelName = channelObj.display_name;
- } else {
- channelName = channelURL;
- }
- }
+ for (var i = 0; i < this.props.audits.length; i++) {
+ const audit = this.props.audits[i];
+ const auditInfo = formatAuditInfo(audit, formatMessage);
- switch (currentActionURL) {
- case '/channels/create':
- currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
- break;
- case '/channels/create_direct':
- currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
- break;
- case '/channels/update':
- currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
- break;
- case '/channels/update_desc': // support the old path
- case '/channels/update_header':
- currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
- break;
- default: {
- let userIdField = [];
- let userId = '';
- let username = '';
-
- if (channelInfo[1]) {
- userIdField = channelInfo[1].split('=');
-
- if (userIdField.indexOf('user_id') >= 0) {
- userId = userIdField[userIdField.indexOf('user_id') + 1];
- username = UserStore.getProfile(userId).username;
- }
- }
+ let uContent;
+ if (this.props.showUserId) {
+ uContent = <td>{auditInfo.userId}</td>;
+ }
- if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
- } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
- } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
- }
+ let iContent;
+ if (this.props.showIp) {
+ iContent = <td>{auditInfo.ip}</td>;
+ }
- break;
+ let sContent;
+ if (this.props.showSession) {
+ sContent = <td>{auditInfo.sessionId}</td>;
}
+
+ let descStyle = {};
+ if (auditInfo.desc.toLowerCase().indexOf('fail') !== -1) {
+ descStyle.color = 'red';
}
- } else if (currentActionURL.indexOf('/oauth') === 0) {
- const oauthInfo = currentAudit.extra_info.split(' ');
- switch (currentActionURL) {
- case '/oauth/register': {
- const clientIdField = oauthInfo[0].split('=');
+ accessList[i] = (
+ <tr key={audit.id}>
+ <td>{auditInfo.timestamp}</td>
+ {uContent}
+ <td style={descStyle}>{auditInfo.desc}</td>
+ {iContent}
+ {sContent}
+ </tr>
+ );
+ }
- if (clientIdField[0] === 'client_id') {
- currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
- }
+ let userIdContent;
+ if (this.props.showUserId) {
+ userIdContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.userId'
+ defaultMessage='User ID'
+ />
+ </th>
+ );
+ }
- break;
- }
- case '/oauth/allow':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
- } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
- currentAuditDesc = formatMessage(holders.failedOAuthAccess);
- }
+ let ipContent;
+ if (this.props.showIp) {
+ ipContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.ip'
+ defaultMessage='IP Address'
+ />
+ </th>
+ );
+ }
- break;
- case '/oauth/access_token':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthToken);
- } else {
- const oauthTokenFailure = oauthInfo[0].split('-');
-
- if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
- currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
- }
- }
+ let sessionContent;
+ if (this.props.showSession) {
+ sessionContent = (
+ <th>
+ <FormattedMessage
+ id='audit_table.session'
+ defaultMessage='Session ID'
+ />
+ </th>
+ );
+ }
- break;
- default:
- break;
- }
- } else if (currentActionURL.indexOf('/users') === 0) {
- const userInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/users/login':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedLogin);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullLogin);
- } else if (userInfo[0]) {
- currentAuditDesc = formatMessage(holders.failedLogin);
- }
+ return (
+ <table className='table'>
+ <thead>
+ <tr>
+ <th>
+ <FormattedMessage
+ id='audit_table.timestamp'
+ defaultMessage='Timestamp'
+ />
+ </th>
+ {userIdContent}
+ <th>
+ <FormattedMessage
+ id='audit_table.action'
+ defaultMessage='Action'
+ />
+ </th>
+ {ipContent}
+ {sessionContent}
+ </tr>
+ </thead>
+ <tbody>
+ {accessList}
+ </tbody>
+ </table>
+ );
+ }
+}
- break;
- case '/users/revoke_session':
- currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
- break;
- case '/users/newimage':
- currentAuditDesc = formatMessage(holders.updatePicture);
- break;
- case '/users/update':
- currentAuditDesc = formatMessage(holders.updateGeneral);
- break;
- case '/users/newpassword':
- if (userInfo[0] === 'attempted') {
- currentAuditDesc = formatMessage(holders.attemptedPassword);
- } else if (userInfo[0] === 'completed') {
- currentAuditDesc = formatMessage(holders.successfullPassword);
- } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
- currentAuditDesc = formatMessage(holders.failedPassword);
- }
+AuditTable.propTypes = {
+ intl: intlShape.isRequired,
+ audits: React.PropTypes.array.isRequired,
+ showUserId: React.PropTypes.bool,
+ showIp: React.PropTypes.bool,
+ showSession: React.PropTypes.bool
+};
- break;
- case '/users/update_roles': {
- const userRoles = userInfo[0].split('=')[1];
+export default injectIntl(AuditTable);
- currentAuditDesc = formatMessage(holders.updatedRol);
- if (userRoles.trim()) {
- currentAuditDesc += userRoles;
- } else {
- currentAuditDesc += formatMessage(holders.member);
+export function formatAuditInfo(audit, formatMessage) {
+ const actionURL = audit.action.replace(/\/api\/v[1-9]/, '');
+ let auditDesc = '';
+
+ if (actionURL.indexOf('/channels') === 0) {
+ const channelInfo = audit.extra_info.split(' ');
+ const channelNameField = channelInfo[0].split('=');
+
+ let channelURL = '';
+ let channelObj;
+ let channelName = '';
+ if (channelNameField.indexOf('name') >= 0) {
+ channelURL = channelNameField[channelNameField.indexOf('name') + 1];
+ channelObj = ChannelStore.getByName(channelURL);
+ if (channelObj) {
+ channelName = channelObj.display_name;
+ } else {
+ channelName = channelURL;
+ }
+ }
+
+ switch (actionURL) {
+ case '/channels/create':
+ auditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
+ break;
+ case '/channels/create_direct':
+ auditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
+ break;
+ case '/channels/update':
+ auditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
+ break;
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ auditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
+ break;
+ default: {
+ let userIdField = [];
+ let userId = '';
+ let username = '';
+
+ if (channelInfo[1]) {
+ userIdField = channelInfo[1].split('=');
+
+ if (userIdField.indexOf('user_id') >= 0) {
+ userId = userIdField[userIdField.indexOf('user_id') + 1];
+ username = UserStore.getProfile(userId).username;
}
+ }
- break;
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(actionURL)) {
+ auditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(actionURL)) {
+ auditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) {
+ auditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
}
- case '/users/update_active': {
- const updateType = userInfo[0].split('=')[0];
- const updateField = userInfo[0].split('=')[1];
-
- /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
- if (updateType === 'active') {
- if (updateField === 'true') {
- currentAuditDesc = formatMessage(holders.accountActive);
- } else if (updateField === 'false') {
- currentAuditDesc = formatMessage(holders.accountInactive);
- }
- const actingUserInfo = userInfo[1].split('=');
- if (actingUserInfo[0] === 'session_user') {
- const actingUser = UserStore.getProfile(actingUserInfo[1]);
- const currentUser = UserStore.getCurrentUser();
- if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
- currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
- } else if (currentUser && actingUser) {
- currentAuditDesc += formatMessage(holders.byAdmin);
- }
- }
- } else if (updateType === 'session_id') {
- currentAuditDesc = this.handleRevokedSession(updateField);
- }
+ break;
+ }
+ }
+ } else if (actionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/oauth/register': {
+ const clientIdField = oauthInfo[0].split('=');
- break;
+ if (clientIdField[0] === 'client_id') {
+ auditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
}
- case '/users/send_password_reset':
- currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
- break;
- case '/users/reset_password':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedReset);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullReset);
- }
- break;
- case '/users/update_notify':
- currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
- break;
- default:
- break;
+ break;
+ }
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
+ } else if (oauthInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullOAuthAccess);
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ auditDesc = formatMessage(holders.failedOAuthAccess);
}
- } else if (currentActionURL.indexOf('/hooks') === 0) {
- const webhookInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/hooks/incoming/create':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
- } else if (webhookInfo[0] === 'fail - bad channel permissions') {
- currentAuditDesc = formatMessage(holders.failedWebhookCreate);
- }
- break;
- case '/hooks/incoming/delete':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
- } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
- currentAuditDesc = formatMessage(holders.failedWebhookDelete);
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedOAuthToken);
+ } else if (oauthInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullOAuthToken);
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ auditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
}
+ }
- break;
- default:
- break;
+ break;
+ default:
+ break;
+ }
+ } else if (actionURL.indexOf('/users') === 0) {
+ const userInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedLogin);
+ } else if (userInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullLogin);
+ } else if (userInfo[0]) {
+ auditDesc = formatMessage(holders.failedLogin);
}
- } else {
- switch (currentActionURL) {
- case '/logout':
- currentAuditDesc = formatMessage(holders.logout);
- break;
- case '/verify_email':
- currentAuditDesc = formatMessage(holders.verified);
- break;
- default:
- break;
+
+ break;
+ case '/users/revoke_session':
+ auditDesc = formatMessage(holders.sessionRevoked, {sessionId: userInfo[0].split('=')[1]});
+ break;
+ case '/users/newimage':
+ auditDesc = formatMessage(holders.updatePicture);
+ break;
+ case '/users/update':
+ auditDesc = formatMessage(holders.updateGeneral);
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ auditDesc = formatMessage(holders.attemptedPassword);
+ } else if (userInfo[0] === 'completed') {
+ auditDesc = formatMessage(holders.successfullPassword);
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ auditDesc = formatMessage(holders.failedPassword);
}
- }
- /* If all else fails... */
- if (!currentAuditDesc) {
- /* Currently not called anywhere */
- if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
- currentAuditDesc = formatMessage(holders.revokedAll);
+ break;
+ case '/users/update_roles': {
+ const userRoles = userInfo[0].split('=')[1];
+
+ auditDesc = formatMessage(holders.updatedRol);
+ if (userRoles.trim()) {
+ auditDesc += userRoles;
} else {
- let currentActionDesc = '';
- if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
- currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
- currentActionDesc = Utils.toTitleCase(currentActionDesc);
- }
+ auditDesc += formatMessage(holders.member);
+ }
- let currentExtraInfoDesc = '';
- if (currentAudit.extra_info) {
- currentExtraInfoDesc = currentAudit.extra_info;
+ break;
+ }
+ case '/users/update_active': {
+ const updateType = userInfo[0].split('=')[0];
+ const updateField = userInfo[0].split('=')[1];
+
+ /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
+ if (updateType === 'active') {
+ if (updateField === 'true') {
+ auditDesc = formatMessage(holders.accountActive);
+ } else if (updateField === 'false') {
+ auditDesc = formatMessage(holders.accountInactive);
+ }
- if (currentExtraInfoDesc.indexOf('=') !== -1) {
- currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const user = UserStore.getCurrentUser();
+ if (user && actingUser && (Utils.isAdmin(user.roles) || Utils.isSystemAdmin(user.roles))) {
+ auditDesc += formatMessage(holders.by, {username: actingUser.username});
+ } else if (user && actingUser) {
+ auditDesc += formatMessage(holders.byAdmin);
}
}
- currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ } else if (updateType === 'session_id') {
+ auditDesc = formatMessage(holders.sessionRevoked, {sessionId: updateField});
}
- }
- const currentDate = new Date(currentAudit.create_at);
- let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+ break;
+ }
+ case '/users/send_password_reset':
+ auditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedReset);
+ } else if (userInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullReset);
+ }
- if (this.props.showUserId) {
- currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
+ break;
+ case '/users/update_notify':
+ auditDesc = formatMessage(holders.updateGlobalNotifications);
+ break;
+ default:
+ break;
}
+ } else if (actionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = audit.extra_info.split(' ');
+
+ switch (actionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedWebhookCreate);
+ } else if (webhookInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.succcessfullWebhookCreate);
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ auditDesc = formatMessage(holders.failedWebhookCreate);
+ }
- currentAuditInfo += ' | ' + currentAuditDesc;
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ auditDesc = formatMessage(holders.attemptedWebhookDelete);
+ } else if (webhookInfo[0] === 'success') {
+ auditDesc = formatMessage(holders.successfullWebhookDelete);
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ auditDesc = formatMessage(holders.failedWebhookDelete);
+ }
- return currentAuditInfo;
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (actionURL) {
+ case '/logout':
+ auditDesc = formatMessage(holders.logout);
+ break;
+ case '/verify_email':
+ auditDesc = formatMessage(holders.verified);
+ break;
+ default:
+ break;
+ }
}
- render() {
- var accessList = [];
- const {formatMessage} = this.props.intl;
- for (var i = 0; i < this.props.audits.length; i++) {
- const currentAudit = this.props.audits[i];
- const currentAuditInfo = this.formatAuditInfo(currentAudit);
-
- let moreInfo;
- if (!this.props.oneLine) {
- moreInfo = (
- <a
- href='#'
- className='theme'
- onClick={this.handleMoreInfo.bind(this, i)}
- >
- <FormattedMessage
- id='audit_table.moreInfo'
- defaultMessage='More info'
- />
- </a>
- );
+ /* If all else fails... */
+ if (!auditDesc) {
+ /* Currently not called anywhere */
+ if (audit.extra_info.indexOf('revoked_all=') >= 0) {
+ auditDesc = formatMessage(holders.revokedAll);
+ } else {
+ let actionDesc = '';
+ if (actionURL && actionURL.lastIndexOf('/') !== -1) {
+ actionDesc = actionURL.substring(actionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ actionDesc = Utils.toTitleCase(actionDesc);
}
- if (this.state.moreInfo[i]) {
- if (!currentAudit.session_id) {
- currentAudit.session_id = 'N/A';
+ let extraInfoDesc = '';
+ if (audit.extra_info) {
+ extraInfoDesc = audit.extra_info;
- if (currentAudit.action.search('/users/login') >= 0) {
- if (currentAudit.extra_info === 'attempt') {
- currentAudit.session_id += formatMessage(holders.loginAttempt);
- } else {
- currentAudit.session_id += formatMessage(holders.loginFailure);
- }
- }
+ if (extraInfoDesc.indexOf('=') !== -1) {
+ extraInfoDesc = extraInfoDesc.substring(extraInfoDesc.indexOf('=') + 1);
}
-
- moreInfo = (
- <div>
- <div>
- <FormattedMessage
- id='audit_table.ip'
- defaultMessage='IP: {ip}'
- values={{
- ip: currentAudit.ip_address
- }}
- />
- </div>
- <div>
- <FormattedMessage
- id='audit_table.session'
- defaultMessage='Session ID: {id}'
- values={{
- id: currentAudit.session_id
- }}
- />
- </div>
- </div>
- );
}
-
- var divider = null;
- if (i < this.props.audits.length - 1) {
- divider = (<div className='divider-light'></div>);
- }
-
- accessList[i] = (
- <div
- key={'accessHistoryEntryKey' + i}
- className='access-history__table'
- >
- <div className='access__report'>
- <div className='report__time'>{currentAuditInfo}</div>
- <div className='report__info'>
- {moreInfo}
- </div>
- {divider}
- </div>
- </div>
- );
+ auditDesc = actionDesc + ' ' + extraInfoDesc;
}
-
- return <form role='form'>{accessList}</form>;
}
-}
-AuditTable.propTypes = {
- intl: intlShape.isRequired,
- audits: React.PropTypes.array.isRequired,
- oneLine: React.PropTypes.bool,
- showUserId: React.PropTypes.bool
-};
+ const date = new Date(audit.create_at);
+ let auditInfo = {};
+ auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+ auditInfo.userId = audit.user_id;
+ auditInfo.desc = auditDesc;
+ auditInfo.ip = audit.ip_address;
+ auditInfo.sessionId = audit.session_id;
-export default injectIntl(AuditTable);
+ return auditInfo;
+}
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 8c49315e7..9e7c67515 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -51,6 +51,7 @@ class CreateComment extends React.Component {
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -74,6 +75,8 @@ class CreateComment extends React.Component {
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
+
+ this.refs.textbox.focus();
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
@@ -94,6 +97,10 @@ class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
}
}
+
+ if (prevProps.rootId !== this.props.rootId) {
+ this.refs.textbox.focus();
+ }
}
handleSubmit(e) {
e.preventDefault();
@@ -218,6 +225,9 @@ class CreateComment extends React.Component {
});
}
}
+ handleUploadClick() {
+ this.refs.textbox.focus();
+ }
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -225,6 +235,10 @@ class CreateComment extends React.Component {
PostStore.storeCommentDraft(this.props.rootId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
+
+ // this is a bit redundant with the code that sets focus when the file input is clicked,
+ // but this also resets the focus after a drag and drop
+ this.refs.textbox.focus();
}
handleFileUploadComplete(filenames, clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -365,6 +379,7 @@ class CreateComment extends React.Component {
<FileUpload
ref='fileUpload'
getFileCount={this.getFileCount}
+ onClick={this.handleUploadClick}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 20892898e..6ea80cd13 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -40,10 +40,6 @@ const holders = defineMessages({
write: {
id: 'create_post.write',
defaultMessage: 'Write a message...'
- },
- deleteMsg: {
- id: 'create_post.deleteMsg',
- defaultMessage: '(message deleted)'
}
});
@@ -57,7 +53,7 @@ class CreatePost extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.resizePostHolder = this.resizePostHolder.bind(this);
+ this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -66,11 +62,9 @@ class CreatePost extends React.Component {
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleResize = this.handleResize.bind(this);
this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
- PostStore.deleteMessage(this.props.intl.formatMessage(holders.deleteMsg));
const draft = this.getCurrentDraft();
@@ -81,34 +75,10 @@ class CreatePost extends React.Component {
previews: draft.previews,
submitting: false,
initialText: draft.messageText,
- windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight(),
ctrlSend: false,
showTutorialTip: false
};
}
- handleResize() {
- this.setState({
- windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
- });
- }
- componentDidUpdate(prevProps, prevState) {
- if (prevState.previews.length !== this.state.previews.length) {
- this.resizePostHolder();
- return;
- }
-
- if (prevState.uploadsInProgress !== this.state.uploadsInProgress) {
- this.resizePostHolder();
- return;
- }
-
- if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeight) {
- this.resizePostHolder();
- return;
- }
- }
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
@@ -245,10 +215,8 @@ class CreatePost extends React.Component {
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
- resizePostHolder() {
- if (this.state.windowWidth > 960) {
- $('#post_textbox').focus();
- }
+ handleUploadClick() {
+ this.refs.textbox.focus();
}
handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -257,6 +225,10 @@ class CreatePost extends React.Component {
PostStore.storeDraft(channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
+
+ // this is a bit redundant with the code that sets focus when the file input is clicked,
+ // but this also resets the focus after a drag and drop
+ this.refs.textbox.focus();
}
handleFileUploadComplete(filenames, clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
@@ -333,13 +305,16 @@ class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
- this.resizePostHolder();
- window.addEventListener('resize', this.handleResize);
+ this.refs.textbox.focus();
+ }
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.channelId !== this.state.channelId) {
+ this.refs.textbox.focus();
+ }
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
- window.removeEventListener('resize', this.handleResize);
}
onChange() {
const channelId = ChannelStore.getCurrentId();
@@ -462,7 +437,6 @@ class CreatePost extends React.Component {
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
onKeyDown={this.handleKeyDown}
- onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage={this.props.intl.formatMessage(holders.write)}
channelId={this.state.channelId}
@@ -472,6 +446,7 @@ class CreatePost extends React.Component {
<FileUpload
ref='fileUpload'
getFileCount={this.getFileCount}
+ onClick={this.handleUploadClick}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
@@ -506,4 +481,4 @@ CreatePost.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(CreatePost); \ No newline at end of file
+export default injectIntl(CreatePost);
diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 34fd724f5..9d7dcb3e5 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -88,7 +88,7 @@ export default class DeletePostModal extends React.Component {
}
}
- PostStore.removePost(this.state.post.id, this.state.post.channel_id);
+ PostStore.deletePost(this.state.post);
AsyncClient.getPosts(this.state.post.channel_id);
},
(err) => {
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 746289653..f5c32c825 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -310,6 +310,7 @@ class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
+ onClick={this.props.onClick}
multiple={multiple}
accept={accept}
/>
@@ -322,6 +323,7 @@ FileUpload.propTypes = {
intl: intlShape.isRequired,
onUploadError: React.PropTypes.func,
getFileCount: React.PropTypes.func,
+ onClick: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
onTextDrop: React.PropTypes.func,
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 16f8528b2..d71ac6ec7 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -44,7 +44,6 @@ class PostBody extends React.Component {
this.state = {
links: linkData.links,
- message: linkData.text,
post: this.props.post,
hasUserProfiles: profiles && Object.keys(profiles).length > 1
};
@@ -106,7 +105,9 @@ class PostBody extends React.Component {
if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) {
this.embed = this.createEmbed(linkData.links[0]);
}
- this.setState({links: linkData.links, message: linkData.text});
+ this.setState({
+ links: linkData.links
+ });
}
createEmbed(link) {
@@ -310,6 +311,23 @@ class PostBody extends React.Component {
);
}
+ let message;
+ if (this.props.post.state === Constants.POST_DELETED) {
+ message = (
+ <FormattedMessage
+ id='post_body.deleted'
+ defaultMessage='(message deleted)'
+ />
+ );
+ } else {
+ message = (
+ <span
+ onClick={TextFormatting.handleClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}}
+ />
+ );
+ }
+
return (
<div>
{comment}
@@ -320,11 +338,7 @@ class PostBody extends React.Component {
className={postClass}
>
{loading}
- <span
- ref='message_span'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
- />
+ {message}
</div>
<PostBodyAdditionalContent
post={this.state.post}
@@ -346,4 +360,4 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired
};
-export default injectIntl(PostBody); \ No newline at end of file
+export default injectIntl(PostBody);
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index ddb393520..b1bc8ca14 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -23,13 +23,14 @@ export default class PostInfo extends React.Component {
};
this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this);
+ this.removePost = this.removePost.bind(this);
}
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
- if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
+ if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
}
@@ -166,6 +167,25 @@ export default class PostInfo extends React.Component {
this.setState({copiedLink: false});
}
}
+ removePost() {
+ EventHelpers.emitRemovePost(this.props.post);
+ }
+ createRemovePostButton(post) {
+ if (!Utils.isPostEphemeral(post)) {
+ return null;
+ }
+
+ return (
+ <a
+ href='#'
+ className='post__remove theme'
+ type='button'
+ onClick={this.removePost}
+ >
+ {'×'}
+ </a>
+ );
+ }
render() {
var post = this.props.post;
var comments = '';
@@ -178,7 +198,7 @@ export default class PostInfo extends React.Component {
commentCountText = '';
}
- if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
+ if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) {
comments = (
<a
href='#'
@@ -264,6 +284,7 @@ export default class PostInfo extends React.Component {
>
{permalinkOverlay}
</Overlay>
+ {this.createRemovePostButton(post)}
</li>
</ul>
);
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 00e5ace98..ec299087d 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -20,6 +20,7 @@ export default class Textbox extends React.Component {
constructor(props) {
super(props);
+ this.focus = this.focus.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
@@ -81,6 +82,10 @@ export default class Textbox extends React.Component {
}
}
+ focus() {
+ this.refs.message.getTextbox().focus();
+ }
+
resize() {
const textbox = this.refs.message.getTextbox();
const $textbox = $(textbox);
@@ -90,8 +95,6 @@ export default class Textbox extends React.Component {
const borders = parseInt($textbox.css('border-bottom-width'), 10) + parseInt($textbox.css('border-top-width'), 10);
const maxHeight = parseInt($textbox.css('max-height'), 10) - borders;
- const prevHeight = $textbox.height();
-
// set the height to auto and remove the scrollbar so we can get the actual size of the contents
$textbox.css('height', 'auto').css('overflow-y', 'hidden');
@@ -116,10 +119,6 @@ export default class Textbox extends React.Component {
if (this.state.preview) {
$(ReactDOM.findDOMNode(this.refs.preview)).height(height + borders);
}
-
- if (height !== prevHeight && this.props.onHeightChange) {
- this.props.onHeightChange();
- }
}
showPreview(e) {
@@ -211,7 +210,6 @@ Textbox.propTypes = {
messageText: React.PropTypes.string.isRequired,
onUserInput: React.PropTypes.func.isRequired,
onKeyPress: React.PropTypes.func.isRequired,
- onHeightChange: React.PropTypes.func,
createMessage: React.PropTypes.string.isRequired,
onKeyDown: React.PropTypes.func,
supportsCommands: React.PropTypes.bool.isRequired
diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx
index 5eb319320..c1041e438 100644
--- a/web/react/dispatcher/event_helpers.jsx
+++ b/web/react/dispatcher/event_helpers.jsx
@@ -9,6 +9,7 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
+import * as Utils from '../utils/utils.jsx';
export function emitChannelClickEvent(channel) {
AsyncClient.getChannels(true);
@@ -180,3 +181,27 @@ export function emitPreferenceChangedEvent(preference) {
preference
});
}
+
+export function emitRemovePost(post) {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.REMOVE_POST,
+ post
+ });
+}
+
+export function sendEphemeralPost(message, channelId) {
+ const timestamp = Utils.getTimestamp();
+ const post = {
+ id: Utils.generateId(),
+ user_id: '0',
+ channel_id: channelId || ChannelStore.getCurrentId(),
+ message,
+ type: Constants.POST_TYPE_EPHEMERAL,
+ create_at: timestamp,
+ update_at: timestamp,
+ filenames: [],
+ props: {}
+ };
+
+ emitPostRecievedEvent(post);
+}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 08ffef822..8ff58f685 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -57,6 +57,7 @@ class PostStoreClass extends EventEmitter {
this.clearFocusedPost = this.clearFocusedPost.bind(this);
this.clearChannelVisibility = this.clearChannelVisibility.bind(this);
+ this.deletePost = this.deletePost.bind(this);
this.removePost = this.removePost.bind(this);
this.getPendingPosts = this.getPendingPosts.bind(this);
@@ -65,10 +66,6 @@ class PostStoreClass extends EventEmitter {
this.clearPendingPosts = this.clearPendingPosts.bind(this);
this.updatePendingPost = this.updatePendingPost.bind(this);
- this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this);
- this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this);
- this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this);
-
// These functions are bad and work should be done to remove this system when the RHS dies
this.storeSelectedPost = this.storeSelectedPost.bind(this);
this.getSelectedPost = this.getSelectedPost.bind(this);
@@ -211,28 +208,6 @@ class PostStoreClass extends EventEmitter {
postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order);
}
- // Add deleted posts
- if (this.postsInfo[id].hasOwnProperty('deletedPosts')) {
- Object.assign(postList.posts, this.postsInfo[id].deletedPosts);
-
- for (const postID in this.postsInfo[id].deletedPosts) {
- if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) {
- postList.order.push(postID);
- }
- }
-
- // Merge would be faster
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
return postList;
}
@@ -286,15 +261,6 @@ class PostStoreClass extends EventEmitter {
if (combinedPosts.order.indexOf(pid) === -1) {
combinedPosts.order.push(pid);
}
- } else {
- if (pid in combinedPosts.posts) {
- Reflect.deleteProperty(combinedPosts.posts, pid);
- }
-
- const index = combinedPosts.order.indexOf(pid);
- if (index !== -1) {
- combinedPosts.order.splice(index, 1);
- }
}
}
}
@@ -365,6 +331,22 @@ class PostStoreClass extends EventEmitter {
this.postsInfo[id].atBottom = atBottom;
}
+ deletePost(post) {
+ const postList = this.postsInfo[post.channel_id].postList;
+
+ if (isPostListNull(postList)) {
+ return;
+ }
+
+ if (post.id in postList.posts) {
+ // make sure to copy the post so that component state changes work properly
+ postList.posts[post.id] = Object.assign({}, post, {
+ state: Constants.POST_DELETED,
+ filenames: []
+ });
+ }
+ }
+
removePost(post) {
const channelId = post.channel_id;
this.makePostsInfo(channelId);
@@ -439,37 +421,6 @@ class PostStoreClass extends EventEmitter {
this.emitChange();
}
- storeUnseenDeletedPost(post) {
- let posts = this.getUnseenDeletedPosts(post.channel_id);
-
- if (!posts) {
- posts = {};
- }
-
- post.message = this.delete_message;
- post.state = Constants.POST_DELETED;
- post.filenames = [];
-
- posts[post.id] = post;
-
- this.makePostsInfo(post.channel_id);
- this.postsInfo[post.channel_id].deletedPosts = posts;
- }
-
- getUnseenDeletedPosts(channelId) {
- if (this.postsInfo.hasOwnProperty(channelId)) {
- return this.postsInfo[channelId].deletedPosts;
- }
-
- return null;
- }
-
- clearUnseenDeletedPosts(channelId) {
- if (this.postsInfo.hasOwnProperty(channelId)) {
- Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts');
- }
- }
-
storeSelectedPost(postList) {
this.selectedPost = postList;
}
@@ -581,9 +532,6 @@ class PostStoreClass extends EventEmitter {
return commentCount;
}
- deleteMessage(msg) {
- this.delete_message = msg;
- }
}
var PostStore = new PostStoreClass();
@@ -615,7 +563,6 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
case ActionTypes.CLICK_CHANNEL:
PostStore.clearFocusedPost();
PostStore.clearChannelVisibility(action.id, true);
- PostStore.clearUnseenDeletedPosts(action.prev);
break;
case ActionTypes.CREATE_POST:
PostStore.storePendingPost(action.post);
@@ -623,7 +570,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
PostStore.jumpPostsViewToBottom();
break;
case ActionTypes.POST_DELETED:
- PostStore.storeUnseenDeletedPost(action.post);
+ PostStore.deletePost(action.post);
+ PostStore.emitChange();
+ break;
+ case ActionTypes.REMOVE_POST:
PostStore.removePost(action.post);
PostStore.emitChange();
break;
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 744c2c8e5..9c3270f68 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -64,6 +64,9 @@ class SocketStoreClass extends EventEmitter {
ErrorStore.storeLastError(null);
ErrorStore.emitChange();
}
+
+ AsyncClient.getChannels();
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
}
this.failCount = 0;
@@ -71,6 +74,16 @@ class SocketStoreClass extends EventEmitter {
conn.onclose = () => {
conn = null;
+
+ if (this.failCount === 0) {
+ console.log('websocket closed'); //eslint-disable-line no-console
+ }
+
+ this.failCount = this.failCount + 1;
+
+ ErrorStore.storeLastError({connErrorCount: this.failCount, message: this.translations.socketError});
+ ErrorStore.emitChange();
+
setTimeout(
() => {
this.initialize();
@@ -80,14 +93,10 @@ class SocketStoreClass extends EventEmitter {
};
conn.onerror = (evt) => {
- if (this.failCount === 0) {
- console.log('websocket error ' + evt); //eslint-disable-line no-console
+ if (this.failCount <= 1) {
+ console.log('websocket error'); //eslint-disable-line no-console
+ console.log(evt); //eslint-disable-line no-console
}
-
- this.failCount = this.failCount + 1;
-
- ErrorStore.storeLastError({connErrorCount: this.failCount, message: this.translations.socketError});
- ErrorStore.emitChange();
};
conn.onmessage = (evt) => {
@@ -109,6 +118,7 @@ class SocketStoreClass extends EventEmitter {
handleMessage(msg) {
switch (msg.action) {
case SocketEvents.POSTED:
+ case SocketEvents.EPHEMERAL_MESSAGE:
handleNewPostEvent(msg, this.translations);
break;
@@ -179,7 +189,6 @@ function handleNewPostEvent(msg, translations) {
mentions = JSON.parse(msg.props.mentions);
}
- const channelType = msgProps.channel_type;
const channel = ChannelStore.get(msg.channel_id);
const user = UserStore.getCurrentUser();
const member = ChannelStore.getMember(msg.channel_id);
@@ -191,7 +200,7 @@ function handleNewPostEvent(msg, translations) {
if (notifyLevel === 'none') {
return;
- } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channelType !== Constants.DM_CHANNEL) {
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) {
return;
}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 11a8da669..c1bd41b88 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -12,6 +12,7 @@ export default {
LEAVE_CHANNEL: null,
CREATE_POST: null,
POST_DELETED: null,
+ REMOVE_POST: null,
RECIEVED_CHANNELS: null,
RECIEVED_CHANNEL: null,
@@ -78,7 +79,8 @@ export default {
USER_ADDED: 'user_added',
USER_REMOVED: 'user_removed',
TYPING: 'typing',
- PREFERENCE_CHANGED: 'preference_changed'
+ PREFERENCE_CHANGED: 'preference_changed',
+ EPHEMERAL_MESSAGE: 'ephemeral_message'
},
//SPECIAL_MENTIONS: ['all', 'channel'],
@@ -126,6 +128,7 @@ export default {
POST_LOADING: 'loading',
POST_FAILED: 'failed',
POST_DELETED: 'deleted',
+ POST_TYPE_EPHEMERAL: 'system_ephemeral',
POST_TYPE_JOIN_LEAVE: 'system_join_leave',
SYSTEM_MESSAGE_PREFIX: 'system_',
SYSTEM_MESSAGE_PROFILE_NAME: 'System',
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 494c38bdb..e8cfc82bc 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -1355,3 +1355,7 @@ export function languages() {
]
);
}
+
+export function isPostEphemeral(post) {
+ return post.type === Constants.POST_TYPE_EPHEMERAL || post.state === Constants.POST_DELETED;
+}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 2ff49c9b7..77b66a1a8 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -408,7 +408,7 @@ body.ios {
@include legacy-pie-clearfix;
&:hover {
- .dropdown, .comment-icon__container, .post__reply {
+ .dropdown, .comment-icon__container, .post__reply, .post__remove {
visibility: visible;
}
.permalink-icon {
@@ -646,6 +646,13 @@ body.ios {
}
}
+ .post__remove {
+ display: inline-block;
+ visibility: hidden;
+ margin-right: 5px;
+ top: -1px;
+ }
+
.post__body {
word-wrap: break-word;
padding: 0.2em 0.5em 0em;
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index aee193c4b..9338cc766 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -8,6 +8,54 @@
"about.date": "Build Date:",
"about.hash": "Build Hash:",
"about.close": "Close",
+ "audit_table.sessionRevoked": "The session with id {sessionId} was revoked",
+ "audit_table.channelCreated": "Created the {channelName} channel/group",
+ "audit_table.establishedDM": "Established a direct message channel with {username}",
+ "audit_table.nameUpdated": "Updated the {channelName} channel/group name",
+ "audit_table.headerUpdated": "Updated the {channelName} channel/group header",
+ "audit_table.channelDeleted": "Deleted the channel/group with the URL {url}",
+ "audit_table.userAdded": "Added {username} to the {channelName} channel/group",
+ "audit_table.userRemoved": "Removed {username} to the {channelName} channel/group",
+ "audit_table.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
+ "audit_table.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
+ "audit_table.successfullOAuthAccess": "Successfully gave a new OAuth service access",
+ "audit_table.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
+ "audit_table.attemptedOAuthToken": "Attempted to get an OAuth access token",
+ "audit_table.successfullOAuthToken": "Successfully added a new OAuth service",
+ "audit_table.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
+ "audit_table.attemptedLogin": "Attempted to login",
+ "audit_table.successfullLogin": "Successfully logged in",
+ "audit_table.failedLogin": "FAILED login attempt",
+ "audit_table.updatePicture": "Updated your profile picture",
+ "audit_table.updateGeneral": "Updated the general settings of your account",
+ "audit_table.attemptedPassword": "Attempted to change password",
+ "audit_table.successfullPassword": "Successfully changed password",
+ "audit_table.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
+ "audit_table.updatedRol": "Updated user role(s) to ",
+ "audit_table.member": "member",
+ "audit_table.accountActive": "Account made active",
+ "audit_table.accountInactive": "Account made inactive",
+ "audit_table.by": " by {username}",
+ "audit_table.byAdmin": " by an admin",
+ "audit_table.sentEmail": "Sent an email to {email} to reset your password",
+ "audit_table.attemptedReset": "Attempted to reset password",
+ "audit_table.successfullReset": "Successfully reset password",
+ "audit_table.updateGlobalNotifications": "Updated your global notification settings",
+ "audit_table.attemptedWebhookCreate": "Attempted to create a webhook",
+ "audit_table.successfullWebhookCreate": "Successfully created a webhook",
+ "audit_table.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
+ "audit_table.attemptedWebhookDelete": "Attempted to delete a webhook",
+ "audit_table.successfullWebhookDelete": "Successfully deleted a webhook",
+ "audit_table.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
+ "audit_table.logout": "Logged out of your account",
+ "audit_table.verified": "Sucessfully verified your email address",
+ "audit_table.revokedAll": "Revoked all current sessions for the team",
+ "audit_table.loginAttempt": " (Login attempt)",
+ "audit_table.loginFailure": " (Login failure)",
+ "audit_table.moreInfo": "More info",
+ "audit_table.ip": "IP Address",
+ "audit_table.session": "Session ID",
+ "audit_table.userId": "User ID",
"access_history.title": "Access History",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
"activity_log_modal.androidNativeApp": "Android Native App",
@@ -33,7 +81,6 @@
"admin.sidebar.statistics": "- Statistics",
"admin.sidebar.ldap": "LDAP Settings",
"admin.sidebar.license": "Edition and License",
- "admin.sidebar.audits": "Audits",
"admin.sidebar.reports": "SITE REPORTS",
"admin.sidebar.view_statistics": "View Statistics",
"admin.sidebar.settings": "SETTINGS",
@@ -50,6 +97,8 @@
"admin.sidebar.teams": "TEAMS ({count})",
"admin.sidebar.other": "OTHER",
"admin.sidebar.logs": "Logs",
+ "admin.sidebar.audits": "Compliance and Auditing",
+ "admin.analytics.loading": "Loading...",
"admin.analytics.totalUsers": "Total Users",
"admin.analytics.publicChannels": "Public Channels",
"admin.analytics.privateGroups": "Private Groups",
@@ -67,8 +116,6 @@
"admin.analytics.recentActive": "Recent Active Users",
"admin.analytics.newlyCreated": "Newly Created Users",
"admin.analytics.title": "Statistics for {title}",
- "admin.audits.title": "Server Audits",
- "admin.audits.reload": "Reload",
"admin.email.notificationDisplayExample": "Ex: \"Mattermost Notification\", \"System\", \"No-Reply\"",
"admin.email.notificationEmailExample": "Ex: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
"admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
@@ -255,7 +302,7 @@
"admin.license.removing": "Removing License...",
"admin.license.uploading": "Uploading License...",
"admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.",
- "admin.license.entrepriseType": "<div><p>This compiled release of Mattermost platform is provided under a <a href=\"http://mattermost.com\" target=\"_blank\">commercial license</a>\n from Mattermost, Inc. based on your subscription level and is subject to the <a href=\"{terms}\" target=\"_blank\">Terms of Service.</a></p>\n <p>Your subscription details are as follows:</p>\n Name: {name}<br />\n Company or organization name: {company}<br/>\n Number of users: {users}<br/>\n License issued: {issued}<br/>\n Start date of license: {start}<br/>\n Expiry date of license: {expires}<br/>\n LDAP: {ldap}<br/></div>",
+ "admin.license.enterpriseType": "<div><p>This compiled release of Mattermost platform is provided under a <a href=\"http://mattermost.com\" target=\"_blank\">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href=\"{terms}\" target=\"_blank\">Terms of Service.</a></p><p>Your subscription details are as follows:</p>Name: {name}<br />Company or organization name: {company}<br/>Number of users: {users}<br/>License issued: {issued}<br/>Start date of license: {start}<br/>Expiry date of license: {expires}<br/>LDAP: {ldap}<br/></div>",
"admin.license.keyRemove": "Remove Enterprise License and Downgrade Server",
"admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start,\n <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>.\n This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.",
"admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.",
@@ -293,6 +340,8 @@
"admin.log.save": "Save",
"admin.logs.title": "Server Logs",
"admin.logs.reload": "Reload",
+ "admin.audits.title": "User Activity",
+ "admin.audits.reload": "Reload",
"admin.privacy.saving": "Saving Config...",
"admin.privacy.title": "Privacy Settings",
"admin.privacy.showEmailTitle": "Show Email Address: ",
@@ -440,54 +489,6 @@
"admin.user_item.makeActive": "Make Active",
"admin.user_item.makeInactive": "Make Inactive",
"admin.user_item.resetPwd": "Reset Password",
- "audit_table.sessionRevoked": "The session with id {sessionId} was revoked",
- "audit_table.channelCreated": "Created the {channelName} channel/group",
- "audit_table.establishedDM": "Established a direct message channel with {username}",
- "audit_table.nameUpdated": "Updated the {channelName} channel/group name",
- "audit_table.headerUpdated": "Updated the {channelName} channel/group header",
- "audit_table.channelDeleted": "Deleted the channel/group with the URL {url}",
- "audit_table.userAdded": "Added {username} to the {channelName} channel/group",
- "audit_table.userRemoved": "Removed {username} to the {channelName} channel/group",
- "audit_table.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
- "audit_table.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
- "audit_table.successfullOAuthAccess": "Successfully gave a new OAuth service access",
- "audit_table.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
- "audit_table.attemptedOAuthToken": "Attempted to get an OAuth access token",
- "audit_table.successfullOAuthToken": "Successfully added a new OAuth service",
- "audit_table.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
- "audit_table.attemptedLogin": "Attempted to login",
- "audit_table.successfullLogin": "Successfully logged in",
- "audit_table.failedLogin": "FAILED login attempt",
- "audit_table.updatePicture": "Updated your profile picture",
- "audit_table.updateGeneral": "Updated the general settings of your account",
- "audit_table.attemptedPassword": "Attempted to change password",
- "audit_table.successfullPassword": "Successfully changed password",
- "audit_table.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
- "audit_table.updatedRol": "Updated user role(s) to ",
- "audit_table.member": "member",
- "audit_table.accountActive": "Account made active",
- "audit_table.accountInactive": "Account made inactive",
- "audit_table.by": " by {username}",
- "audit_table.byAdmin": " by an admin",
- "audit_table.sentEmail": "Sent an email to {email} to reset your password",
- "audit_table.attemptedReset": "Attempted to reset password",
- "audit_table.successfullReset": "Successfully reset password",
- "audit_table.updateGlobalNotifications": "Updated your global notification settings",
- "audit_table.attemptedWebhookCreate": "Attempted to create a webhook",
- "audit_table.successfullWebhookCreate": "Successfully created a webhook",
- "audit_table.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
- "audit_table.attemptedWebhookDelete": "Attempted to delete a webhook",
- "audit_table.successfullWebhookDelete": "Successfully deleted a webhook",
- "audit_table.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
- "audit_table.logout": "Logged out of your account",
- "audit_table.verified": "Sucessfully verified your email address",
- "audit_table.revokedAll": "Revoked all current sessions for the team",
- "audit_table.loginAttempt": " (Login attempt)",
- "audit_table.loginFailure": " (Login failure)",
- "audit_table.userId": "User ID",
- "audit_table.moreInfo": "More info",
- "audit_table.ip": "IP: {ip}",
- "audit_table.session": "Session ID: {id}",
"authorize.title": "An application would like to connect to your {teamName} account",
"authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.",
"authorize.access": "Allow <strong>{appName}</strong> access?",
@@ -566,7 +567,6 @@
"create_post.comment": "Comment",
"create_post.post": "Post",
"create_post.write": "Write a message...",
- "create_post.deleteMsg": "(message deleted)",
"create_post.tutorialTip": "<h4>Sending Messages</h4><p>Type here to write a message and press <strong>Enter</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>",
"delete_channel.channel": "channel",
"delete_channel.group": "group",
@@ -772,6 +772,7 @@
"members_popover.title": "Members",
"post_attachment.collapse": "▲ collapse text",
"post_attachment.more": "▼ read more",
+ "post_body.deleted": "(message deleted)",
"post_body.plusOne": " plus 1 other file",
"post_body.plusMore": " plus {count} other files",
"post_body.commentedOn": "Commented on {name}{apostrophe} message: ",
@@ -1262,4 +1263,4 @@
"intro_messages.beginning": "Beginning of {name}",
"intro_messages.invite": "Invite others to this {type}",
"intro_messages.setHeader": "Set a Header"
-} \ No newline at end of file
+}
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index 962fe176a..5cd36930c 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -211,7 +211,7 @@
"admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar,\n <a href=\"http://mattermost.com\" target=\"_blank\">deshabilita todas las características de la Edición Enterprise de este servidor</a>.\n Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.",
"admin.license.edition": "Edición: ",
"admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.",
- "admin.license.entrepriseType": "<div><p>Esta versión compilada de la plataforma de Mattermost es proporcionada bajo una <a href=\"http://mattermost.com\" target=\"_blank\">licencia comercial</a>\n de Mattermost, Inc. basado en tu nivel de subscripción y sujeto a los <a href=\"{terms}\" target=\"_blank\">Términos del Servicio.</a></p>\n <p>Los detalles de tu subscripción son los siguientes:</p>\n Nombre: {name}<br />\n Nombre de compañia u organización: {company}<br/>\n Cantidad de usuarios: {users}<br/>\n Licencia emitida por: {issued}<br/>\n Inicio de la licencia: {start}<br/>\n Fecha de expiración: {expires}<br/>\n LDAP: {ldap}<br/></div>",
+ "admin.license.enterpriseType": "<div><p>Esta versión compilada de la plataforma de Mattermost es proporcionada bajo una <a href=\"http://mattermost.com\" target=\"_blank\">licencia comercial</a> de Mattermost, Inc. basado en tu nivel de subscripción y sujeto a los <a href=\"{terms}\" target=\"_blank\">Términos del Servicio.</a></p><p>Los detalles de tu subscripción son los siguientes:</p>Nombre: {name}<br />Nombre de compañia u organización: {company}<br/>Cantidad de usuarios: {users}<br/>Licencia emitida por: {issued}<br/>Inicio de la licencia: {start}<br/>Fecha de expiración: {expires}<br/>LDAP: {ldap}<br/></div>",
"admin.license.key": "Llave de la Licencia: ",
"admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor",
"admin.license.removing": "Removiendo Licencia...",
@@ -593,7 +593,6 @@
"create_comment.file": "Subiendo archivo",
"create_comment.files": "Subiendo archivos",
"create_post.comment": "Comentario",
- "create_post.deleteMsg": "(mensaje eliminado)",
"create_post.post": "Mensaje",
"create_post.tutorialTip": "<h4>Enviar Mensajes</h4> <p>Escribe aquí para redactar un mensaje y presiona <strong>Retorno</strong> para enviarlo.</p><p>Pincha el botón de <strong>Adjuntar</strong> para subir una imagen o archivo.</p>",
"create_post.write": "Escribe un mensaje...",
@@ -813,6 +812,7 @@
"post_attachment.collapse": "▲ colapsar texto",
"post_attachment.more": "▼ leer más",
"post_body.commentedOn": "Comentó el mensaje de {name}{apostrophe}: ",
+ "post_body.deleted": "(mensaje eliminado)",
"post_body.plusMore": " más {count} otros archivos",
"post_body.plusOne": " más 1 archivo",
"post_body.retry": "Reintentar",
@@ -1262,4 +1262,4 @@
"view_image_popover.download": "Descargar",
"view_image_popover.file": "Archivo {count} de {total}",
"view_image_popover.publicLink": "Obtener Enlace Público"
-} \ No newline at end of file
+}