/* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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. */ /* * State vars: * 'can': { * kick: boolean, * ban: boolean, * mute: boolean, * modifyLevel: boolean * }, * 'muted': boolean, * 'isTargetMod': boolean */ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import createRoom from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import Unread from '../../../Unread'; import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import GeminiScrollbar from 'react-gemini-scrollbar'; import RoomViewStore from '../../../stores/RoomViewStore'; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', propTypes: { matrixClient: PropTypes.object.isRequired, member: PropTypes.object.isRequired, }, getInitialState: function() { return { can: { kick: false, ban: false, mute: false, modifyLevel: false, }, muted: false, isTargetMod: false, updating: 0, devicesLoading: true, devices: null, isIgnoring: false, }; }, componentWillMount: function() { this._cancelDeviceList = null; // only display the devices list if our client supports E2E this._enableDevices = this.props.matrixClient.isCryptoEnabled(); const cli = this.props.matrixClient; 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("RoomMember.membership", this.onRoomMemberMembership); cli.on("accountData", this.onAccountData); this._checkIgnoreState(); }, componentDidMount: function() { this._updateStateForNewMember(this.props.member); }, componentWillReceiveProps: function(newProps) { if (this.props.member.userId !== newProps.member.userId) { this._updateStateForNewMember(newProps.member); } }, componentWillUnmount: function() { const client = this.props.matrixClient; 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("RoomMember.membership", this.onRoomMemberMembership); client.removeListener("accountData", this.onAccountData); } if (this._cancelDeviceList) { this._cancelDeviceList(); } }, _checkIgnoreState: function() { const isIgnoring = this.props.matrixClient.isUserIgnored(this.props.member.userId); this.setState({isIgnoring: isIgnoring}); }, _disambiguateDevices: function(devices) { const names = Object.create(null); for (let i = 0; i < devices.length; i++) { const name = devices[i].getDisplayName(); const indexList = names[name] || []; indexList.push(i); names[name] = indexList; } for (const 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. // Promise.resolve to handle transition from static result to promise; can be removed // in future Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => { 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 (findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { this.forceUpdate(); } }, onRoomStateEvents: function(ev, state) { this.forceUpdate(); }, onRoomMemberName: function(ev, member) { this.forceUpdate(); }, onRoomMemberMembership: function(ev, member) { if (this.props.member.userId === member.userId) this.forceUpdate(); }, onAccountData: function(ev) { if (ev.getType() === 'm.direct') { this.forceUpdate(); } }, _updateStateForNewMember: function(member) { const 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; } let cancelled = false; this._cancelDeviceList = function() { cancelled = true; }; const client = this.props.matrixClient; const self = this; client.downloadKeys([member.userId], true).then(() => { return client.getStoredDevicesForUser(member.userId); }).finally(function() { self._cancelDeviceList = null; }).done(function(devices) { if (cancelled) { // we got cancelled - presumably a different user now return; } self._disambiguateDevices(devices); self.setState({devicesLoading: false, devices: devices}); }, function(err) { console.log("Error downloading devices", err); self.setState({devicesLoading: false}); }); }, onIgnoreToggle: function() { const ignoredUsers = this.props.matrixClient.getIgnoredUsers(); if (this.state.isIgnoring) { const index = ignoredUsers.indexOf(this.props.member.userId); if (index !== -1) ignoredUsers.splice(index, 1); } else { ignoredUsers.push(this.props.member.userId); } this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => { return this.setState({isIgnoring: !this.state.isIgnoring}); }); }, onKick: function() { const membership = this.props.member.membership; const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { member: this.props.member, action: membership === "invite" ? _t("Disinvite") : _t("Kick"), title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"), askReason: membership === "join", danger: true, onFinished: (proceed, reason) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); this.props.matrixClient.kick( this.props.member.roomId, this.props.member.userId, reason || undefined, ).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) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { title: _t("Failed to kick"), description: ((err && err.message) ? err.message : "Operation failed"), }); }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); }, }); }, onBanOrUnban: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { member: this.props.member, action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"), title: this.props.member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"), askReason: this.props.member.membership !== 'ban', danger: this.props.member.membership !== 'ban', onFinished: (proceed, reason) => { if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); let promise; if (this.props.member.membership === 'ban') { promise = this.props.matrixClient.unban( this.props.member.roomId, this.props.member.userId, ); } else { promise = this.props.matrixClient.ban( this.props.member.roomId, this.props.member.userId, reason || undefined, ); } promise.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) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Ban error: " + err); Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to ban user"), }); }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); }, }); }, onMuteToggle: function() { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomId = this.props.member.roomId; const target = this.props.member.userId; const room = this.props.matrixClient.getRoom(roomId); if (!room) return; const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; const isMuted = this.state.muted; const powerLevels = powerLevelEvent.getContent(); const levelToSend = ( (powerLevels.events ? powerLevels.events["m.room.message"] : null) || powerLevels.events_default ); let level; if (isMuted) { // unmute level = levelToSend; } else { // mute level = levelToSend - 1; } level = parseInt(level); if (!isNaN(level)) { this.setState({ updating: this.state.updating + 1 }); this.props.matrixClient.setPowerLevel(roomId, target, level, 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("Mute toggle success"); }, function(err) { console.error("Mute error: " + err); Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to mute user"), }); }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); } }, onModToggle: function() { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomId = this.props.member.roomId; const target = this.props.member.userId; const room = this.props.matrixClient.getRoom(roomId); if (!room) return; const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; const me = room.getMember(this.props.matrixClient.credentials.userId); if (!me) return; const defaultLevel = powerLevelEvent.getContent().users_default; let modLevel = me.powerLevel - 1; if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults // toggle the level const newLevel = this.state.isTargetMod ? defaultLevel : modLevel; this.setState({ updating: this.state.updating + 1 }); this.props.matrixClient.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) { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { dis.dispatch({action: 'view_set_mxid'}); } else { console.error("Toggle moderator error:" + err); Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to toggle moderator status"), }); } }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }); }, _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { this.setState({ updating: this.state.updating + 1 }); this.props.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), 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("Power change success"); }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change power level " + err); Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to change power level"), }); }, ).finally(()=>{ this.setState({ updating: this.state.updating - 1 }); }).done(); }, onPowerChange: function(powerLevel) { const roomId = this.props.member.roomId; const target = this.props.member.userId; const room = this.props.matrixClient.getRoom(roomId); if (!room) return; const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; if (!powerLevelEvent.getContent().users) { this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); return; } const myUserId = this.props.matrixClient.getUserId(); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. if (myUserId === target) { Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, { title: _t("Warning!"), description: