From 5beec37c43c1765356d80ec3d55fb12f67c5f0c1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 28 Mar 2019 20:38:15 -0600 Subject: [PATCH 1/2] Add MemberInfo for 3pid invites and support revoking those invites Fixes https://github.com/vector-im/riot-web/issues/625 Fixes https://github.com/vector-im/riot-web/issues/6411 Fixes https://github.com/vector-im/riot-web/issues/5490 --- src/TextForEvent.js | 9 ++ src/components/structures/RightPanel.js | 5 + .../views/right_panel/RoomHeaderButtons.js | 7 + src/components/views/rooms/MemberList.js | 8 + .../views/rooms/ThirdPartyMemberInfo.js | 142 ++++++++++++++++++ src/i18n/strings/en_EN.json | 5 + 6 files changed, 176 insertions(+) create mode 100644 src/components/views/rooms/ThirdPartyMemberInfo.js diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 030c346ccc..05d83d740a 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -366,6 +366,15 @@ function textForCallInviteEvent(event) { function textForThreePidInviteEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); + + if (!event.getContent().display_name) { + const targetDisplayName = event.getPrevContent().display_name || _t("Someone"); + return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', { + senderName, + targetDisplayName, + }); + } + return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', { senderName, targetDisplayName: event.getContent().display_name, diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 5c745b04cc..74820c804a 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -50,6 +50,7 @@ export default class RightPanel extends React.Component { FilePanel: 'FilePanel', NotificationPanel: 'NotificationPanel', RoomMemberInfo: 'RoomMemberInfo', + Room3pidMemberInfo: 'Room3pidMemberInfo', GroupMemberInfo: 'GroupMemberInfo', }); @@ -155,6 +156,7 @@ export default class RightPanel extends React.Component { groupRoomId: payload.groupRoomId, groupId: payload.groupId, member: payload.member, + event: payload.event, }); } } @@ -162,6 +164,7 @@ export default class RightPanel extends React.Component { render() { const MemberList = sdk.getComponent('rooms.MemberList'); const MemberInfo = sdk.getComponent('rooms.MemberInfo'); + const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const FilePanel = sdk.getComponent('structures.FilePanel'); @@ -180,6 +183,8 @@ export default class RightPanel extends React.Component { panel = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { panel = ; + } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { + panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { panel = { if (query) { @@ -408,6 +415,7 @@ module.exports = React.createClass({ return this._onPending3pidInviteClick(e)} />; })); } diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js new file mode 100644 index 0000000000..3fe8382251 --- /dev/null +++ b/src/components/views/rooms/ThirdPartyMemberInfo.js @@ -0,0 +1,142 @@ +/* +Copyright 2019 New Vector Ltd. + +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 MatrixClientPeg from "../../../MatrixClientPeg"; +import {MatrixEvent} from "matrix-js-sdk"; +import {_t} from "../../../languageHandler"; +import dis from "../../../dispatcher"; +import sdk from "../../../index"; +import Modal from "../../../Modal"; + +export default class ThirdPartyMemberInfo extends React.Component { + static propTypes = { + event: PropTypes.instanceOf(MatrixEvent).isRequired, + }; + + constructor(props) { + super(props); + + const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId()); + const me = room.getMember(MatrixClientPeg.get().getUserId()); + const powerLevels = room.currentState.getStateEvents("m.room.power_levels", ""); + + let kickLevel = powerLevels ? powerLevels.getContent().kick : 50; + if (typeof(kickLevel) !== 'number') kickLevel = 50; + + const sender = room.getMember(this.props.event.getSender()); + + this.state = { + stateKey: this.props.event.getStateKey(), + roomId: this.props.event.getRoomId(), + displayName: this.props.event.getContent().display_name, + invited: true, + canKick: me ? me.powerLevel > kickLevel : false, + senderName: sender ? sender.name : this.props.event.getSender(), + }; + } + + componentWillMount(): void { + MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); + } + + componentWillUnmount(): void { + const client = MatrixClientPeg.get(); + if (client) { + client.removeListener("RoomState.events", this.onRoomStateEvents); + } + } + + onRoomStateEvents = (ev) => { + if (ev.getType() === "m.room.third_party_invite" && ev.getStateKey() === this.state.stateKey) { + const newDisplayName = ev.getContent().display_name; + const isInvited = !!newDisplayName; // display_name indicates a valid invite + + const newState = {invited: isInvited}; + if (newDisplayName) newState['displayName'] = newDisplayName; + this.setState(newState); + } + }; + + onCancel = () => { + dis.dispatch({ + action: "view_3pid_invite", + event: null, + }); + }; + + onKickClick = () => { + MatrixClientPeg.get().sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey) + .catch((err) => { + console.error(err); + + // Revert echo because of error + this.setState({invited: true}); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Revoke 3pid invite failed', '', ErrorDialog, { + title: _t("Failed to revoke invite"), + description: _t( + "Could not revoke the invite. The server may be experiencing a temporary problem or " + + "you do not have sufficient permissions to revoke the invite.", + ), + }); + }); + + // Local echo + this.setState({invited: false}); + }; + + render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + let adminTools = null; + if (this.state.canKick && this.state.invited) { + adminTools = ( +
+

