diff --git a/src/component-index.js b/src/component-index.js
index e250838cc4..19a016aec8 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -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.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
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.voip.CallView'] = require('./components/views/voip/CallView');
module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox');
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 75fe1f0825..6555668ff4 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -319,7 +319,7 @@ module.exports = React.createClass({
);
},
- _renderDeviceInfo: function() {
+ _renderCryptoInfo: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
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 (
+
+
Devices
+
+
+ );
+ },
+
+ _renderLabs: function () {
+ let features = LABS_FEATURES.map(feature => (
+
+ {
+ UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
+ this.forceUpdate();
+ }}/>
+
+
+ ));
+ return (
+
+
Labs
+
+
These are experimental features that may break in unexpected ways. Use with caution.
+ {features}
+
+
+ )
+ },
+
render: function() {
var self = this;
var Loader = sdk.getComponent("elements.Spinner");
@@ -360,6 +399,7 @@ module.exports = React.createClass({
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications");
var EditableText = sdk.getComponent('elements.EditableText');
+
var avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
@@ -434,30 +474,6 @@ module.exports = React.createClass({
);
}
- this._renderLabs = function () {
- let features = LABS_FEATURES.map(feature => (
-
- UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} />
-
-
- ));
- return (
-
-
Labs
-
-
-
These are experimental features that may break in unexpected ways. Use with caution.
- {features}
-
-
- )
- };
-
return (
@@ -510,10 +526,9 @@ module.exports = React.createClass({
{notification_area}
{this._renderUserInterfaceSettings()}
-
- {this._renderDeviceInfo()}
-
{this._renderLabs()}
+ {this._renderDevicesPanel()}
+ {this._renderCryptoInfo()}
Advanced
diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js
new file mode 100644
index 0000000000..8dd6bb9230
--- /dev/null
+++ b/src/components/views/settings/DevicesPanel.js
@@ -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 (
+
{this._onDeviceDeleted(device)}} />
+ );
+ }
+
+ render() {
+ const Spinner = sdk.getComponent("elements.Spinner");
+
+ if (this.state.deviceLoadError !== undefined) {
+ const classes = classNames(this.props.className, "error");
+ return (
+
+ {this.state.deviceLoadError}
+
+ );
+ }
+
+ const devices = this.state.devices;
+ if (devices === undefined) {
+ // still loading
+ const classes = this.props.className;
+ return ;
+ }
+
+ devices.sort(this._deviceCompare);
+
+ const classes = classNames(this.props.className, "mx_DevicesPanel");
+ return (
+
+
+ {devices.map(this._renderDevice)}
+
+ );
+ }
+}
diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js
new file mode 100644
index 0000000000..6858e62102
--- /dev/null
+++ b/src/components/views/settings/DevicesPanelEntry.js
@@ -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 (
+
+
+
+ );
+ }
+
+ 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 = {this.state.deleteError}
+ } else {
+ deleteButton = (
+
+ Delete
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {lastSeen}
+
+
+ {deleteButton}
+
+
+ );
+ }
+}
+
+DevicesPanelEntry.propTypes = {
+ device: React.PropTypes.object.isRequired,
+ onDeleted: React.PropTypes.func,
+};
+
+DevicesPanelEntry.defaultProps = {
+ onDeleted: function() {},
+};