diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss
index 2b6b31acb4..a2867de3a7 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 {
@@ -30,7 +40,7 @@ limitations under the License.
content: "";
position: absolute;
top: calc(50% - 8px); // center
- right: 10px;
+ right: -8px;
mask: url('$(res)/img/member_chevron.png');
mask-repeat: no-repeat;
width: 16px;
@@ -64,14 +74,6 @@ limitations under the License.
position: relative;
}
-.mx_EntityTile_power {
- position: absolute;
- width: 16px;
- height: 17px;
- top: 0px;
- right: 6px;
-}
-
.mx_EntityTile_name,
.mx_GroupRoomTile_name {
flex: 1 1 0;
@@ -83,6 +85,7 @@ limitations under the License.
.mx_EntityTile_details {
overflow: hidden;
+ flex: 1;
}
.mx_EntityTile_ellipsis .mx_EntityTile_name {
@@ -112,10 +115,6 @@ limitations under the License.
opacity: 0.25;
}
-.mx_EntityTile:not(.mx_EntityTile_noHover):hover .mx_EntityTile_name {
- font-size: 13px;
-}
-
.mx_EntityTile_subtext {
font-size: 11px;
opacity: 0.5;
@@ -123,3 +122,17 @@ limitations under the License.
white-space: nowrap;
text-overflow: clip;
}
+
+.mx_EntityTile_power {
+ padding-inline-start: 6px;
+ font-size: 10px;
+ color: $notice-secondary-color;
+ max-width: 6em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mx_EntityTile:hover .mx_EntityTile_power {
+ display: none;
+}
diff --git a/res/img/admin.svg b/res/img/admin.svg
deleted file mode 100644
index 7ea7459304..0000000000
--- a/res/img/admin.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
diff --git a/res/img/mod.svg b/res/img/mod.svg
deleted file mode 100644
index 847baf98f9..0000000000
--- a/res/img/mod.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
diff --git a/src/Entities.js b/src/Entities.js
deleted file mode 100644
index 872a837f3a..0000000000
--- a/src/Entities.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
-Copyright 2015, 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.
-*/
-
-import * as sdk from './index';
-
-function isMatch(query, name, uid) {
- query = query.toLowerCase();
- name = name.toLowerCase();
- uid = uid.toLowerCase();
-
- // direct prefix matches
- if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
- return true;
- }
-
- // strip @ on uid and try matching again
- if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) {
- return true;
- }
-
- // split spaces in name and try matching constituent parts
- const parts = name.split(" ");
- for (let i = 0; i < parts.length; i++) {
- if (parts[i].indexOf(query) === 0) {
- return true;
- }
- }
- return false;
-}
-
-/*
- * Converts various data models to Entity objects.
- *
- * Entity objects provide an interface for UI components to use to display
- * members in a data-agnostic way. This means they don't need to care if the
- * underlying data model is a RoomMember, User or 3PID data structure, it just
- * cares about rendering.
- */
-
-class Entity {
- constructor(model) {
- this.model = model;
- }
-
- getJsx() {
- return null;
- }
-
- matches(queryString) {
- return false;
- }
-}
-
-class MemberEntity extends Entity {
- getJsx() {
- const MemberTile = sdk.getComponent("rooms.MemberTile");
- return (
-
- );
- }
-
- matches(queryString) {
- return isMatch(queryString, this.model.name, this.model.userId);
- }
-}
-
-class UserEntity extends Entity {
- constructor(model, showInviteButton, inviteFn) {
- super(model);
- this.showInviteButton = Boolean(showInviteButton);
- this.inviteFn = inviteFn;
- this.onClick = this.onClick.bind(this);
- }
-
- onClick() {
- if (this.inviteFn) {
- this.inviteFn(this.model.userId);
- }
- }
-
- getJsx() {
- const UserTile = sdk.getComponent("rooms.UserTile");
- return (
-
- );
- }
-
- matches(queryString) {
- const name = this.model.displayName || this.model.userId;
- return isMatch(queryString, name, this.model.userId);
- }
-}
-
-export function newEntity(jsx, matchFn) {
- const entity = new Entity();
- entity.getJsx = function() {
- return jsx;
- };
- entity.matches = matchFn;
- return entity;
-}
-
-/**
- * @param {RoomMember[]} members
- * @return {Entity[]}
- */
-export function fromRoomMembers(members) {
- return members.map(function(m) {
- return new MemberEntity(m);
- });
-}
-
-/**
- * @param {User[]} users
- * @param {boolean} showInviteButton
- * @param {Function} inviteFn Called with the user ID.
- * @return {Entity[]}
- */
-export function fromUsers(users, showInviteButton, inviteFn) {
- return users.map(function(u) {
- return new UserEntity(u, showInviteButton, inviteFn);
- });
-}
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js
index 57db1ac240..fd92cb8e57 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() {
@@ -156,18 +157,20 @@ const EntityTile = createReactClass({
);
}
- let power;
+ let powerLabel;
const powerStatus = this.props.powerStatus;
if (powerStatus) {
- const src = {
- [EntityTile.POWER_STATUS_MODERATOR]: require("../../../../res/img/mod.svg"),
- [EntityTile.POWER_STATUS_ADMIN]: require("../../../../res/img/admin.svg"),
- }[powerStatus];
- const alt = {
- [EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
+ const powerText = {
+ [EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
- power = ;
+ powerLabel =
{powerText}
;
+ }
+
+ let e2eIcon;
+ const { e2eStatus } = this.props;
+ if (e2eStatus) {
+ e2eIcon = ;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
@@ -181,9 +184,10 @@ const EntityTile = createReactClass({
onClick={this.props.onClick}>
{ av }
- { power }
+ { e2eIcon }
{ nameEl }
+ { powerLabel }
{ inviteButton }
@@ -194,5 +198,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 (
-
);
},
diff --git a/src/components/views/rooms/UserTile.js b/src/components/views/rooms/UserTile.js
deleted file mode 100644
index 01bbe7d4d1..0000000000
--- a/src/components/views/rooms/UserTile.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 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.
-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.
-*/
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
-import * as Avatar from '../../../Avatar';
-import * as sdk from "../../../index";
-
-export default createReactClass({
- displayName: 'UserTile',
-
- propTypes: {
- user: PropTypes.any.isRequired, // User
- },
-
- render: function() {
- const EntityTile = sdk.getComponent("rooms.EntityTile");
- const user = this.props.user;
- const name = user.displayName || user.userId;
- let active = -1;
-
- // FIXME: make presence data update whenever User.presence changes...
- active = user.lastActiveAgo ?
- (Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1;
-
- const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
- const avatarJsx = (
-
- );
-
- return (
-
- );
- },
-});
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 3c7b515833..aa73777b3c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -909,6 +909,7 @@
"Some sessions in this encrypted room are not trusted": "Some sessions in this encrypted room are not trusted",
"All sessions in this encrypted room are trusted": "All sessions in this encrypted room are trusted",
"Edit message": "Edit message",
+ "Mod": "Mod",
"This event could not be displayed": "This event could not be displayed",
"%(senderName)s sent an image": "%(senderName)s sent an image",
"%(senderName)s sent a video": "%(senderName)s sent a video",
diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js
index 867190f6f4..39720177cc 100644
--- a/test/components/views/groups/GroupMemberList-test.js
+++ b/test/components/views/groups/GroupMemberList-test.js
@@ -112,7 +112,9 @@ describe("GroupMemberList", function() {
const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined");
const memberListElement = ReactDOM.findDOMNode(memberList);
expect(memberListElement).toBeTruthy();
- expect(memberListElement.textContent).toBe("Test");
+ const userNameElement = memberListElement.querySelector(".mx_EntityTile_name");
+ expect(userNameElement).toBeTruthy();
+ expect(userNameElement.textContent).toBe("Test");
});
httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse);