Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

UI for blacklisting unverified devices per-room & globally #636

Merged
merged 14 commits into from
Feb 3, 2017
Merged
3 changes: 2 additions & 1 deletion src/Resend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
17 changes: 17 additions & 0 deletions src/UserSettingsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,23 @@ module.exports = {
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
},

getLocalSettings: function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to explain wtf a 'local setting' is. Turns out, it's a setting which is persisted in localstorage.

var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ought to handle localStorage === undefined here

return JSON.parse(localSettingsString);
},

getLocalSetting: function(type, defaultValue = null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type? why type?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments on what the behaviour is if the setting is absent would be nice.

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;
Expand Down
38 changes: 38 additions & 0 deletions src/components/structures/UserSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -151,6 +163,8 @@ module.exports = React.createClass({
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;

this._localSettings = UserSettingsStore.getLocalSettings();
},

componentDidMount: function() {
Expand Down Expand Up @@ -566,10 +580,34 @@ module.exports = React.createClass({
{exportButton}
{importButton}
</div>
<div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
</div>
</div>
);
},

_renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is crying out to be made a separate react component.

type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
e => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
}
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},

_renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
Expand Down
90 changes: 73 additions & 17 deletions src/components/views/dialogs/UnknownDeviceDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<li>
<DeviceVerifyButtons device={ device } userId={ userId } />
{ device.deviceId }
<br/>
{ device.getDisplayName() }
</li>
);
}

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) =>
<li key={ deviceId }>
{ deviceId } ( { userDevices[deviceId].getDisplayName() } )
</li>,
<DeviceListEntry key={ deviceId } userId={ userId }
device={ userDevices[deviceId] } />,
);

return <ul>{deviceListEntries}</ul>;
return (
<ul className="mx_UnknownDeviceDialog_deviceList">
{deviceListEntries}
</ul>
);
}

UserUnknownDeviceList.propTypes = {
userId: React.PropTypes.string.isRequired,

// map from deviceid -> deviceinfo
userDevices: React.PropTypes.object.isRequired,
};
Expand All @@ -43,7 +71,7 @@ function UnknownDeviceList(props) {
const userListEntries = Object.keys(devices).map((userId) =>
<li key={ userId }>
<p>{ userId }:</p>
<UserUnknownDeviceList userDevices={devices[userId]} />
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
</li>,
);

Expand All @@ -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,
Expand All @@ -76,24 +106,48 @@ export default React.createClass({
},

render: function() {
const client = MatrixClientPeg.get();
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
this.props.room.getBlacklistUnverifiedDevices();

let warning;
if (blacklistUnverified) {
warning = (
<h4>
You are currently blacklisting unverified devices; to send
messages to these devices you must verify them.
</h4>
);
} else {
warning = (
<div>
<p>
This means there is no guarantee that the devices
belong to the users they claim to.
</p>
<p>
We recommend you go through the verification process
for each device before continuing, but you can resend
the message without verifying if you prefer.
</p>
</div>
);
}

const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title='Room contains unknown devices'
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>This room contains devices which have not been
verified.</h4>
<p>
This means there is no guarantee that the devices belong
to a rightful user of the room.
</p><p>
We recommend you go through the verification process
for each device before continuing, but you can resend
the message without verifying if you prefer.
</p>
<p>Unknown devices:</p>
<h4>
This room contains unknown devices which have not been
verified.
</h4>
{ warning }
Unknown devices:

<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
Expand All @@ -104,5 +158,7 @@ export default React.createClass({
</div>
</BaseDialog>
);
// 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...
},
});
40 changes: 31 additions & 9 deletions src/components/views/elements/DeviceVerifyButtons.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -41,9 +63,9 @@ export default React.createClass({
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
</ul>
</div>
<p>
Expand All @@ -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
);
}
},
Expand All @@ -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 = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
Expand All @@ -104,7 +126,7 @@ export default React.createClass({
);
}

if (this.props.device.isVerified()) {
if (this.state.device.isVerified()) {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/MessageComposerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
5 changes: 3 additions & 2 deletions src/components/views/rooms/MessageComposerInputOld.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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();
Expand Down
Loading