Merge pull request #2843 from matrix-org/travis/cancel-3pid
Add MemberInfo for 3pid invites and support revoking those invites
This commit is contained in:
commit
05e47766b4
8 changed files with 199 additions and 7 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||
panel = <GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
|
|
|
@ -42,6 +42,12 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
}
|
||||
} else if (payload.action === "view_room") {
|
||||
this.setPhase(RightPanel.Phase.RoomMemberList);
|
||||
} else if (payload.action === "view_3pid_invite") {
|
||||
if (payload.event) {
|
||||
this.setPhase(RightPanel.Phase.Room3pidMemberInfo, {event: payload.event});
|
||||
} else {
|
||||
this.setPhase(RightPanel.Phase.RoomMemberList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +55,7 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
const membersPhases = [
|
||||
RightPanel.Phase.RoomMemberList,
|
||||
RightPanel.Phase.RoomMemberInfo,
|
||||
RightPanel.Phase.Room3pidMemberInfo,
|
||||
];
|
||||
|
||||
return [
|
||||
|
|
|
@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
|
@ -349,6 +349,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onPending3pidInviteClick: function(inviteEvent) {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
event: inviteEvent,
|
||||
});
|
||||
},
|
||||
|
||||
_filterMembers: function(members, membership, query) {
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
|
@ -374,11 +381,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.
|
||||
|
@ -410,6 +413,7 @@ module.exports = React.createClass({
|
|||
return <EntityTile key={e.getStateKey()}
|
||||
name={e.getContent().display_name}
|
||||
suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(e)}
|
||||
/>;
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
|||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
},
|
||||
|
||||
componentWillUmount() {
|
||||
componentWillUnmount() {
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
|
|
143
src/components/views/rooms/ThirdPartyMemberInfo.js
Normal file
143
src/components/views/rooms/ThirdPartyMemberInfo.js
Normal file
|
@ -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 = (
|
||||
<div className="mx_MemberInfo_container">
|
||||
<h3>{_t("Admin Tools")}</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
|
||||
{_t("Revoke invite")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We shamelessly rip off the MemberInfo styles here.
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
title={_t('Close')}
|
||||
/>
|
||||
<h2>{this.state.displayName}</h2>
|
||||
</div>
|
||||
<div className="mx_MemberInfo_container">
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{_t("Invited by %(sender)s", {sender: this.state.senderName})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{adminTools}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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.",
|
||||
|
|
Loading…
Reference in a new issue