Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
@import "./views/elements/_ProgressBar.scss";
@import "./views/elements/_QRCode.scss";
@import "./views/elements/_ReplyThread.scss";
@import "./views/elements/_ForwardedEvent.scss";
@import "./views/elements/_ResizeHandle.scss";
@import "./views/elements/_RichText.scss";
@import "./views/elements/_RoleButton.scss";
Expand Down
22 changes: 22 additions & 0 deletions res/css/views/elements/_ForwardedEvent.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Copyright 2020 Tulir Asokan <[email protected]>

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.
*/

blockquote.mx_ForwardedEvent {
margin-top: 0;
margin-left: 0;
padding-left: 10px;
border-left: 4px solid $blockquote-bar-color;
}
17 changes: 17 additions & 0 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
case 'send_forward':
this.onForwardEvent(payload.room_id, payload.event);
break;
case 'aria_hide_main_app':
this.setState({
hideToSRUsers: true,
Expand Down Expand Up @@ -1831,6 +1834,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}

onForwardEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get();
if (!cli) {
dis.dispatch({action: 'message_send_failed'});
return;
}

cli._unstable_forwardEvent(event.getRoomId(), event.getId(), roomId).then(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
dis.dispatch({action: 'message_send_failed'});
});
}

private setPageSubtitle(subtitle = '') {
if (this.state.currentRoomId) {
const client = MatrixClientPeg.get();
Expand Down
6 changes: 4 additions & 2 deletions src/components/views/context_menus/MessageContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import { isContentActionable, isContentForwardable } from '../../../utils/EventUtils';
import {MenuItem} from "../../structures/ContextMenu";

function canCancel(eventStatus) {
Expand Down Expand Up @@ -366,13 +366,15 @@ export default createReactClass({
);
}

if (isContentActionable(mxEvent)) {
if (isContentForwardable(mxEvent)) {
forwardButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
{ _t('Forward Message') }
</MenuItem>
);
}

if (isContentActionable(mxEvent)) {
if (this.state.canPin) {
pinButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
Expand Down
88 changes: 88 additions & 0 deletions src/components/views/messages/ForwardedEventTile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2020 Tulir Asokan <[email protected]>

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 PropTypes from 'prop-types';
import { MatrixEvent } from "matrix-js-sdk";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import * as Avatar from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";

const AVATAR_SIZE = 32;

export default class ForwardedEventTile extends React.Component {
static propTypes = {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
};

constructor(props) {
super(props);
const contentCopy = {...props.mxEvent.getWireContent()};
const forwardMeta = contentCopy["net.maunium.msc2730.forwarded"];
delete contentCopy["net.maunium.msc2730.forwarded"];
const forwardVerification = props.mxEvent.getUnsigned()["net.maunium.msc2730.forwarded"] || {};
this.state = {
evt: new MatrixEvent({
...forwardMeta,
content: contentCopy,
event_id: forwardVerification.event_id,
type: props.mxEvent.getType(),
}),
senderProfile: {
name: forwardMeta.sender,
getAvatarUrl: (..._) => null,
},
};
if (forwardMeta.unsigned && forwardMeta.unsigned.displayname) {
this.state.senderProfile.name = forwardMeta.unsigned.displayname;
this.state.senderProfile.getAvatarUrl = (..._) => forwardMeta.unsigned.avatar_url;
}
}

async componentDidMount() {
const client = MatrixClientPeg.get();
const profileInfo = await client.getProfileInfo(this.state.evt.getSender());
const avatarUrl = Avatar.avatarUrlForUser(
{avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop");
this.setState({
senderProfile: {
name: profileInfo.displayname,
getAvatarUrl: (..._) => avatarUrl,
},
});
}

render() {
const EventTile = sdk.getComponent('views.rooms.EventTile');
this.state.evt.sender = {
...this.state.senderProfile,
userId: this.state.evt.getSender(),
};
return <blockquote className="mx_ForwardedEvent" key={this.props.mxEvent.getId()}>
<EventTile
mxEvent={this.state.evt}
tileShape="reply"
onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator}
isRedacted={false}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
useIRCLayout={this.props.useIRCLayout}
/>
</blockquote>;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For easier review and reference, let's add some before/after screenshots in the PR description for the various pieces here.

9 changes: 9 additions & 0 deletions src/components/views/rooms/EventTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ export function getHandlerTile(ev) {
}
}

// TODO make sure server supports verifying forwards somewhere before trusting
if (SettingsStore.isFeatureEnabled("feature_verifiable_forwarded_events")) {
const forwardMeta = ev.getWireContent()["net.maunium.msc2730.forwarded"];
const forwardVerification = ev.getUnsigned()["net.maunium.msc2730.forwarded"] || {};
if (forwardMeta && forwardVerification.valid) {
return "messages.ForwardedEventTile";
}
}

return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}

Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
"Include original metadata when forwarding messages": "Include original metadata when forwarding messages",
"Show info about bridges in room settings": "Show info about bridges in room settings",
"Font size": "Font size",
"Use custom size": "Use custom size",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"feature_verifiable_forwarded_events": {
isFeature: true,
displayName: _td("Include original metadata when forwarding messages"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT],
default: [],
Expand Down
5 changes: 4 additions & 1 deletion src/stores/RoomViewStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Modal from '../Modal';
import { _t } from '../languageHandler';
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
import {ActionPayload} from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";

const INITIAL_STATE = {
// Whether we're joining the currently viewed room (see isJoining())
Expand Down Expand Up @@ -194,8 +195,10 @@ class RoomViewStore extends Store<ActionPayload> {
}

if (this.state.forwardingEvent) {
const action = SettingsStore.isFeatureEnabled("feature_verifiable_forwarded_events")
? 'send_forward' : 'send_event'
dis.dispatch({
action: 'send_event',
action: action,
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
Expand Down
15 changes: 15 additions & 0 deletions src/utils/EventUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import { EventStatus } from 'matrix-js-sdk';
import {MatrixClientPeg} from '../MatrixClientPeg';
import shouldHideEvent from "../shouldHideEvent";
import SettingsStore from "../settings/SettingsStore";
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
* which effectively checks whether it's a regular message that has been sent and that we
Expand Down Expand Up @@ -45,6 +46,20 @@ export function isContentActionable(mxEvent) {
return false;
}

export function isContentForwardable(mxEvent) {
if (!SettingsStore.isFeatureEnabled("feature_verifiable_forwarded_events")) {
return isContentActionable(mxEvent);
}
const isSent = !mxEvent.status || mxEvent.status === EventStatus.SENT;
const isForwarded = !!mxEvent.getWireContent()["net.maunium.msc2730.forwarded"];
let isValidForward = false;
if (isForwarded) {
isValidForward = mxEvent.getUnsigned()["net.maunium.msc2730.forwarded"]?.["valid"];
}
return isSent && !mxEvent.isRedacted() && !mxEvent.isRedaction() && !mxEvent.isState()
&& (!isForwarded || isValidForward);
}

export function canEditContent(mxEvent) {
if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) {
return false;
Expand Down