diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index b762ad3fca..be9be4db81 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -16,65 +16,176 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import SyntaxHighlight from '../views/elements/SyntaxHighlight'; -import {_t} from "../../languageHandler"; +import React from "react"; +import PropTypes from "prop-types"; +import SyntaxHighlight from "../views/elements/SyntaxHighlight"; +import { _t } from "../../languageHandler"; import * as sdk from "../../index"; -import {replaceableComponent} from "../../utils/replaceableComponent"; - +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; +import { canEditContent } from "../../utils/EventUtils"; +import { MatrixClientPeg } from '../../MatrixClientPeg'; +import { replaceableComponent } from "../../utils/replaceableComponent"; @replaceableComponent("structures.ViewSource") export default class ViewSource extends React.Component { static propTypes = { - content: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - eventId: PropTypes.string.isRequired, - isEncrypted: PropTypes.bool.isRequired, - decryptedContent: PropTypes.object, + mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu }; - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + constructor(props) { + super(props); - let content; - if (this.props.isEncrypted) { - content = <> -
- - {_t("Decrypted event source")} - - - { JSON.stringify(this.props.decryptedContent, null, 2) } - -
-
- - {_t("Original event source")} - - - { JSON.stringify(this.props.content, null, 2) } - -
- ; + this.state = { + isEditing: false, + }; + } + + onBack() { + // TODO: refresh the "Event ID:" modal header + this.setState({ isEditing: false }); + } + + onEdit() { + this.setState({ isEditing: true }); + } + + // returns the dialog body for viewing the event source + viewSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = mxEvent.isEncrypted(); + const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const originalEventSource = mxEvent.event; + + if (isEncrypted) { + return ( + <> +
+ + {_t("Decrypted event source")} + + {JSON.stringify(decryptedEventSource, null, 2)} +
+
+ + {_t("Original event source")} + + {JSON.stringify(originalEventSource, null, 2)} +
+ + ); } else { - content = <> -
{_t("Original event source")}
- - { JSON.stringify(this.props.content, null, 2) } - - ; + return ( + <> +
{_t("Original event source")}
+ {JSON.stringify(originalEventSource, null, 2)} + + ); } + } + // returns the id of the initial message, not the id of the previous edit + getBaseEventId() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = mxEvent.isEncrypted(); + const baseMxEvent = this.props.mxEvent; + + if (isEncrypted) { + // `relates_to` field is inside the encrypted event + return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } else { + return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } + } + + // returns the SendCustomEvent component prefilled with the correct details + editSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + + const isStateEvent = mxEvent.isState(); + const roomId = mxEvent.getRoomId(); + const originalContent = mxEvent.getContent(); + + if (isStateEvent) { + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(originalContent, null, "\t"), + stateKey: mxEvent.getStateKey(), + }} + /> + )} + + ); + } else { + // prefill an edit-message event + // keep only the `body` and `msgtype` fields of originalContent + const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there + const newContent = { + "body": ` * ${bodyToStartFrom}`, + "msgtype": originalContent.msgtype, + "m.new_content": { + body: bodyToStartFrom, + msgtype: originalContent.msgtype, + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: this.getBaseEventId(), + }, + }; + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(newContent, null, "\t"), + }} + /> + )} + + ); + } + } + + canSendStateEvent(mxEvent) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(mxEvent.getRoomId()); + return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + + const isEditing = this.state.isEditing; + const roomId = mxEvent.getRoomId(); + const eventId = mxEvent.getId(); + const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); return ( - -
-
Room ID: { this.props.roomId }
-
Event ID: { this.props.eventId }
+ +
+
Room ID: {roomId}
+
Event ID: {eventId}
- { content } + {isEditing ? this.editSourceContent() : this.viewSourceContent()}
+ {!isEditing && canEdit && ( +
+ +
+ )} ); } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index f97b429abc..daabf224dc 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -126,15 +126,9 @@ export default class MessageContextMenu extends React.Component { }; onViewSourceClick = () => { - const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', '', ViewSource, { - roomId: ev.getRoomId(), - eventId: ev.getId(), - content: ev.event, - isEncrypted: ev.isEncrypted(), - // FIXME: _clearEvent is private - decryptedContent: ev._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 9b24aaa571..dfcc5d6dfb 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -74,13 +74,14 @@ class GenericEditor extends React.PureComponent { } } -class SendCustomEvent extends GenericEditor { +export class SendCustomEvent extends GenericEditor { static getLabel() { return _t('Send Custom Event'); } static propTypes = { onBack: PropTypes.func.isRequired, room: PropTypes.instanceOf(Room).isRequired, forceStateEvent: PropTypes.bool, + forceGeneralEvent: PropTypes.bool, inputs: PropTypes.object, }; @@ -141,6 +142,8 @@ class SendCustomEvent extends GenericEditor {
; } + const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent; + return
@@ -156,7 +159,7 @@ class SendCustomEvent extends GenericEditor {
{ !this.state.message && } - { !this.state.message && !this.props.forceStateEvent &&
+ { showTglFlip &&
} diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index c5002b3308..a298f7eb68 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -76,12 +76,7 @@ export default class EditHistoryMessage extends React.PureComponent { _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, - isEncrypted: this.props.mxEvent.isEncrypted(), - // FIXME: _clearEvent is private - decryptedContent: this.props.mxEvent._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); };