element-web/src/components/views/rooms/MemberInfo.js

713 lines
25 KiB
JavaScript
Raw Normal View History

2015-09-18 17:39:16 +00:00
/*
2016-01-07 04:06:39 +00:00
Copyright 2015, 2016 OpenMarket Ltd
2015-09-18 17:39:16 +00:00
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.
*/
/*
* State vars:
* 'can': {
* kick: boolean,
* ban: boolean,
* mute: boolean,
* modifyLevel: boolean
* },
* 'muted': boolean,
* 'isTargetMod': boolean
*/
2015-11-26 17:49:39 +00:00
var React = require('react');
2016-09-09 15:15:01 +00:00
var classNames = require('classnames');
2015-11-26 17:49:39 +00:00
var MatrixClientPeg = require("../../../MatrixClientPeg");
var dis = require("../../../dispatcher");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom');
2016-09-09 15:15:01 +00:00
var DMRoomMap = require('../../../utils/DMRoomMap');
var Unread = require('../../../Unread');
var Receipt = require('../../../utils/Receipt');
2015-09-18 17:39:16 +00:00
2015-11-26 17:49:39 +00:00
module.exports = React.createClass({
displayName: 'MemberInfo',
2015-09-18 17:39:16 +00:00
propTypes: {
member: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func,
},
getDefaultProps: function() {
return {
onFinished: function() {}
};
},
getInitialState: function() {
return {
can: {
kick: false,
ban: false,
mute: false,
modifyLevel: false
},
muted: false,
isTargetMod: false,
updating: 0,
devicesLoading: true,
devices: null,
2015-09-18 17:39:16 +00:00
}
},
componentWillMount: function() {
this._cancelDeviceList = null;
// only display the devices list if our client supports E2E *and* the
// feature is enabled in the user settings
this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() &&
UserSettingsStore.isFeatureEnabled("e2e_encryption");
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
},
componentDidMount: function() {
this._updateStateForNewMember(this.props.member);
},
componentWillReceiveProps: function(newProps) {
if (this.props.member.userId != newProps.member.userId) {
this._updateStateForNewMember(newProps.member);
}
},
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("Room", this.onRoom);
client.removeListener("deleteRoom", this.onDeleteRoom);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.name", this.onRoomName);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("RoomState.events", this.onRoomStateEvents);
client.removeListener("RoomMember.name", this.onRoomMemberName);
client.removeListener("accountData", this.onAccountData);
}
if (this._cancelDeviceList) {
this._cancelDeviceList();
}
},
_disambiguateDevices: function(devices) {
var names = Object.create(null);
for (var i = 0; i < devices.length; i++) {
var name = devices[i].getDisplayName();
var indexList = names[name] || [];
indexList.push(i);
names[name] = indexList;
}
for (name in names) {
if (names[name].length > 1) {
names[name].forEach((j)=>{
devices[j].ambiguous = true;
});
}
}
},
onDeviceVerificationChanged: function(userId, device) {
if (!this._enableDevices) {
return;
}
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
var devices = MatrixClientPeg.get().getStoredDevicesForUser(userId);
this.setState({devices: devices});
}
},
onRoom: function(room) {
this.forceUpdate();
},
onDeleteRoom: function(roomId) {
this.forceUpdate();
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
this.forceUpdate();
},
onRoomName: function(room) {
this.forceUpdate();
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
this.forceUpdate();
}
},
onRoomStateEvents: function(ev, state) {
this.forceUpdate();
},
onRoomMemberName: function(ev, member) {
this.forceUpdate();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
this.forceUpdate();
}
},
_updateStateForNewMember: function(member) {
var newState = this._calculateOpsPermissions(member);
newState.devicesLoading = true;
newState.devices = null;
this.setState(newState);
if (this._cancelDeviceList) {
this._cancelDeviceList();
this._cancelDeviceList = null;
}
this._downloadDeviceList(member);
},
_downloadDeviceList: function(member) {
if (!this._enableDevices) {
return;
}
var cancelled = false;
this._cancelDeviceList = function() { cancelled = true; }
var client = MatrixClientPeg.get();
var self = this;
client.downloadKeys([member.userId], true).finally(function() {
self._cancelDeviceList = null;
}).done(function() {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
var devices = client.getStoredDevicesForUser(member.userId);
self._disambiguateDevices(devices);
self.setState({devicesLoading: false, devices: devices});
}, function(err) {
console.log("Error downloading devices", err);
self.setState({devicesLoading: false});
});
},
2015-09-18 17:39:16 +00:00
onKick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
2015-09-18 17:39:16 +00:00
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().kick(roomId, target).then(function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Kick success");
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Kick error",
description: err.message
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
},
onBan: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
2015-09-18 17:39:16 +00:00
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().ban(roomId, target).then(
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Ban success");
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Ban error",
description: err.message
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
},
onMuteToggle: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
2015-09-18 17:39:16 +00:00
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
return;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevelEvent) {
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
return;
}
var isMuted = this.state.muted;
var powerLevels = powerLevelEvent.getContent();
var levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
var level;
if (isMuted) { // unmute
level = levelToSend;
}
else { // mute
level = levelToSend - 1;
}
2016-01-22 15:29:57 +00:00
level = parseInt(level);
2015-09-18 17:39:16 +00:00
2016-01-22 15:29:57 +00:00
if (level !== NaN) {
this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).then(
2016-01-22 15:29:57 +00:00
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Mute toggle success");
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Mute error",
description: err.message
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
2016-01-22 15:29:57 +00:00
}
this.props.onFinished();
2015-09-18 17:39:16 +00:00
},
onModToggle: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
2015-09-18 17:39:16 +00:00
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
return;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevelEvent) {
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
return;
}
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) {
2015-09-22 15:37:39 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
return;
}
var defaultLevel = powerLevelEvent.getContent().users_default;
var modLevel = me.powerLevel - 1;
2016-01-18 01:18:02 +00:00
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
2015-09-18 17:39:16 +00:00
// toggle the level
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Mod toggle success");
}, function(err) {
2016-03-22 13:19:29 +00:00
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "This action cannot be performed by a guest user. Please register to be able to do this."
});
} else {
Modal.createDialog(ErrorDialog, {
title: "Moderator toggle error",
2016-03-22 13:19:29 +00:00
description: err.message
});
}
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
this.props.onFinished();
2015-09-18 17:39:16 +00:00
},
2016-03-21 00:49:18 +00:00
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
2016-03-21 00:49:18 +00:00
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Power change success");
}, function(err) {
Modal.createDialog(ErrorDialog, {
title: "Failure to change power level",
description: err.message
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
2016-03-21 00:49:18 +00:00
this.props.onFinished();
},
2016-01-18 01:18:02 +00:00
onPowerChange: function(powerLevel) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = MatrixClientPeg.get().getRoom(roomId);
2016-03-21 00:49:18 +00:00
var self = this;
2016-01-18 01:18:02 +00:00
if (!room) {
this.props.onFinished();
return;
}
var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevelEvent) {
this.props.onFinished();
return;
}
2016-03-21 00:49:18 +00:00
if (powerLevelEvent.getContent().users) {
var myPower = powerLevelEvent.getContent().users[MatrixClientPeg.get().credentials.userId];
if (parseInt(myPower) === parseInt(powerLevel)) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
description:
<div>
You will not be able to undo this change as you are promoting the user to have the same power level as yourself.<br/>
Are you sure?
</div>,
button: "Continue",
onFinished: function(confirmed) {
if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
else {
self.props.onFinished();
}
},
});
}
2016-03-21 00:49:18 +00:00
else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
}
else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
2016-01-18 01:18:02 +00:00
2016-09-09 15:15:01 +00:00
onNewDMClick: function() {
this.setState({ updating: this.state.updating + 1 });
createRoom({dmUserId: this.props.member.userId}).finally(() => {
this.props.onFinished();
2016-09-09 15:15:01 +00:00
this.setState({ updating: this.state.updating - 1 });
}).done();
2015-09-22 15:37:39 +00:00
},
onLeaveClick: function() {
dis.dispatch({
action: 'leave_room',
room_id: this.props.member.roomId,
2015-09-22 15:37:39 +00:00
});
2016-06-01 22:42:34 +00:00
this.props.onFinished();
2015-09-18 17:39:16 +00:00
},
_calculateOpsPermissions: function(member) {
2015-09-18 17:39:16 +00:00
var defaultPerms = {
can: {},
muted: false,
modifyLevel: false
};
var room = MatrixClientPeg.get().getRoom(member.roomId);
2015-09-18 17:39:16 +00:00
if (!room) {
return defaultPerms;
}
var powerLevels = room.currentState.getStateEvents(
"m.room.power_levels", ""
);
if (!powerLevels) {
return defaultPerms;
}
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) {
return defaultPerms;
}
var them = member;
2015-09-18 17:39:16 +00:00
return {
can: this._calculateCanPermissions(
me, them, powerLevels.getContent()
),
muted: this._isMuted(them, powerLevels.getContent()),
isTargetMod: them.powerLevel > powerLevels.getContent().users_default
};
},
_calculateCanPermissions: function(me, them, powerLevels) {
var can = {
kick: false,
ban: false,
mute: false,
modifyLevel: false
};
var canAffectUser = them.powerLevel < me.powerLevel;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can;
}
var editPowerLevel = (
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
);
2016-01-18 01:40:19 +00:00
var levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
2015-09-18 17:39:16 +00:00
can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel;
2016-01-18 01:40:19 +00:00
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
2015-09-18 17:39:16 +00:00
can.modifyLevel = me.powerLevel > them.powerLevel;
return can;
},
_isMuted: function(member, powerLevelContent) {
if (!powerLevelContent || !member) {
return false;
}
var levelToSend = (
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default
);
return member.powerLevel < levelToSend;
2015-11-26 17:49:39 +00:00
},
onCancel: function(e) {
dis.dispatch({
action: "view_user",
member: null
});
},
onMemberAvatarClick: function () {
2016-08-25 15:20:31 +00:00
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
if(!avatarUrl) return;
2016-04-03 23:18:18 +00:00
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
src: httpUrl,
name: this.props.member.name
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
_renderDevices: function() {
if (!this._enableDevices) {
return null;
}
var devices = this.state.devices;
var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
var Spinner = sdk.getComponent("elements.Spinner");
var devComponents;
if (this.state.devicesLoading) {
// still loading
devComponents = <Spinner />;
} else if (devices === null) {
devComponents = "Unable to load device list";
} else if (devices.length === 0) {
devComponents = "No registered devices";
} else {
devComponents = [];
for (var i = 0; i < devices.length; i++) {
devComponents.push(<MemberDeviceInfo key={i}
userId={this.props.member.userId}
device={devices[i]}/>);
}
}
return (
<div>
<h3>Devices</h3>
<div className="mx_MemberInfo_devices">
{devComponents}
</div>
</div>
);
},
2015-11-26 17:49:39 +00:00
render: function() {
2016-01-18 01:18:02 +00:00
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
2016-09-09 15:15:01 +00:00
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
const RoomTile = sdk.getComponent("rooms.RoomTile");
const tiles = [];
for (const roomId of dmRooms) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const highlight = (
room.getUnreadNotificationCount('highlight') > 0 ||
me.membership == "invite"
);
tiles.push(
<RoomTile key={room.roomId} room={room}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={me.membership == "invite"}
/>
);
}
}
2016-09-09 15:15:01 +00:00
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <div
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new direct chat</i></div>
</div>
startChat = <div>
2016-09-16 13:35:43 +00:00
<h3>Direct chats</h3>
2016-09-09 15:15:01 +00:00
{tiles}
{startNewChat}
</div>;
2015-11-26 17:49:39 +00:00
}
if (this.state.updating) {
2015-11-26 17:49:39 +00:00
var Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
}
if (this.state.can.kick) {
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
{ this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
2015-11-26 17:49:39 +00:00
</div>;
}
if (this.state.can.ban) {
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
Ban
</div>;
}
if (this.state.can.mute) {
var muteLabel = this.state.muted ? "Unmute" : "Mute";
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
{muteLabel}
</div>;
}
2016-01-18 01:40:19 +00:00
if (this.state.can.toggleMod) {
2016-01-18 01:18:02 +00:00
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
2015-11-26 17:49:39 +00:00
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</div>
}
2016-01-18 01:26:15 +00:00
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
// e.g. clicking on a linkified userid in a room
2016-01-18 01:18:02 +00:00
var adminTools;
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
2016-01-18 01:18:02 +00:00
<div>
<h3>Admin tools</h3>
<div className="mx_MemberInfo_buttons">
{muteButton}
{kickButton}
{banButton}
{giveModButton}
</div>
</div>
}
const memberName = this.props.member.name;
2016-07-05 04:54:18 +00:00
2015-11-26 17:49:39 +00:00
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
2016-01-18 01:18:02 +00:00
var PowerSelector = sdk.getComponent('elements.PowerSelector');
const EmojiText = sdk.getComponent('elements.EmojiText');
2015-11-26 17:49:39 +00:00
return (
<div className="mx_MemberInfo">
2015-12-06 22:07:49 +00:00
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
2015-11-26 17:49:39 +00:00
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
2015-11-26 17:49:39 +00:00
</div>
2016-01-18 01:18:02 +00:00
2016-08-11 10:34:14 +00:00
<EmojiText element="h2">{memberName}</EmojiText>
2016-01-18 01:18:02 +00:00
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
2016-01-18 01:18:02 +00:00
</div>
2015-11-26 17:49:39 +00:00
</div>
2016-01-18 01:18:02 +00:00
2016-09-17 14:07:41 +00:00
{ adminTools }
2016-01-18 01:18:02 +00:00
{ startChat }
{ this._renderDevices() }
2016-01-18 01:18:02 +00:00
{ spinner }
2015-11-26 17:49:39 +00:00
</div>
);
2015-09-18 17:39:16 +00:00
}
2015-11-26 17:49:39 +00:00
});