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 030c346ccc..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"
@@ -366,6 +367,15 @@ function textForCallInviteEvent(event) {
function textForThreePidInviteEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
+
+ 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,
+ 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) {
@@ -372,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.
@@ -408,6 +412,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..754e32871f
--- /dev/null
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.js
@@ -0,0 +1,143 @@
+/*
+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";
+import {isValid3pidInvite} from "../../../RoomInvite";
+
+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 = isValid3pidInvite(ev);
+
+ 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.",