mirror of https://github.com/vector-im/riot-web
Add verification status icons to room members
This displays verification status on each room member tile using the same shield logic as elsewhere. Part of https://github.com/vector-im/riot-web/issues/11940pull/21833/head
parent
bae35c0859
commit
57c4293fc6
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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;
|
align-items: center;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
.mx_E2EIcon {
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
right: 7px;
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EntityTile:hover {
|
.mx_EntityTile:hover {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import E2EIcon from './E2EIcon';
|
||||||
|
|
||||||
const PRESENCE_CLASS = {
|
const PRESENCE_CLASS = {
|
||||||
"offline": "mx_EntityTile_offline",
|
"offline": "mx_EntityTile_offline",
|
||||||
|
@ -30,7 +31,6 @@ const PRESENCE_CLASS = {
|
||||||
"unavailable": "mx_EntityTile_unavailable",
|
"unavailable": "mx_EntityTile_unavailable",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
||||||
if (showPresence === false) {
|
if (showPresence === false) {
|
||||||
return 'mx_EntityTile_online_beenactive';
|
return 'mx_EntityTile_online_beenactive';
|
||||||
|
@ -69,6 +69,7 @@ const EntityTile = createReactClass({
|
||||||
suppressOnHover: PropTypes.bool,
|
suppressOnHover: PropTypes.bool,
|
||||||
showPresence: PropTypes.bool,
|
showPresence: PropTypes.bool,
|
||||||
subtextLabel: PropTypes.string,
|
subtextLabel: PropTypes.string,
|
||||||
|
e2eStatus: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -165,6 +166,12 @@ const EntityTile = createReactClass({
|
||||||
}[powerStatus];
|
}[powerStatus];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let e2eIcon;
|
||||||
|
const { e2eStatus } = this.props;
|
||||||
|
if (e2eStatus) {
|
||||||
|
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
|
||||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||||
|
@ -177,6 +184,7 @@ const EntityTile = createReactClass({
|
||||||
<div className="mx_EntityTile_avatar">
|
<div className="mx_EntityTile_avatar">
|
||||||
{ av }
|
{ av }
|
||||||
{ powerLabel }
|
{ powerLabel }
|
||||||
|
{ e2eIcon }
|
||||||
</div>
|
</div>
|
||||||
{ nameEl }
|
{ nameEl }
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
|
@ -189,5 +197,4 @@ const EntityTile = createReactClass({
|
||||||
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
||||||
EntityTile.POWER_STATUS_ADMIN = "admin";
|
EntityTile.POWER_STATUS_ADMIN = "admin";
|
||||||
|
|
||||||
|
|
||||||
export default EntityTile;
|
export default EntityTile;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 * as sdk from "../../../index";
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'MemberTile',
|
displayName: 'MemberTile',
|
||||||
|
@ -40,29 +41,101 @@ export default createReactClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
statusMessage: this.getStatusMessage(),
|
statusMessage: this.getStatusMessage(),
|
||||||
|
isRoomEncrypted: false,
|
||||||
|
e2eStatus: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
const cli = MatrixClientPeg.get();
|
||||||
return;
|
|
||||||
|
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) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
return;
|
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() {
|
componentWillUnmount() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const { user } = this.props.member;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
|
||||||
"User._unstable_statusMessage",
|
const devices = await cli.getStoredDevicesForUser(userId);
|
||||||
this._onStatusMessageCommitted,
|
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() {
|
getStatusMessage() {
|
||||||
|
@ -94,6 +167,12 @@ export default createReactClass({
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
nextState.isRoomEncrypted !== this.state.isRoomEncrypted ||
|
||||||
|
nextState.e2eStatus !== this.state.e2eStatus
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -153,14 +232,26 @@ export default createReactClass({
|
||||||
|
|
||||||
const powerStatus = powerStatusMap.get(powerLevel);
|
const powerStatus = powerStatusMap.get(powerLevel);
|
||||||
|
|
||||||
|
let e2eStatus;
|
||||||
|
if (this.state.isRoomEncrypted) {
|
||||||
|
e2eStatus = this.state.e2eStatus;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityTile {...this.props} presenceState={presenceState}
|
<EntityTile
|
||||||
|
{...this.props}
|
||||||
|
presenceState={presenceState}
|
||||||
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
||||||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
||||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
||||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
avatarJsx={av}
|
||||||
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence}
|
title={this.getPowerLabel()}
|
||||||
|
name={name}
|
||||||
|
powerStatus={powerStatus}
|
||||||
|
showPresence={this.props.showPresence}
|
||||||
subtextLabel={statusMessage}
|
subtextLabel={statusMessage}
|
||||||
|
e2eStatus={e2eStatus}
|
||||||
|
onClick={this.onClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue