diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 0d92247735..6bfcd437c1 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -21,8 +21,10 @@ limitations under the License. .mx_E2EIcon { margin: 0; position: absolute; - bottom: 0; - right: -5px; + bottom: -1px; + right: -2px; + height: 10px; + width: 10px } } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index db2c09f6f1..376f4370e3 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -98,6 +98,19 @@ limitations under the License. z-index: 2; } +// Note we match .mx_E2EIcon to make sure this matches more tightly than just +// .mx_E2EIcon on its own +.mx_RoomTile_e2eIcon.mx_E2EIcon { + height: 10px; + width: 10px; + display: block; + position: absolute; + bottom: -1px; + right: -2px; + z-index: 1; + margin: 0; +} + .mx_RoomTile_name { font-size: 14px; padding: 0 6px; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3f438ea909..6e20b2c8f2 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -796,6 +796,7 @@ export default createReactClass({ return; } + // Duplication between here and _updateE2eStatus in RoomTile /* At this point, the user has encryption on and cross-signing on */ const e2eMembers = await room.getEncryptionTargetMembers(); const verified = []; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f4f5fa10fc..0b50d85ff6 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -33,6 +33,9 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; +import E2EIcon from './E2EIcon'; +// eslint-disable-next-line camelcase +import rate_limited_func from '../../../ratelimitedfunc'; export default createReactClass({ displayName: 'RoomTile', @@ -70,6 +73,7 @@ export default createReactClass({ notificationCount: this.props.room.getUnreadNotificationCount(), selected: this.props.room.roomId === RoomViewStore.getRoomId(), statusMessage: this._getStatusMessage(), + e2eStatus: null, }); }, @@ -102,6 +106,83 @@ export default createReactClass({ return statusUser._unstable_statusMessage; }, + onRoomStateMember: function(ev, state, member) { + // we only care about leaving users + // because trust state will change if someone joins a megolm session anyway + if (member.membership !== "leave") { + return; + } + // ignore members in other rooms + if (member.roomId !== this.props.room.roomId) { + return; + } + + this._updateE2eStatus(); + }, + + onUserVerificationChanged: function(userId, _trustStatus) { + if (!this.props.room.getMember(userId)) { + // Not in this room + return; + } + this._updateE2eStatus(); + }, + + onRoomTimeline: function(ev, room) { + if (!room) return; + if (room.roomId != this.props.room.roomId) return; + if (ev.getType() !== "m.room.encryption") return; + MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); + this.onFindingRoomToBeEncrypted(); + }, + + onFindingRoomToBeEncrypted: function() { + const cli = MatrixClientPeg.get(); + cli.on("RoomState.members", this.onRoomStateMember); + cli.on("userTrustStatusChanged", this.onUserVerificationChanged); + + this._updateE2eStatus(); + }, + + _updateE2eStatus: async function() { + const cli = MatrixClientPeg.get(); + if (!cli.isRoomEncrypted(this.props.room.roomId)) { + return; + } + if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { + return; + } + + // Duplication between here and _updateE2eStatus in RoomView + const e2eMembers = await this.props.room.getEncryptionTargetMembers(); + const verified = []; + const unverified = []; + e2eMembers.map(({userId}) => userId) + .filter((userId) => userId !== cli.getUserId()) + .forEach((userId) => { + (cli.checkUserTrust(userId).isCrossSigningVerified() ? + verified : unverified).push(userId); + }); + + /* Check all verified user devices. */ + for (const userId of verified) { + const devices = await cli.getStoredDevicesForUser(userId); + const allDevicesVerified = devices.every(({deviceId}) => { + return cli.checkDeviceTrust(userId, deviceId).isVerified(); + }); + if (!allDevicesVerified) { + this.setState({ + e2eStatus: "warning", + }); + return; + } + } + + this.setState({ + e2eStatus: unverified.length === 0 ? "verified" : "normal", + }); + }, + onRoomName: function(room) { if (room !== this.props.room) return; this.setState({ @@ -151,10 +232,19 @@ export default createReactClass({ }, componentDidMount: function() { + /* We bind here rather than in the definition because otherwise we wind up with the + method only being callable once every 500ms across all instances, which would be wrong */ + this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500); + const cli = MatrixClientPeg.get(); cli.on("accountData", this.onAccountData); cli.on("Room.name", this.onRoomName); cli.on("RoomState.events", this.onJoinRule); + if (cli.isRoomEncrypted(this.props.room.roomId)) { + this.onFindingRoomToBeEncrypted(); + } else { + cli.on("Room.timeline", this.onRoomTimeline); + } ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange); this.dispatcherRef = dis.register(this.onAction); @@ -172,6 +262,9 @@ export default createReactClass({ MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); cli.removeListener("RoomState.events", this.onJoinRule); + cli.removeListener("RoomState.members", this.onRoomStateMember); + cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); + cli.removeListener("Room.timeline", this.onRoomTimeline); } ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); dis.unregister(this.dispatcherRef); @@ -433,6 +526,12 @@ export default createReactClass({ privateIcon =
; } + let e2eIcon = null; + // For now, skip the icon for DMs. Possibly we want to move the DM icon elsewhere? + if (!dmUserId && this.state.e2eStatus) { + e2eIcon = ; + } + return {({onFocus, isActive, ref}) => @@ -453,6 +552,7 @@ export default createReactClass({
{ dmIndicator } + { e2eIcon }
{ privateIcon }