diff --git a/src/Resend.js b/src/Resend.js
index 59a8bd4192..ad0f58eb9b 100644
--- a/src/Resend.js
+++ b/src/Resend.js
@@ -32,7 +32,8 @@ module.exports = {
if (err.name === "UnknownDeviceError") {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
Modal.createDialog(UnknownDeviceDialog, {
- devices: err.devices
+ devices: err.devices,
+ room: MatrixClientPeg.get().getRoom(event.getRoomId()),
}, "mx_Dialog_unknownDevice");
}
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index e5dba62ee7..d7d3e7bc7a 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -149,6 +149,23 @@ module.exports = {
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
},
+ getLocalSettings: function() {
+ var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
+ return JSON.parse(localSettingsString);
+ },
+
+ getLocalSetting: function(type, defaultValue = null) {
+ var settings = this.getLocalSettings();
+ return settings.hasOwnProperty(type) ? settings[type] : null;
+ },
+
+ setLocalSetting: function(type, value) {
+ var settings = this.getLocalSettings();
+ settings[type] = value;
+ // FIXME: handle errors
+ localStorage.setItem('mx_local_settings', JSON.stringify(settings));
+ },
+
isFeatureEnabled: function(feature: string): boolean {
// Disable labs for guests.
if (MatrixClientPeg.get().isGuest()) return false;
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 3d330e3649..ff19e7c239 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -59,6 +59,18 @@ const SETTINGS_LABELS = [
*/
];
+const CRYPTO_SETTINGS_LABELS = [
+ {
+ id: 'blacklistUnverifiedDevices',
+ label: 'Never send encrypted messages to unverified devices from this device',
+ },
+ // XXX: this is here for documentation; the actual setting is managed via RoomSettings
+ // {
+ // id: 'blacklistUnverifiedDevicesPerRoom'
+ // label: 'Never send encrypted messages to unverified devices in this room',
+ // }
+];
+
// Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
@@ -151,6 +163,8 @@ module.exports = React.createClass({
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
+
+ this._localSettings = UserSettingsStore.getLocalSettings();
},
componentDidMount: function() {
@@ -566,10 +580,34 @@ module.exports = React.createClass({
{exportButton}
{importButton}
+
+ { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
+
);
},
+ _renderLocalSetting: function(setting) {
+ const client = MatrixClientPeg.get();
+ return
+ {
+ UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
+ if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
+ client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
+ }
+ }
+ }
+ />
+
+ { setting.label }
+
+
;
+ },
+
_renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js
index be1138fe43..409852a2cc 100644
--- a/src/components/views/dialogs/UnknownDeviceDialog.js
+++ b/src/components/views/dialogs/UnknownDeviceDialog.js
@@ -19,19 +19,47 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
+function DeviceListEntry(props) {
+ const {userId, device} = props;
+
+ const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
+
+ return (
+
+
+ { device.deviceId }
+
+ { device.getDisplayName() }
+
+ );
+}
+
+DeviceListEntry.propTypes = {
+ userId: React.PropTypes.string.isRequired,
+
+ // deviceinfo
+ device: React.PropTypes.object.isRequired,
+};
+
+
function UserUnknownDeviceList(props) {
- const {userDevices} = props;
+ const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
-
- { deviceId } ( { userDevices[deviceId].getDisplayName() } )
- ,
+ ,
);
- return ;
+ return (
+
+ );
}
UserUnknownDeviceList.propTypes = {
+ userId: React.PropTypes.string.isRequired,
+
// map from deviceid -> deviceinfo
userDevices: React.PropTypes.object.isRequired,
};
@@ -43,7 +71,7 @@ function UnknownDeviceList(props) {
const userListEntries = Object.keys(devices).map((userId) =>
{ userId }:
-
+
,
);
@@ -60,6 +88,8 @@ export default React.createClass({
displayName: 'UnknownEventDialog',
propTypes: {
+ room: React.PropTypes.object.isRequired,
+
// map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
@@ -76,6 +106,34 @@ export default React.createClass({
},
render: function() {
+ const client = MatrixClientPeg.get();
+ const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
+ this.props.room.getBlacklistUnverifiedDevices();
+
+ let warning;
+ if (blacklistUnverified) {
+ warning = (
+
+ You are currently blacklisting unverified devices; to send
+ messages to these devices you must verify them.
+
+ );
+ } else {
+ warning = (
+
+
+ This means there is no guarantee that the devices
+ belong to the users they claim to.
+
+
+ We recommend you go through the verification process
+ for each device before continuing, but you can resend
+ the message without verifying if you prefer.
+
+
+ );
+ }
+
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
- This room contains devices which have not been
- verified.
-
- This means there is no guarantee that the devices belong
- to a rightful user of the room.
-
- We recommend you go through the verification process
- for each device before continuing, but you can resend
- the message without verifying if you prefer.
-
- Unknown devices:
+
+ This room contains unknown devices which have not been
+ verified.
+
+ { warning }
+ Unknown devices:
+
@@ -104,5 +158,7 @@ export default React.createClass({
);
+ // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
+ // It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
},
});
diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js
index da3975e4db..7e209232b6 100644
--- a/src/components/views/elements/DeviceVerifyButtons.js
+++ b/src/components/views/elements/DeviceVerifyButtons.js
@@ -27,6 +27,28 @@ export default React.createClass({
device: React.PropTypes.object.isRequired,
},
+ getInitialState: function() {
+ return {
+ device: this.props.device
+ };
+ },
+
+ componentWillMount: function() {
+ const cli = MatrixClientPeg.get();
+ cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ },
+
+ componentWillUnmount: function() {
+ const cli = MatrixClientPeg.get();
+ cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
+ },
+
+ onDeviceVerificationChanged: function(userId, deviceId) {
+ if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
+ this.setState({ device: MatrixClientPeg.get().getStoredDevice(userId, deviceId) });
+ }
+ },
+
onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
@@ -41,9 +63,9 @@ export default React.createClass({
- Device name: { this.props.device.getDisplayName() }
- Device ID: { this.props.device.deviceId}
- Device key: { this.props.device.getFingerprint() }
+ Device name: { this.state.device.getDisplayName() }
+ Device ID: { this.state.device.deviceId}
+ Device key: { this.state.device.getFingerprint() }
@@ -60,7 +82,7 @@ export default React.createClass({
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
- this.props.userId, this.props.device.deviceId, true
+ this.props.userId, this.state.device.deviceId, true
);
}
},
@@ -69,26 +91,26 @@ export default React.createClass({
onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(
- this.props.userId, this.props.device.deviceId, false
+ this.props.userId, this.state.device.deviceId, false
);
},
onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
- this.props.userId, this.props.device.deviceId, true
+ this.props.userId, this.state.device.deviceId, true
);
},
onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
- this.props.userId, this.props.device.deviceId, false
+ this.props.userId, this.state.device.deviceId, false
);
},
render: function() {
var blacklistButton = null, verifyButton = null;
- if (this.props.device.isBlocked()) {
+ if (this.state.device.isBlocked()) {
blacklistButton = (
@@ -104,7 +126,7 @@ export default React.createClass({
);
}
- if (this.props.device.isVerified()) {
+ if (this.state.device.isVerified()) {
verifyButton = (
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 80e41555a2..9aab174511 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -558,7 +558,7 @@ export default class MessageComposerInput extends React.Component {
dis.dispatch({
action: 'message_sent',
});
- }, onSendMessageFailed);
+ }, (e) => onSendMessageFailed(e, this.props.room));
this.setState({
editorState: this.createEditorState(),
diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js
index e8a4727fc6..b91e5c4391 100644
--- a/src/components/views/rooms/MessageComposerInputOld.js
+++ b/src/components/views/rooms/MessageComposerInputOld.js
@@ -29,11 +29,12 @@ var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
var MARKDOWN_ENABLED = true;
-export function onSendMessageFailed(err) {
+export function onSendMessageFailed(err, room) {
if (err.name === "UnknownDeviceError") {
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
Modal.createDialog(UnknownDeviceDialog, {
devices: err.devices,
+ room: room,
}, "mx_Dialog_unknownDevice");
}
dis.dispatch({
@@ -353,7 +354,7 @@ export default React.createClass({
dis.dispatch({
action: 'message_sent'
});
- }, onSendMessageFailed);
+ }, (e) => onSendMessageFailed(e, this.props.room));
this.refs.textarea.value = '';
this.resizeInput();
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index e14a929ebe..a23368f5e8 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
var ScalarAuthClient = require("../../../ScalarAuthClient");
var ScalarMessaging = require('../../../ScalarMessaging');
+var UserSettingsStore = require('../../../UserSettingsStore');
+
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
@@ -228,11 +230,13 @@ module.exports = React.createClass({
}
// encryption
- p = this.saveEncryption();
+ p = this.saveEnableEncryption();
if (!q.isFulfilled(p)) {
promises.push(p);
}
+ this.saveBlacklistUnverifiedDevicesPerRoom();
+
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
return promises;
},
@@ -252,7 +256,7 @@ module.exports = React.createClass({
return this.refs.url_preview_settings.saveSettings();
},
- saveEncryption: function() {
+ saveEnableEncryption: function() {
if (!this.refs.encrypt) { return q(); }
var encrypt = this.refs.encrypt.checked;
@@ -265,6 +269,29 @@ module.exports = React.createClass({
);
},
+ saveBlacklistUnverifiedDevicesPerRoom: function() {
+ if (!this.refs.blacklistUnverified) return;
+ if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
+ this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
+ }
+ },
+
+ _isRoomBlacklistUnverified: function() {
+ var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
+ if (blacklistUnverifiedDevicesPerRoom) {
+ return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
+ }
+ return false;
+ },
+
+ _setRoomBlacklistUnverified: function(value) {
+ var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
+ blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
+ UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
+
+ this.props.room.setBlacklistUnverifiedDevices(value);
+ },
+
_hasDiff: function(strA, strB) {
// treat undefined as an empty string because other components may blindly
// call setName("") when there has been no diff made to the name!
@@ -477,26 +504,42 @@ module.exports = React.createClass({
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
+ var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
+ var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
+
+ var settings =
+
+
+ Never send encrypted messages to unverified devices in this room from this device.
+ ;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return (
-
-
-
- Enable encryption (warning: cannot be disabled again!)
-
+
);
}
else {
return (
-
- { isEncrypted
- ?
- :
- }
- Encryption is { isEncrypted ? "" : "not " } enabled in this room.
-
+
+
+ { isEncrypted
+ ?
+ :
+ }
+ Encryption is { isEncrypted ? "" : "not " } enabled in this room.
+
+ { settings }
+
);
}
},