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) {
|
function _onStartChatFinished(shouldInvite, addrs) {
|
||||||
if (!shouldInvite) return;
|
if (!shouldInvite) return;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import MatrixClientPeg from './MatrixClientPeg';
|
||||||
import CallHandler from './CallHandler';
|
import CallHandler from './CallHandler';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
|
import {isValid3pidInvite} from "./RoomInvite";
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
function textForMemberEvent(ev) {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
|
@ -366,6 +367,15 @@ function textForCallInviteEvent(event) {
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
function textForThreePidInviteEvent(event) {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
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.', {
|
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||||
senderName,
|
senderName,
|
||||||
targetDisplayName: event.getContent().display_name,
|
targetDisplayName: event.getContent().display_name,
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default class RightPanel extends React.Component {
|
||||||
FilePanel: 'FilePanel',
|
FilePanel: 'FilePanel',
|
||||||
NotificationPanel: 'NotificationPanel',
|
NotificationPanel: 'NotificationPanel',
|
||||||
RoomMemberInfo: 'RoomMemberInfo',
|
RoomMemberInfo: 'RoomMemberInfo',
|
||||||
|
Room3pidMemberInfo: 'Room3pidMemberInfo',
|
||||||
GroupMemberInfo: 'GroupMemberInfo',
|
GroupMemberInfo: 'GroupMemberInfo',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -155,6 +156,7 @@ export default class RightPanel extends React.Component {
|
||||||
groupRoomId: payload.groupRoomId,
|
groupRoomId: payload.groupRoomId,
|
||||||
groupId: payload.groupId,
|
groupId: payload.groupId,
|
||||||
member: payload.member,
|
member: payload.member,
|
||||||
|
event: payload.event,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,6 +164,7 @@ export default class RightPanel extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||||
|
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
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} />;
|
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
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) {
|
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||||
panel = <GroupMemberInfo
|
panel = <GroupMemberInfo
|
||||||
groupMember={this.state.member}
|
groupMember={this.state.member}
|
||||||
|
|
|
@ -42,6 +42,12 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
||||||
}
|
}
|
||||||
} else if (payload.action === "view_room") {
|
} else if (payload.action === "view_room") {
|
||||||
this.setPhase(RightPanel.Phase.RoomMemberList);
|
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 = [
|
const membersPhases = [
|
||||||
RightPanel.Phase.RoomMemberList,
|
RightPanel.Phase.RoomMemberList,
|
||||||
RightPanel.Phase.RoomMemberInfo,
|
RightPanel.Phase.RoomMemberInfo,
|
||||||
|
RightPanel.Phase.Room3pidMemberInfo,
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
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) {
|
_filterMembers: function(members, membership, query) {
|
||||||
return members.filter((m) => {
|
return members.filter((m) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
|
@ -374,11 +381,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
|
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
|
if (!isValid3pidInvite(e)) return false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// discard all invites which have a m.room.member event since we've
|
// discard all invites which have a m.room.member event since we've
|
||||||
// already added them.
|
// already added them.
|
||||||
|
@ -410,6 +413,7 @@ module.exports = React.createClass({
|
||||||
return <EntityTile key={e.getStateKey()}
|
return <EntityTile key={e.getStateKey()}
|
||||||
name={e.getContent().display_name}
|
name={e.getContent().display_name}
|
||||||
suppressOnHover={true}
|
suppressOnHover={true}
|
||||||
|
onClick={() => this._onPending3pidInviteClick(e)}
|
||||||
/>;
|
/>;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUmount() {
|
componentWillUnmount() {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
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)",
|
"(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
|
||||||
"%(senderName)s ended the call.": "%(senderName)s ended the call.",
|
"%(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 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 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 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.",
|
"%(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",
|
"Stickerpack": "Stickerpack",
|
||||||
"Hide Stickers": "Hide Stickers",
|
"Hide Stickers": "Hide Stickers",
|
||||||
"Show Stickers": "Show 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.",
|
"Jump to first unread message.": "Jump to first unread message.",
|
||||||
"Error updating main address": "Error updating main address",
|
"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.",
|
"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