{_t("Admin Tools")}

+
+ + {_t("Revoke invite")} + +
+
+ ); + } + + // We shamelessly rip off the MemberInfo styles here. + return ( +
+
+ +

{this.state.displayName}

+
+
+
+
+ {_t("Invited by %(sender)s", {sender: this.state.senderName})} +
+
+
+ {adminTools} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 94d524d767..0f696e2893 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -223,6 +223,7 @@ "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)", "%(senderName)s ended the call.": "%(senderName)s ended the call.", "%(senderName)s placed a %(callType)s call.": "%(senderName)s placed a %(callType)s call.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.", @@ -823,6 +824,10 @@ "Stickerpack": "Stickerpack", "Hide Stickers": "Hide Stickers", "Show Stickers": "Show Stickers", + "Failed to revoke invite": "Failed to revoke invite", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.", + "Revoke invite": "Revoke invite", + "Invited by %(sender)s": "Invited by %(sender)s", "Jump to first unread message.": "Jump to first unread message.", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", From 42cfe74f700b4db0fdbdc5e8f24e373df23a6246 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 29 Mar 2019 11:45:07 -0600 Subject: [PATCH 2/2] Add common utility for checking 3pid invites We just need to make sure they are structurally sound - actual validation is done by other parties. --- src/RoomInvite.js | 18 ++++++++++++++++++ src/TextForEvent.js | 3 ++- src/components/views/rooms/MemberList.js | 7 ++----- .../views/rooms/ThirdPartyMemberInfo.js | 7 ++++--- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 3547b9195f..b808b935a6 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -65,6 +65,24 @@ export function showRoomInviteDialog(roomId) { }); } +/** + * Checks if the given MatrixEvent is a valid 3rd party user invite. + * @param {MatrixEvent} event The event to check + * @returns {boolean} True if valid, false otherwise + */ +export function isValid3pidInvite(event) { + if (!event || event.getType() !== "m.room.third_party_invite") return false; + + // any events without these keys are not valid 3pid invites, so we ignore them + const requiredKeys = ['key_validity_url', 'public_key', 'display_name']; + for (let i = 0; i < requiredKeys.length; ++i) { + if (!event.getContent()[requiredKeys[i]]) return false; + } + + // Valid enough by our standards + return true; +} + function _onStartChatFinished(shouldInvite, addrs) { if (!shouldInvite) return; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 05d83d740a..a700fe2a3c 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -17,6 +17,7 @@ import MatrixClientPeg from './MatrixClientPeg'; import CallHandler from './CallHandler'; import { _t } from './languageHandler'; import * as Roles from './Roles'; +import {isValid3pidInvite} from "./RoomInvite"; function textForMemberEvent(ev) { // XXX: SYJS-16 "sender is sometimes null for join messages" @@ -367,7 +368,7 @@ function textForCallInviteEvent(event) { function textForThreePidInviteEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); - if (!event.getContent().display_name) { + if (!isValid3pidInvite(event)) { const targetDisplayName = event.getPrevContent().display_name || _t("Someone"); return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', { senderName, diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index a8cc948f63..e79f2f21d4 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -20,6 +20,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; +import {isValid3pidInvite} from "../../../RoomInvite"; const MatrixClientPeg = require("../../../MatrixClientPeg"); const sdk = require('../../../index'); const rate_limited_func = require('../../../ratelimitedfunc'); @@ -379,11 +380,7 @@ module.exports = React.createClass({ if (room) { return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) { - // any events without these keys are not valid 3pid invites, so we ignore them - const requiredKeys = ['key_validity_url', 'public_key', 'display_name']; - for (let i = 0; i < requiredKeys.length; ++i) { - if (e.getContent()[requiredKeys[i]] === undefined) return false; - } + if (!isValid3pidInvite(e)) return false; // discard all invites which have a m.room.member event since we've // already added them. diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js index 3fe8382251..754e32871f 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.js +++ b/src/components/views/rooms/ThirdPartyMemberInfo.js @@ -22,6 +22,7 @@ import {_t} from "../../../languageHandler"; import dis from "../../../dispatcher"; import sdk from "../../../index"; import Modal from "../../../Modal"; +import {isValid3pidInvite} from "../../../RoomInvite"; export default class ThirdPartyMemberInfo extends React.Component { static propTypes = { @@ -64,7 +65,7 @@ export default class ThirdPartyMemberInfo extends React.Component { onRoomStateEvents = (ev) => { if (ev.getType() === "m.room.third_party_invite" && ev.getStateKey() === this.state.stateKey) { const newDisplayName = ev.getContent().display_name; - const isInvited = !!newDisplayName; // display_name indicates a valid invite + const isInvited = isValid3pidInvite(ev); const newState = {invited: isInvited}; if (newDisplayName) newState['displayName'] = newDisplayName; @@ -123,8 +124,8 @@ export default class ThirdPartyMemberInfo extends React.Component {

{this.state.displayName}