diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index fbac1e932a..d292c729dd 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -367,6 +367,11 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
opacity: 1;
}
+.mx_EventTile_e2eIcon_unknown {
+ background-image: url('$(res)/img/e2e/warning.svg');
+ opacity: 1;
+}
+
.mx_EventTile_e2eIcon_unencrypted {
background-image: url('$(res)/img/e2e/warning.svg');
opacity: 1;
@@ -415,7 +420,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
}
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
-.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
+.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
+.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: 60px;
}
@@ -427,8 +433,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
border-left: $e2e-unverified-color 5px solid;
}
+.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
+ border-left: $e2e-unknown-color 5px solid;
+}
+
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
-.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line {
+.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
+.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: 78px;
}
@@ -439,14 +450,16 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
-.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp {
+.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
+.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: 3px;
width: auto;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
-.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon {
+.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
+.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
display: block;
left: 41px;
}
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 288fb3cadc..c868c81549 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -224,6 +224,7 @@ $copy-button-url: "$(res)/img/icon_copy_message.svg";
// e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
+$e2e-unknown-color: #e8bf37;
$e2e-unverified-color: #e8bf37;
$e2e-warning-color: #ba6363;
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index dce4dc8a93..bcd32d2c9c 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -66,6 +66,12 @@ const stateEventTileTypes = {
'm.room.related_groups': 'messages.TextualEvent',
};
+const E2E_STATE = {
+ VERIFIED: "verified",
+ WARNING: "warning",
+ UNKNOWN: "unknown",
+};
+
// Add all the Mjolnir stuff to the renderer
for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent';
@@ -235,6 +241,7 @@ export default createReactClass({
this._suppressReadReceiptAnimation = false;
const client = this.context;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ client.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
@@ -260,6 +267,7 @@ export default createReactClass({
componentWillUnmount: function() {
const client = this.context;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
if (this.props.showReactions) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
@@ -282,18 +290,40 @@ export default createReactClass({
}
},
+ onUserVerificationChanged: function(userId, _trustStatus) {
+ if (userId === this.props.mxEvent.getSender()) {
+ this._verifyEvent(this.props.mxEvent);
+ }
+ },
+
_verifyEvent: async function(mxEvent) {
if (!mxEvent.isEncrypted()) {
return;
}
+ // If we directly trust the device, short-circuit here
const verified = await this.context.isEventSenderVerified(mxEvent);
+ if (verified) {
+ this.setState({
+ verified: E2E_STATE.VERIFIED,
+ }, () => {
+ // Decryption may have caused a change in size
+ this.props.onHeightChanged();
+ });
+ return;
+ }
+
+ const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent);
+ if (!eventSenderTrust) {
+ this.setState({
+ verified: E2E_STATE.UNKNOWN,
+ }, this.props.onHeightChanged); // Decryption may have cause a change in size
+ return;
+ }
+
this.setState({
- verified: verified,
- }, () => {
- // Decryption may have caused a change in size
- this.props.onHeightChanged();
- });
+ verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
+ }, this.props.onHeightChanged); // Decryption may have caused a change in size
},
_propsEqual: function(objA, objB) {
@@ -473,8 +503,10 @@ export default createReactClass({
// event is encrypted, display padlock corresponding to whether or not it is verified
if (ev.isEncrypted()) {
- if (this.state.verified) {
+ if (this.state.verified === E2E_STATE.VERIFIED) {
return; // no icon for verified
+ } else if (this.state.verified === E2E_STATE.UNKNOWN) {
+ return ();
} else {
return ();
}
@@ -604,8 +636,9 @@ export default createReactClass({
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
- mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
- mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
+ mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
+ mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
+ mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
@@ -901,6 +934,12 @@ function E2ePadlockUnencrypted(props) {
);
}
+function E2ePadlockUnknown(props) {
+ return (
+
+ );
+}
+
class E2ePadlock extends React.Component {
static propTypes = {
icon: PropTypes.string.isRequired,
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f0eab6b12d..e6a0792ae5 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -905,6 +905,7 @@
"This message cannot be decrypted": "This message cannot be decrypted",
"Encrypted by an unverified device": "Encrypted by an unverified device",
"Unencrypted": "Unencrypted",
+ "Encrypted by a deleted device": "Encrypted by a deleted device",
"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",
"Close preview": "Close preview",