diff --git a/res/css/_components.scss b/res/css/_components.scss index e017d4b95a..ee55c000ff 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -107,6 +107,7 @@ @import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_Autocomplete.scss"; @import "./views/rooms/_AuxPanel.scss"; +@import "./views/rooms/_E2EIcon.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 2a9cc9f6c7..1054654670 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -121,7 +121,7 @@ limitations under the License. .mx_RoomStatusBar_connectionLostBar img { padding-left: 10px; - padding-right: 22px; + padding-right: 10px; vertical-align: middle; float: left; } diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss new file mode 100644 index 0000000000..cd577df87b --- /dev/null +++ b/res/css/views/rooms/_E2EIcon.scss @@ -0,0 +1,33 @@ +/* +Copyright 2019 New Vector 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. +*/ + +.mx_E2EIcon { + width: 25px; + height: 25px; + mask-repeat: no-repeat; + mask-position: center 0; + margin: 0 9px; +} + +.mx_E2EIcon_verified { + mask-image: url('$(res)/img/feather-icons/e2e/lock-verified.svg'); + background-color: $accent-color; +} + +.mx_E2EIcon_warning { + mask-image: url('$(res)/img/feather-icons/e2e/lock-warning.svg'); + background-color: $warning-color; +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index c920d6e390..7c9a6babea 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -281,9 +281,24 @@ limitations under the License. .mx_EventTile_e2eIcon { display: block; position: absolute; - top: 9px; + top: 8px; left: 46px; + width: 15px; + height: 15px; cursor: pointer; + mask-size: 14px; + mask-repeat: no-repeat; + mask-position: 0; +} + +.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { + mask-image: url('$(res)/img/feather-icons/e2e/warning.svg'); + background-color: $warning-color; +} + +.mx_EventTile_e2eIcon_unencrypted { + mask-image: url('$(res)/img/feather-icons/e2e/warning.svg'); + background-color: $composer-e2e-icon-color; } .mx_EventTile_e2eIcon_hidden { diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 6ccc4c7ae3..f780c50410 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -20,6 +20,25 @@ limitations under the License. align-items: start; } +.mx_MemberDeviceInfo_icon { + margin-top: 4px; + width: 12px; + height: 12px; + mask-repeat: no-repeat; +} +.mx_MemberDeviceInfo_icon_blacklisted { + mask-image: url('$(res)/img/feather-icons/e2e/blacklisted.svg'); + background-color: $warning-color; +} +.mx_MemberDeviceInfo_icon_verified { + mask-image: url('$(res)/img/feather-icons/e2e/verified.svg'); + background-color: $accent-color; +} +.mx_MemberDeviceInfo_icon_unverified { + mask-image: url('$(res)/img/feather-icons/e2e/warning.svg'); + background-color: $warning-color; +} + .mx_MemberDeviceInfo > .mx_DeviceVerifyButtons { display: flex; flex-direction: column; diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index 99771fece0..707c186518 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -26,6 +26,10 @@ limitations under the License. display: flex; } +.mx_MemberInfo_name > .mx_E2EIcon { + margin-left: 0; +} + .mx_MemberInfo_cancel { height: 16px; padding: 10px 15px; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index dc4aec691b..7ee7efcaff 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -23,6 +23,10 @@ limitations under the License. padding-left: 84px; } +.mx_MessageComposer_wrapper.mx_MessageComposer_hasE2EIcon { + padding-left: 109px; +} + .mx_MessageComposer_replaced_wrapper { margin-left: auto; margin-right: auto; @@ -71,9 +75,10 @@ limitations under the License. width: 100%; } -.mx_MessageComposer_e2eIcon { +.mx_MessageComposer_e2eIcon.mx_E2EIcon { position: absolute; left: 60px; + background-color: $composer-e2e-icon-color; } .mx_MessageComposer_noperm_error { diff --git a/res/img/feather-icons/e2e/blacklisted.svg b/res/img/feather-icons/e2e/blacklisted.svg new file mode 100644 index 0000000000..63897c2227 --- /dev/null +++ b/res/img/feather-icons/e2e/blacklisted.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12"> + <defs> + <path id="a" d="M5 10A5 5 0 1 0 5 0a5 5 0 0 0 0 10zM2.5 3.5h5a1.5 1.5 0 0 1 0 3h-5a1.5 1.5 0 0 1 0-3z"/> + </defs> + <use fill="#F56679" fill-rule="evenodd" stroke="#F56679" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" transform="translate(1 1)" xlink:href="#a"/> +</svg> diff --git a/res/img/feather-icons/e2e/lock-verified.svg b/res/img/feather-icons/e2e/lock-verified.svg new file mode 100644 index 0000000000..4cd4684ea2 --- /dev/null +++ b/res/img/feather-icons/e2e/lock-verified.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="22" height="25" viewBox="0 0 22 25"> + <g fill="none" fill-rule="evenodd" stroke="#7AC9A1" stroke-linecap="round" stroke-linejoin="round"> + <path stroke-width="2" d="M8.23 21.01l-5.233-.007a1.995 1.995 0 0 1-1.997-2V11a2 2 0 0 1 2-2h14c1.259 0 2 .939 2 1M5 9V6a5 5 0 1 1 10 0v3"/> + <path fill="#7AC9A1" d="M15.5 24s5.5-2.4 5.5-6v-4.2L15.5 12 10 13.8V18c0 3.6 5.5 6 5.5 6z"/> + </g> +</svg> diff --git a/res/img/feather-icons/e2e/lock-warning.svg b/res/img/feather-icons/e2e/lock-warning.svg new file mode 100644 index 0000000000..507c532f9d --- /dev/null +++ b/res/img/feather-icons/e2e/lock-warning.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="23" height="25" viewBox="0 0 23 25"> + <defs> + <path id="a" d="M15 23a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0-10.5a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-3 0v-3a1.5 1.5 0 0 1 1.5-1.5zm0 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/> + </defs> + <g fill="none" fill-rule="evenodd" stroke="#F56679" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)"> + <path stroke-width="2" d="M7.23 20.01l-5.233-.007a2 2 0 0 1-1.997-2V10a2 2 0 0 1 2-2h14c1.259 0 2 .939 2 1M4 8V5a5 5 0 1 1 10 0v3"/> + <use fill="#F56679" xlink:href="#a"/> + </g> +</svg> diff --git a/res/img/feather-icons/e2e/verified.svg b/res/img/feather-icons/e2e/verified.svg new file mode 100644 index 0000000000..f143f854e6 --- /dev/null +++ b/res/img/feather-icons/e2e/verified.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="11" height="12" viewBox="0 0 11 12"> + <path fill="#7AC9A1" fill-rule="evenodd" stroke="#7AC9A1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.5 11S10 9 10 6V2.5L5.5 1 1 2.5V6c0 3 4.5 5 4.5 5z"/> +</svg> diff --git a/res/img/feather-icons/e2e/warning.svg b/res/img/feather-icons/e2e/warning.svg new file mode 100644 index 0000000000..e6c246dba9 --- /dev/null +++ b/res/img/feather-icons/e2e/warning.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12"> + <defs> + <path id="a" d="M5 10A5 5 0 1 0 5 0a5 5 0 0 0 0 10zM5 .5A1.5 1.5 0 0 1 6.5 2v3a1.5 1.5 0 0 1-3 0V2A1.5 1.5 0 0 1 5 .5zm0 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/> + </defs> + <use fill="#F56679" fill-rule="evenodd" stroke="#F56679" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" transform="translate(1 1)" xlink:href="#a"/> +</svg> diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index f976180144..482b3c51cb 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -142,6 +142,8 @@ $roomheader-addroom-color: #91A1C0; $roomtopic-color: #9fa9ba; $eventtile-meta-color: $roomtopic-color; +$composer-e2e-icon-color: #c9ced6; + // ******************** $roomtile-name-color: #61708b; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 998325e1b7..8185ba0ace 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -134,6 +134,9 @@ $roomheader-color: $primary-fg-color; $roomheader-addroom-color: $primary-bg-color; $roomtopic-color: $settings-grey-fg-color; $eventtile-meta-color: $roomtopic-color; + +$composer-e2e-icon-color: #c9ced6; + // ******************** $roomtile-name-color: rgba(69, 69, 69, 0.8); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index b57bac805e..ab7f472931 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -290,7 +290,7 @@ module.exports = React.createClass({ } return <div className="mx_RoomStatusBar_connectionLostBar"> - <img src={require("../../../res/img/warning.svg")} width="24" height="23" title={_t("Warning")} alt="" /> + <img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" /> <div> <div className="mx_RoomStatusBar_connectionLostBar_title"> { title } @@ -309,7 +309,7 @@ module.exports = React.createClass({ if (this._shouldShowConnectionError()) { return ( <div className="mx_RoomStatusBar_connectionLostBar"> - <img src={require("../../../res/img/warning.svg")} width="24" height="23" title="/!\ " alt="/!\ " /> + <img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " /> <div> <div className="mx_RoomStatusBar_connectionLostBar_title"> { _t('Connectivity to the server has been lost.') } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 26ef3da739..b6513f0418 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -168,6 +168,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); + MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); @@ -457,6 +458,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); + MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -589,6 +591,10 @@ module.exports = React.createClass({ this._updatePreviewUrlVisibility(room); } + if (ev.getType() === "m.room.encryption") { + this._updateE2EStatus(room); + } + // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; @@ -642,6 +648,7 @@ module.exports = React.createClass({ this._updatePreviewUrlVisibility(room); this._loadMembersIfJoined(room); this._calculateRecommendedVersion(room); + this._updateE2EStatus(room); }, _calculateRecommendedVersion: async function(room) { @@ -733,6 +740,23 @@ module.exports = React.createClass({ }); }, + onDeviceVerificationChanged: function(userId, device) { + const room = this.state.room; + if (!room.currentState.getMember(userId)) { + return; + } + this._updateE2EStatus(room); + }, + + _updateE2EStatus: function(room) { + if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { + return; + } + room.hasUnverifiedDevices().then((hasUnverifiedDevices) => { + this.setState({e2eStatus: hasUnverifiedDevices ? "warning" : "verified"}); + }); + }, + updateTint: function() { const room = this.state.room; if (!room) return; @@ -1575,6 +1599,7 @@ module.exports = React.createClass({ room={this.state.room} oobData={this.props.oobData} collapsedRhs={this.props.collapsedRhs} + e2eStatus={this.state.e2eStatus} /> <div className="mx_RoomView_body"> <div className="mx_RoomView_auxPanel"> @@ -1622,6 +1647,7 @@ module.exports = React.createClass({ ref="header" room={this.state.room} collapsedRhs={this.props.collapsedRhs} + e2eStatus={this.state.e2eStatus} /> <div className="mx_RoomView_body"> <div className="mx_RoomView_auxPanel"> @@ -1767,6 +1793,7 @@ module.exports = React.createClass({ disabled={this.props.disabled} showApps={this.state.showApps} uploadAllowed={this.isFileUploadAllowed} + e2eStatus={this.state.e2eStatus} />; } @@ -1917,6 +1944,7 @@ module.exports = React.createClass({ onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} + e2eStatus={this.state.e2eStatus} /> <MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}> <div className={fadableSectionClasses}> diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js new file mode 100644 index 0000000000..30891e84b7 --- /dev/null +++ b/src/components/views/rooms/E2EIcon.js @@ -0,0 +1,39 @@ +/* +Copyright 2019 New Vector 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 classNames from 'classnames'; +import { _t } from '../../../languageHandler'; + +export default function(props) { + const isWarning = props.status === "warning"; + const isVerified = props.status === "verified"; + const e2eIconClasses = classNames({ + mx_E2EIcon: true, + mx_E2EIcon_warning: isWarning, + mx_E2EIcon_verified: isVerified, + }, props.className); + let e2eTitle; + if (isWarning) { + e2eTitle = props.isUser ? + _t("Some devices for this user are not trusted") : + _t("Some devices in this encrypted room are not trusted"); + } else if (isVerified) { + e2eTitle = props.isUser ? + _t("All devices for this user are trusted") : + _t("All devices in this encrypted room are trusted"); + } + return (<div className={e2eIconClasses} title={e2eTitle} />); +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index acb122ad4e..e4a6695ff5 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -459,17 +459,21 @@ module.exports = withMatrixClient(React.createClass({ // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { - return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />; + if (this.state.verified) { + return; // no icon for verified + } else { + return (<E2ePadlockUnverified {...props} />); + } } if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { - return <E2ePadlockEncrypting {...props} />; + return; } if (ev.status === EventStatus.NOT_SENT) { - return <E2ePadlockNotSent {...props} />; + return; } // if the event is not encrypted, but it's an e2e room, show the open padlock return <E2ePadlockUnencrypted {...props} />; @@ -767,57 +771,29 @@ module.exports.haveTileForEvent = function(e) { function E2ePadlockUndecryptable(props) { return ( - <E2ePadlock alt={_t("Undecryptable")} - src={require("../../../../res/img/e2e-blocked.svg")} width="12" height="12" - style={{ marginLeft: "-1px" }} {...props} /> - ); -} - -function E2ePadlockEncrypting(props) { - return ( - <E2ePadlock alt={_t("Encrypting")} - src={require("../../../../res/img/e2e-encrypting.svg")} width="10" height="12" - {...props} /> - ); -} - -function E2ePadlockNotSent(props) { - return ( - <E2ePadlock alt={_t("Encrypted, not sent")} - src={require("../../../../res/img/e2e-not_sent.svg")} width="10" height="12" - {...props} /> - ); -} - -function E2ePadlockVerified(props) { - return ( - <E2ePadlock alt={_t("Encrypted by a verified device")} - src={require("../../../../res/img/e2e-verified.svg")} width="10" height="12" - {...props} /> + <E2ePadlock title={_t("Undecryptable")} icon="undecryptable" /> ); } function E2ePadlockUnverified(props) { return ( - <E2ePadlock alt={_t("Encrypted by an unverified device")} - src={require("../../../../res/img/e2e-warning.svg")} width="15" height="12" - style={{ marginLeft: "-2px" }} {...props} /> + <E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" /> ); } function E2ePadlockUnencrypted(props) { return ( - <E2ePadlock alt={_t("Unencrypted message")} - src={require("../../../../res/img/e2e-unencrypted.svg")} width="12" height="12" - {...props} /> + <E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" /> ); } function E2ePadlock(props) { if (SettingsStore.getValue("alwaysShowEncryptionIcons")) { - return <img className="mx_EventTile_e2eIcon" {...props} />; + return <div + className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`} + title={props.title} onClick={props.onClick} />; } else { - return <img className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" {...props} />; + return <div className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" onClick={props.onClick} />; } } diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index b9c276f0d1..67f99e4d1d 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -18,32 +18,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import classNames from 'classnames'; export default class MemberDeviceInfo extends React.Component { render() { - let indicator = null; const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); - - if (this.props.device.isBlocked()) { - indicator = ( - <div className="mx_MemberDeviceInfo_blacklisted"> - <img src={require("../../../../res/img/e2e-blocked.svg")} width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")} /> - </div> - ); - } else if (this.props.device.isVerified()) { - indicator = ( - <div className="mx_MemberDeviceInfo_verified"> - <img src={require("../../../../res/img/e2e-verified.svg")} width="10" height="12" alt={_t("Verified")} /> - </div> - ); - } else { - indicator = ( - <div className="mx_MemberDeviceInfo_unverified"> - <img src={require("../../../../res/img/e2e-warning.svg")} width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")} /> - </div> - ); - } - + const iconClasses = classNames({ + mx_MemberDeviceInfo_icon: true, + mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(), + mx_MemberDeviceInfo_icon_verified: this.props.device.isVerified(), + mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(), + }); + const indicator = (<div className={iconClasses} />); const deviceName = this.props.device.ambiguous ? (this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" : this.props.device.getDisplayName(); @@ -52,10 +38,10 @@ export default class MemberDeviceInfo extends React.Component { return ( <div className="mx_MemberDeviceInfo" title={_t("device id: ") + this.props.device.deviceId} > + { indicator } <div className="mx_MemberDeviceInfo_deviceInfo"> <div className="mx_MemberDeviceInfo_deviceId"> { deviceName } - { indicator } </div> </div> <DeviceVerifyButtons userId={this.props.userId} device={this.props.device} /> diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 9859861870..5df0da7491 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -43,6 +43,7 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; import MultiInviter from "../../../utils/MultiInviter"; import SettingsStore from "../../../settings/SettingsStore"; +import E2EIcon from "./E2EIcon"; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -153,11 +154,19 @@ module.exports = withMatrixClient(React.createClass({ // Promise.resolve to handle transition from static result to promise; can be removed // in future Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => { - this.setState({devices: devices}); + this.setState({ + devices: devices, + e2eStatus: this._getE2EStatus(devices), + }); }); } }, + _getE2EStatus: function(devices) { + const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); + return hasUnverifiedDevice ? "warning" : "verified"; + }, + onRoom: function(room) { this.forceUpdate(); }, @@ -234,8 +243,13 @@ module.exports = withMatrixClient(React.createClass({ // we got cancelled - presumably a different user now return; } + self._disambiguateDevices(devices); - self.setState({devicesLoading: false, devices: devices}); + self.setState({ + devicesLoading: false, + devices: devices, + e2eStatus: self._getE2EStatus(devices), + }); }, function(err) { console.log("Error downloading devices", err); self.setState({devicesLoading: false}); @@ -965,6 +979,7 @@ module.exports = withMatrixClient(React.createClass({ <AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} /> </AccessibleButton> + { this.state.e2eStatus ? <E2EIcon status={this.state.e2eStatus} isUser={true} /> : undefined } <EmojiText element="h2">{ memberName }</EmojiText> </div> { avatarElement } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 7681c2dc13..7117825d76 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -26,6 +26,9 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../matrix-to'; +import classNames from 'classnames'; + +import E2EIcon from './E2EIcon'; const formatButtonList = [ _td("bold"), @@ -316,25 +319,14 @@ export default class MessageComposer extends React.Component { ); } - let e2eImg; let e2eTitle; let e2eClass; - const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); - if (roomIsEncrypted) { - // FIXME: show a /!\ if there are untrusted devices in the room... - e2eImg = require("../../../../res/img/e2e-verified.svg"); - e2eTitle = _t('Encrypted room'); - e2eClass = 'mx_MessageComposer_e2eIcon'; - } else { - e2eImg = require("../../../../res/img/e2e-unencrypted.svg"); - e2eTitle = _t('Unencrypted room'); - e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor'; + if (this.props.e2eStatus) { + controls.push(<E2EIcon + status={this.props.e2eStatus} + key="e2eIcon" + className="mx_MessageComposer_e2eIcon" /> + ); } - controls.push( - <img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12" - alt={e2eTitle} title={e2eTitle} - />, - ); - let callButton; let videoCallButton; let hangupButton; @@ -413,6 +405,7 @@ export default class MessageComposer extends React.Component { key="controls_formatting" /> ) : null; + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); let placeholderText; if (this.state.isQuoting) { if (roomIsEncrypted) { @@ -509,9 +502,13 @@ export default class MessageComposer extends React.Component { </div>; } + const wrapperClasses = classNames({ + mx_MessageComposer_wrapper: true, + mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus, + }); return ( <div className="mx_MessageComposer"> - <div className="mx_MessageComposer_wrapper"> + <div className={wrapperClasses}> <div className="mx_MessageComposer_row"> { controls } </div> diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 3cdb9237be..cee1814011 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -33,6 +33,7 @@ import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; +import E2EIcon from './E2EIcon'; linkifyMatrix(linkify); @@ -52,6 +53,7 @@ module.exports = React.createClass({ onSearchClick: PropTypes.func, onLeaveClick: PropTypes.func, onCancelClick: PropTypes.func, + e2eStatus: PropTypes.string, }, getDefaultProps: function() { @@ -237,6 +239,10 @@ module.exports = React.createClass({ ); } + const e2eIcon = this.props.e2eStatus ? + <E2EIcon status={this.props.e2eStatus} /> : + undefined; + if (this.props.onCancelClick) { cancelButton = <CancelButton onClick={this.props.onCancelClick} />; } @@ -413,6 +419,7 @@ module.exports = React.createClass({ <div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}> <div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_avatar">{ roomAvatar }</div> + { e2eIcon } { name } { topicElement } { spinner } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8fe1b926f9..f1d7296d03 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -571,6 +571,10 @@ " (unsupported)": " (unsupported)", "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", + "Some devices for this user are not trusted": "Some devices for this user are not trusted", + "Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted", + "All devices for this user are trusted": "All devices for this user are trusted", + "All devices in this encrypted room are trusted": "All devices in this encrypted room are trusted", "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", @@ -582,16 +586,10 @@ "Key request sent.": "Key request sent.", "<requestLink>Re-request encryption keys</requestLink> from your other devices.": "<requestLink>Re-request encryption keys</requestLink> from your other devices.", "Undecryptable": "Undecryptable", - "Encrypting": "Encrypting", - "Encrypted, not sent": "Encrypted, not sent", - "Encrypted by a verified device": "Encrypted by a verified device", "Encrypted by an unverified device": "Encrypted by an unverified device", "Unencrypted message": "Unencrypted message", "Please select the destination room for this message": "Please select the destination room for this message", "Scroll to bottom of page": "Scroll to bottom of page", - "Blacklisted": "Blacklisted", - "Verified": "Verified", - "Unverified": "Unverified", "device id: ": "device id: ", "Disinvite": "Disinvite", "Kick": "Kick", @@ -643,8 +641,6 @@ "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?", "The following files cannot be uploaded:": "The following files cannot be uploaded:", "Upload Files": "Upload Files", - "Encrypted room": "Encrypted room", - "Unencrypted room": "Unencrypted room", "Hangup": "Hangup", "Voice call": "Voice call", "Video call": "Video call", @@ -1437,6 +1433,7 @@ "Users": "Users", "unknown device": "unknown device", "NOT verified": "NOT verified", + "Blacklisted": "Blacklisted", "verified": "verified", "Name": "Name", "Verification": "Verification",