diff --git a/src/component-index.js b/src/component-index.js index 967cc5d685..3570523bde 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -79,6 +79,7 @@ module.exports.components['views.rooms.EntityTile'] = require('./components/view module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList'); module.exports.components['views.rooms.LinkPreviewWidget'] = require('./components/views/rooms/LinkPreviewWidget'); +module.exports.components['views.rooms.MemberDeviceInfo'] = require('./components/views/rooms/MemberDeviceInfo'); module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo'); module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3face4649c..635f9c5413 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -246,6 +246,23 @@ module.exports = React.createClass({ }); }, + _renderDeviceInfo: function() { + var client = MatrixClientPeg.get(); + var deviceId = client.deviceId; + var olmKey = client.getDeviceEd25519Key() || ""; + return ( +
+

Cryptography

+
+
    +
  • Device ID: {deviceId}
  • +
  • Device key: {olmKey}
  • +
+
+
+ ); + }, + render: function() { var self = this; var Loader = sdk.getComponent("elements.Spinner"); @@ -392,6 +409,8 @@ module.exports = React.createClass({ {notification_area} + {this._renderDeviceInfo()} +

Advanced

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7db8af9312..ff02139d87 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -128,16 +128,24 @@ module.exports = React.createClass({ }, getInitialState: function() { - return {menu: false, allReadAvatars: false}; + return {menu: false, allReadAvatars: false, verified: null}; }, componentWillMount: function() { // don't do RR animations until we are mounted this._suppressReadReceiptAnimation = true; + this._verifyEvent(this.props.mxEvent); }, componentDidMount: function() { this._suppressReadReceiptAnimation = false; + MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); + }, + + componentWillReceiveProps: function (nextProps) { + if (nextProps.mxEvent !== this.props.mxEvent) { + this._verifyEvent(nextProps.mxEvent); + } }, shouldComponentUpdate: function (nextProps, nextState) { @@ -152,6 +160,31 @@ module.exports = React.createClass({ return false; }, + componentWillUnmount: function() { + var client = MatrixClientPeg.get(); + if (client) { + client.removeListener("deviceVerified", this.onDeviceVerified); + } + }, + + onDeviceVerified: function(userId, device) { + if (userId == this.props.mxEvent.getSender()) { + this._verifyEvent(this.props.mxEvent); + } + }, + + _verifyEvent: function(mxEvent) { + var verified = null; + + if (mxEvent.isEncrypted()) { + verified = MatrixClientPeg.get().isEventSenderVerified(mxEvent); + } + + this.setState({ + verified: verified + }); + }, + _propsEqual: function(objA, objB) { var keysA = Object.keys(objA); var keysB = Object.keys(objB); @@ -346,6 +379,8 @@ module.exports = React.createClass({ mx_EventTile_last: this.props.last, mx_EventTile_contextual: this.props.contextual, menu: this.state.menu, + mx_EventTile_verified: this.state.verified == true, + mx_EventTile_unverified: this.state.verified == false, }); var timestamp = diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js new file mode 100644 index 0000000000..da53c744b2 --- /dev/null +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -0,0 +1,55 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +var React = require('react'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'MemberDeviceInfo', + propTypes: { + userId: React.PropTypes.string.isRequired, + device: React.PropTypes.object.isRequired, + }, + + onVerifyClick: function() { + MatrixClientPeg.get().setDeviceVerified(this.props.userId, + this.props.device.id); + }, + + render: function() { + var indicator = null, button = null; + if (this.props.device.verified) { + indicator = ( +
+ ); + } else { + button = ( +
+ Verify +
+ ); + } + return ( +
+
{this.props.device.id}
+
{this.props.device.key}
+ {indicator} + {button} +
+ ); + }, +}); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index ba4a3734f5..c50b6e919e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -34,23 +34,95 @@ var sdk = require('../../../index'); module.exports = React.createClass({ displayName: 'MemberInfo', + propTypes: { + member: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func, + }, + getDefaultProps: function() { return { onFinished: function() {} }; }, - componentDidMount: function() { - // work out the current state - if (this.props.member) { - var memberState = this._calculateOpsPermissions(this.props.member); - this.setState(memberState); + getInitialState: function() { + return { + can: { + kick: false, + ban: false, + mute: false, + modifyLevel: false + }, + muted: false, + isTargetMod: false, + updating: 0, + devices: null, // null means device list is loading } }, + + componentWillMount: function() { + this._cancelDeviceList = null; + }, + + componentDidMount: function() { + this._updateStateForNewMember(this.props.member); + MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); + }, + componentWillReceiveProps: function(newProps) { - var memberState = this._calculateOpsPermissions(newProps.member); - this.setState(memberState); + if (this.props.member.userId != newProps.member.userId) { + this._updateStateForNewMember(newProps.member); + } + }, + + componentWillUnmount: function() { + var client = MatrixClientPeg.get(); + if (client) { + client.removeListener("deviceVerified", this.onDeviceVerified); + } + if (this._cancelDeviceList) { + this._cancelDeviceList(); + } + }, + + onDeviceVerified: function(userId, device) { + 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().listDeviceKeys(userId); + this.setState({devices: devices}); + } + }, + + _updateStateForNewMember: function(member) { + var newState = this._calculateOpsPermissions(member); + newState.devices = null; + this.setState(newState); + + if (this._cancelDeviceList) { + this._cancelDeviceList(); + this._cancelDeviceList = null; + } + + this._downloadDeviceList(member); + }, + + _downloadDeviceList: function(member) { + var cancelled = false; + this._cancelDeviceList = function() { cancelled = true; } + + var client = MatrixClientPeg.get(); + var self = this; + client.downloadKeys([member.userId], true).done(function() { + if (cancelled) { + // we got cancelled - presumably a different user now + return; + } + self._cancelDeviceList = null; + var devices = client.listDeviceKeys(member.userId); + self.setState({devices: devices}); + }); }, onKick: function() { @@ -371,20 +443,6 @@ module.exports = React.createClass({ this.props.onFinished(); }, - getInitialState: function() { - return { - can: { - kick: false, - ban: false, - mute: false, - modifyLevel: false - }, - muted: false, - isTargetMod: false, - updating: 0, - } - }, - _calculateOpsPermissions: function(member) { var defaultPerms = { can: {}, @@ -476,6 +534,32 @@ module.exports = React.createClass({ Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); }, + _renderDevices: function() { + var devices = this.state.devices; + var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); + var Spinner = sdk.getComponent("elements.Spinner"); + + var devComponents; + if (devices === null) { + // still loading + devComponents = ; + } else { + devComponents = []; + for (var i = 0; i < devices.length; i++) { + devComponents.push(); + } + } + + return ( +
+

Devices

+ {devComponents} +
+ ); + }, + render: function() { var startChat, kickButton, banButton, muteButton, giveModButton, spinner; if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { @@ -552,6 +636,8 @@ module.exports = React.createClass({ { startChat } + { this._renderDevices() } + { adminTools } { spinner } @@ -559,4 +645,3 @@ module.exports = React.createClass({ ); } }); -