diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 92e0d5690c..e6800ef7b5 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +20,15 @@ limitations under the License. align-items: center; color: $primary-fg-color; cursor: pointer; + + .mx_E2EIcon { + margin: 0; + position: absolute; + bottom: 2px; + right: 7px; + height: 15px; + width: 15px; + } } .mx_EntityTile:hover { diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 40bf9f6d78..133205b1a5 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,7 +23,7 @@ import * as sdk from '../../../index'; import AccessibleButton from '../elements/AccessibleButton'; import { _t } from '../../../languageHandler'; import classNames from "classnames"; - +import E2EIcon from './E2EIcon'; const PRESENCE_CLASS = { "offline": "mx_EntityTile_offline", @@ -30,7 +31,6 @@ const PRESENCE_CLASS = { "unavailable": "mx_EntityTile_unavailable", }; - function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { if (showPresence === false) { return 'mx_EntityTile_online_beenactive'; @@ -69,6 +69,7 @@ const EntityTile = createReactClass({ suppressOnHover: PropTypes.bool, showPresence: PropTypes.bool, subtextLabel: PropTypes.string, + e2eStatus: PropTypes.string, }, getDefaultProps: function() { @@ -165,6 +166,12 @@ const EntityTile = createReactClass({ }[powerStatus]; } + let e2eIcon; + const { e2eStatus } = this.props; + if (e2eStatus) { + e2eIcon = ; + } + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const av = this.props.avatarJsx || ; @@ -177,6 +184,7 @@ const EntityTile = createReactClass({
{ av } { powerLabel } + { e2eIcon }
{ nameEl } { inviteButton } @@ -189,5 +197,4 @@ const EntityTile = createReactClass({ EntityTile.POWER_STATUS_MODERATOR = "moderator"; EntityTile.POWER_STATUS_ADMIN = "admin"; - export default EntityTile; diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 95e5495339..649e1b4277 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import createReactClass from 'create-react-class'; import * as sdk from "../../../index"; import dis from "../../../dispatcher"; import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; export default createReactClass({ displayName: 'MemberTile', @@ -40,29 +41,101 @@ export default createReactClass({ getInitialState: function() { return { statusMessage: this.getStatusMessage(), + isRoomEncrypted: false, + e2eStatus: null, }; }, componentDidMount() { - if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { - return; + const cli = MatrixClientPeg.get(); + + if (SettingsStore.isFeatureEnabled("feature_custom_status")) { + const { user } = this.props.member; + if (user) { + user.on("User._unstable_statusMessage", this._onStatusMessageCommitted); + } } - const { user } = this.props.member; - if (!user) { - return; + + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + const { roomId } = this.props.member; + if (roomId) { + const isRoomEncrypted = cli.isRoomEncrypted(roomId); + this.setState({ + isRoomEncrypted, + }); + if (isRoomEncrypted) { + cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged); + this.updateE2EStatus(); + } else { + // Listen for room to become encrypted + cli.on("RoomState.events", this.onRoomStateEvents); + } + } } - user.on("User._unstable_statusMessage", this._onStatusMessageCommitted); }, componentWillUnmount() { + const cli = MatrixClientPeg.get(); + const { user } = this.props.member; - if (!user) { + if (user) { + user.removeListener( + "User._unstable_statusMessage", + this._onStatusMessageCommitted, + ); + } + + if (cli) { + cli.removeListener("RoomState.events", this.onRoomStateEvents); + cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged); + } + }, + + onRoomStateEvents: function(ev) { + if (ev.getType() !== "m.room.encryption") return; + const { roomId } = this.props.member; + if (ev.getRoomId() !== roomId) return; + + // The room is encrypted now. + const cli = MatrixClientPeg.get(); + cli.removeListener("RoomState.events", this.onRoomStateEvents); + this.setState({ + isRoomEncrypted: true, + }); + this.updateE2EStatus(); + }, + + onUserTrustStatusChanged: function(userId, trustStatus) { + if (userId !== this.props.member.userId) return; + this.updateE2EStatus(); + }, + + updateE2EStatus: async function() { + const cli = MatrixClientPeg.get(); + const { userId } = this.props.member; + const isMe = userId === cli.getUserId(); + const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); + if (!userVerified) { + this.setState({ + e2eStatus: "normal", + }); return; } - user.removeListener( - "User._unstable_statusMessage", - this._onStatusMessageCommitted, - ); + + const devices = await cli.getStoredDevicesForUser(userId); + const anyDeviceUnverified = devices.some(device => { + const { deviceId } = device; + // For your own devices, we use the stricter check of cross-signing + // verification to encourage everyone to trust their own devices via + // cross-signing so that other users can then safely trust you. + // For other people's devices, the more general verified check that + // includes locally verified devices can be used. + const deviceTrust = cli.checkDeviceTrust(userId, deviceId); + return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); + }); + this.setState({ + e2eStatus: anyDeviceUnverified ? "warning" : "verified", + }); }, getStatusMessage() { @@ -94,6 +167,12 @@ export default createReactClass({ ) { return true; } + if ( + nextState.isRoomEncrypted !== this.state.isRoomEncrypted || + nextState.e2eStatus !== this.state.e2eStatus + ) { + return true; + } return false; }, @@ -153,14 +232,26 @@ export default createReactClass({ const powerStatus = powerStatusMap.get(powerLevel); + let e2eStatus; + if (this.state.isRoomEncrypted) { + e2eStatus = this.state.e2eStatus; + } + return ( - ); },