diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
index 951b863e6a..38188974ac 100644
--- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss
+++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
@@ -42,5 +42,10 @@ limitations under the License.
.mx_EventTile_line, .mx_EventTile_content {
margin-right: 0px;
}
+
+ .mx_MessageActionBar .mx_AccessibleButton {
+ font-size: 10px;
+ padding: 0 8px;
+ }
}
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index 7ac0e95e81..b7ba2ef27d 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -30,9 +30,9 @@ limitations under the License.
z-index: 1;
> * {
+ white-space: nowrap;
display: inline-block;
position: relative;
- width: 27px;
border: 1px solid $message-action-bar-border-color;
margin-left: -1px;
@@ -55,6 +55,11 @@ limitations under the License.
}
}
+
+.mx_MessageActionBar_maskButton {
+ width: 27px;
+}
+
.mx_MessageActionBar_maskButton::after {
content: '';
position: absolute;
diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
new file mode 100644
index 0000000000..db00f445a8
--- /dev/null
+++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
@@ -0,0 +1,90 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+
+/*
+ * A dialog for confirming a redaction.
+ * Also shows a spinner (and possible error) while the redaction is ongoing,
+ * and only closes the dialog when the redaction is done or failed.
+ *
+ * This is done to prevent the edit history dialog racing with the redaction:
+ * if this dialog closes and the MessageEditHistoryDialog is shown again,
+ * it will fetch the relations again, which will race with the ongoing /redact request.
+ * which will cause the edit to appear unredacted.
+ *
+ * To avoid this, we keep the dialog open as long as /redact is in progress.
+ */
+export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isRedacting: false,
+ redactionErrorCode: null,
+ };
+ }
+
+ onParentFinished = async (proceed) => {
+ if (proceed) {
+ this.setState({isRedacting: true});
+ try {
+ await this.props.redact();
+ this.props.onFinished(true);
+ } catch (error) {
+ const code = error.errcode || error.statusCode;
+ if (typeof code !== "undefined") {
+ this.setState({redactionErrorCode: code});
+ } else {
+ this.props.onFinished(true);
+ }
+ }
+ } else {
+ this.props.onFinished(false);
+ }
+ };
+
+ render() {
+ if (this.state.isRedacting) {
+ if (this.state.redactionErrorCode) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const code = this.state.redactionErrorCode;
+ return (
+
+ );
+ } else {
+ const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
+ const Spinner = sdk.getComponent('elements.Spinner');
+ return (
+
+
+
+ );
+ }
+ } else {
+ const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
+ return ;
+ }
+ }
+}
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
index b30891488b..53dd6b2a1b 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -46,12 +46,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
const opts = {from: this.state.nextBatch};
const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId();
+ const client = MatrixClientPeg.get();
let result;
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
try {
- result = await MatrixClientPeg.get().relations(
+ result = await client.relations(
roomId, eventId, "m.replace", "m.room.message", opts);
} catch (error) {
// log if the server returned an error
@@ -61,8 +62,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
this.setState({error}, () => reject(error));
return promise;
}
+
+ const newEvents = result.events;
+ this._locallyRedactEventsIfNeeded(newEvents);
this.setState({
- events: this.state.events.concat(result.events),
+ events: this.state.events.concat(newEvents),
nextBatch: result.nextBatch,
isLoading: false,
}, () => {
@@ -72,6 +76,21 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
return promise;
}
+ _locallyRedactEventsIfNeeded(newEvents) {
+ const roomId = this.props.mxEvent.getRoomId();
+ const client = MatrixClientPeg.get();
+ const room = client.getRoom(roomId);
+ const pendingEvents = room.getPendingEvents();
+ for (const e of newEvents) {
+ const pendingRedaction = pendingEvents.find(pe => {
+ return pe.getType() === "m.room.redaction" && pe.getAssociatedId() === e.getId();
+ });
+ if (pendingRedaction) {
+ e.markLocallyRedacted(pendingRedaction);
+ }
+ }
+ }
+
componentDidMount() {
this.loadMoreEdits();
}
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index fef9c362c6..797e010aae 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -20,6 +20,11 @@ import * as HtmlUtils from '../../../HtmlUtils';
import {formatTime} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {pillifyLinks} from '../../../utils/pillify';
+import { _t } from '../../../languageHandler';
+import sdk from '../../../index';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import Modal from '../../../Modal';
+import classNames from 'classnames';
export default class EditHistoryMessage extends React.PureComponent {
static propTypes = {
@@ -27,12 +32,87 @@ export default class EditHistoryMessage extends React.PureComponent {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
};
+ constructor(props) {
+ super(props);
+ const cli = MatrixClientPeg.get();
+ const {userId} = cli.credentials;
+ const event = this.props.mxEvent;
+ const room = cli.getRoom(event.getRoomId());
+ if (event.localRedactionEvent()) {
+ event.localRedactionEvent().on("status", this._onAssociatedStatusChanged);
+ }
+ const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
+ this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
+ }
+
+ _onAssociatedStatusChanged = () => {
+ this.setState({sendStatus: this.props.mxEvent.getAssociatedStatus()});
+ };
+
+ _onRedactClick = async () => {
+ const event = this.props.mxEvent;
+ const cli = MatrixClientPeg.get();
+ const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
+
+ Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
+ redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
+ }, 'mx_Dialog_confirmredact');
+ };
+
+ _onViewSourceClick = () => {
+ const ViewSource = sdk.getComponent('structures.ViewSource');
+ Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
+ roomId: this.props.mxEvent.getRoomId(),
+ eventId: this.props.mxEvent.getId(),
+ content: this.props.mxEvent.event,
+ }, 'mx_Dialog_viewsource');
+ };
+
+ pillifyLinks() {
+ // not present for redacted events
+ if (this.refs.content) {
+ pillifyLinks(this.refs.content.children, this.props.mxEvent);
+ }
+ }
+
componentDidMount() {
- pillifyLinks(this.refs.content.children, this.props.mxEvent);
+ this.pillifyLinks();
+ }
+
+ componentWillUnmount() {
+ const event = this.props.mxEvent;
+ if (event.localRedactionEvent()) {
+ event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
+ }
}
componentDidUpdate() {
- pillifyLinks(this.refs.content.children, this.props.mxEvent);
+ this.pillifyLinks();
+ }
+
+ _renderActionBar() {
+ const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+ // hide the button when already redacted
+ let redactButton;
+ if (!this.props.mxEvent.isRedacted()) {
+ redactButton = (
+
+ {_t("Remove")}
+
+ );
+ }
+ const viewSourceButton = (
+
+ {_t("View Source")}
+
+ );
+ // disabled remove button when not allowed
+ return (
+
+ {redactButton}
+ {viewSourceButton}
+
+ );
}
render() {
@@ -41,21 +121,38 @@ export default class EditHistoryMessage extends React.PureComponent {
const content = originalContent["m.new_content"] || originalContent;
const contentElements = HtmlUtils.bodyToHtml(content);
let contentContainer;
- if (mxEvent.getContent().msgtype === "m.emote") {
+ if (mxEvent.isRedacted()) {
+ const UnknownBody = sdk.getComponent('messages.UnknownBody');
+ contentContainer = ;
+ } else if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
- contentContainer = (*
- { name }
- {contentElements}
-
);
+ contentContainer = (
+ *
+ { name }
+ {contentElements}
+
+ );
} else {
- contentContainer = ({contentElements}
);
+ contentContainer = {contentElements}
;
}
const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
- return
-
- {timestamp}
- { contentContainer }
-
- ;
+ const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
+ const classes = classNames({
+ "mx_EventTile": true,
+ "mx_EventTile_redacted": mxEvent.isRedacted(),
+ "mx_EventTile_sending": isSending,
+ "mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
+ });
+ return (
+
+
+
+ {timestamp}
+ { contentContainer }
+ { this._renderActionBar() }
+
+
+
+ );
}
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a8febdf2e4..5d100bcfb0 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -93,6 +93,7 @@
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
"Unnamed Room": "Unnamed Room",
"Error": "Error",
+ "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
@@ -1126,6 +1127,7 @@
"Start chatting": "Start chatting",
"Click on the button below to start chatting!": "Click on the button below to start chatting!",
"Start Chatting": "Start Chatting",
+ "Removing…": "Removing…",
"Confirm Removal": "Confirm Removal",
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
"Clear all data on this device?": "Clear all data on this device?",
@@ -1310,7 +1312,6 @@
"Reject invitation": "Reject invitation",
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
"Unable to reject invite": "Unable to reject invite",
- "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Resend": "Resend",
"Resend edit": "Resend edit",
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",