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 (
-
);
},