Implement device management UI
View/rename/delete.
This commit is contained in:
parent
9333793a7c
commit
c071bc54f9
4 changed files with 317 additions and 28 deletions
|
@ -107,6 +107,8 @@ module.exports.components['views.rooms.UserTile'] = require('./components/views/
|
||||||
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||||
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||||
|
module.exports.components['views.settings.DevicesPanel'] = require('./components/views/settings/DevicesPanel');
|
||||||
|
module.exports.components['views.settings.DevicesPanelEntry'] = require('./components/views/settings/DevicesPanelEntry');
|
||||||
module.exports.components['views.settings.EnableNotificationsButton'] = require('./components/views/settings/EnableNotificationsButton');
|
module.exports.components['views.settings.EnableNotificationsButton'] = require('./components/views/settings/EnableNotificationsButton');
|
||||||
module.exports.components['views.voip.CallView'] = require('./components/views/voip/CallView');
|
module.exports.components['views.voip.CallView'] = require('./components/views/voip/CallView');
|
||||||
module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox');
|
module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox');
|
||||||
|
|
|
@ -319,7 +319,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderDeviceInfo: function() {
|
_renderCryptoInfo: function() {
|
||||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -340,6 +340,45 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderDevicesPanel: function() {
|
||||||
|
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Devices</h3>
|
||||||
|
<DevicesPanel className="mx_UserSettings_section" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderLabs: function () {
|
||||||
|
let features = LABS_FEATURES.map(feature => (
|
||||||
|
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={feature.id}
|
||||||
|
name={feature.id}
|
||||||
|
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||||
|
onChange={e => {
|
||||||
|
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
|
||||||
|
this.forceUpdate();
|
||||||
|
}}/>
|
||||||
|
<label htmlFor={feature.id}>{feature.name}</label>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Labs</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
|
||||||
|
{features}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -360,6 +399,7 @@ module.exports = React.createClass({
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
var Notifications = sdk.getComponent("settings.Notifications");
|
var Notifications = sdk.getComponent("settings.Notifications");
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
|
||||||
var avatarUrl = (
|
var avatarUrl = (
|
||||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||||
);
|
);
|
||||||
|
@ -434,30 +474,6 @@ module.exports = React.createClass({
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._renderLabs = function () {
|
|
||||||
let features = LABS_FEATURES.map(feature => (
|
|
||||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={feature.id}
|
|
||||||
name={feature.id}
|
|
||||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
|
||||||
onChange={e => UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} />
|
|
||||||
<label htmlFor={feature.id}>{feature.name}</label>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>Labs</h3>
|
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
|
||||||
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
|
|
||||||
{features}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings">
|
<div className="mx_UserSettings">
|
||||||
<SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
|
<SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
|
||||||
|
@ -510,10 +526,9 @@ module.exports = React.createClass({
|
||||||
{notification_area}
|
{notification_area}
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{this._renderUserInterfaceSettings()}
|
||||||
|
|
||||||
{this._renderDeviceInfo()}
|
|
||||||
|
|
||||||
{this._renderLabs()}
|
{this._renderLabs()}
|
||||||
|
{this._renderDevicesPanel()}
|
||||||
|
{this._renderCryptoInfo()}
|
||||||
|
|
||||||
<h3>Advanced</h3>
|
<h3>Advanced</h3>
|
||||||
|
|
||||||
|
|
138
src/components/views/settings/DevicesPanel.js
Normal file
138
src/components/views/settings/DevicesPanel.js
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket 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 React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
|
||||||
|
export default class DevicesPanel extends React.Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
devices: undefined,
|
||||||
|
deviceLoadError: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._unmounted = false;
|
||||||
|
|
||||||
|
this._renderDevice = this._renderDevice.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._loadDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadDevices() {
|
||||||
|
MatrixClientPeg.get().getDevices().done(
|
||||||
|
(resp) => {
|
||||||
|
if (this._unmounted) { return; }
|
||||||
|
this.setState({devices: resp.devices || []});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (this._unmounted) { return; }
|
||||||
|
var errtxt;
|
||||||
|
if (err.httpStatus == 404) {
|
||||||
|
// 404 probably means the HS doesn't yet support the API.
|
||||||
|
errtxt = "Your home server does not support device management.";
|
||||||
|
} else {
|
||||||
|
console.error("Error loading devices:", error);
|
||||||
|
errtxt = "Unable to load device list.";
|
||||||
|
}
|
||||||
|
this.setState({deviceLoadError: errtxt});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compare two devices, sorting from most-recently-seen to least-recently-seen
|
||||||
|
* (and then, for stability, by device id)
|
||||||
|
*/
|
||||||
|
_deviceCompare(a, b) {
|
||||||
|
// return < 0 if a comes before b, > 0 if a comes after b.
|
||||||
|
const lastSeenDelta =
|
||||||
|
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
|
||||||
|
|
||||||
|
if (lastSeenDelta !== 0) { return lastSeenDelta; }
|
||||||
|
|
||||||
|
const idA = a.device_id;
|
||||||
|
const idB = b.device_id;
|
||||||
|
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDeviceDeleted(device) {
|
||||||
|
if (this._unmounted) { return; }
|
||||||
|
|
||||||
|
// delete the removed device from our list.
|
||||||
|
const removed_id = device.device_id;
|
||||||
|
this.setState((state, props) => {
|
||||||
|
const newDevices = state.devices.filter(
|
||||||
|
d => { return d.device_id != removed_id }
|
||||||
|
);
|
||||||
|
return { devices: newDevices };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDevice(device) {
|
||||||
|
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||||
|
return (
|
||||||
|
<DevicesPanelEntry key={device.device_id} device={device}
|
||||||
|
onDeleted={()=>{this._onDeviceDeleted(device)}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
if (this.state.deviceLoadError !== undefined) {
|
||||||
|
const classes = classNames(this.props.className, "error");
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
{this.state.deviceLoadError}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = this.state.devices;
|
||||||
|
if (devices === undefined) {
|
||||||
|
// still loading
|
||||||
|
const classes = this.props.className;
|
||||||
|
return <Spinner className={classes}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.sort(this._deviceCompare);
|
||||||
|
|
||||||
|
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<div className="mx_DevicesPanel_header">
|
||||||
|
<div className="mx_DevicesPanel_deviceName">Name</div>
|
||||||
|
<div className="mx_DevicesPanel_deviceLastSeen">Last seen</div>
|
||||||
|
<div className="mx_DevicesPanel_deviceButtons"></div>
|
||||||
|
</div>
|
||||||
|
{devices.map(this._renderDevice)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
134
src/components/views/settings/DevicesPanelEntry.js
Normal file
134
src/components/views/settings/DevicesPanelEntry.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket 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 React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import DateUtils from '../../../DateUtils';
|
||||||
|
|
||||||
|
export default class DevicesPanelEntry extends React.Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
deleting: false,
|
||||||
|
deleteError: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._unmounted = false;
|
||||||
|
|
||||||
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
|
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisplayNameChanged(value) {
|
||||||
|
const device = this.props.device;
|
||||||
|
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
|
||||||
|
display_name: value,
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("Error setting device display name", e);
|
||||||
|
throw new Error("Failed to set display name");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDeleteClick() {
|
||||||
|
const device = this.props.device;
|
||||||
|
this.setState({deleting: true});
|
||||||
|
|
||||||
|
MatrixClientPeg.get().deleteDevice(device.device_id).done(
|
||||||
|
() => {
|
||||||
|
this.props.onDeleted();
|
||||||
|
if (this._unmounted) { return; }
|
||||||
|
this.setState({ deleting: false });
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
console.error("Error deleting device", e);
|
||||||
|
if (this._unmounted) { return; }
|
||||||
|
this.setState({
|
||||||
|
deleting: false,
|
||||||
|
deleteError: "Failed to delete device",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||||
|
|
||||||
|
const device = this.props.device;
|
||||||
|
|
||||||
|
if (this.state.deleting) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_DevicesPanel_device">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastSeen = "";
|
||||||
|
if (device.last_seen_ts) {
|
||||||
|
// todo: format the timestamp as "5 minutes ago" or whatever.
|
||||||
|
const lastSeenDate = new Date(device.last_seen_ts);
|
||||||
|
lastSeen = device.last_seen_ip + " @ " +
|
||||||
|
lastSeenDate.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteButton;
|
||||||
|
if (this.state.deleteError) {
|
||||||
|
deleteButton = <div className="error">{this.state.deleteError}</div>
|
||||||
|
} else {
|
||||||
|
deleteButton = (
|
||||||
|
<div className="textButton"
|
||||||
|
onClick={this._onDeleteClick}>
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_DevicesPanel_device">
|
||||||
|
<div className="mx_DevicesPanel_deviceName">
|
||||||
|
<EditableTextContainer initialValue={device.display_name}
|
||||||
|
onSubmit={this._onDisplayNameChanged} />
|
||||||
|
</div>
|
||||||
|
<div className="mx_DevicesPanel_lastSeen">
|
||||||
|
{lastSeen}
|
||||||
|
</div>
|
||||||
|
<div className="mx_DevicesPanel_deviceButtons">
|
||||||
|
{deleteButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicesPanelEntry.propTypes = {
|
||||||
|
device: React.PropTypes.object.isRequired,
|
||||||
|
onDeleted: React.PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
DevicesPanelEntry.defaultProps = {
|
||||||
|
onDeleted: function() {},
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue