Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2017-11-28 16:01:43 +00:00
commit 31680e6f27
3 changed files with 92 additions and 107 deletions

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
export default class DevicesPanel extends React.Component { export default class DevicesPanel extends React.Component {
constructor(props, context) { constructor(props, context) {
@ -29,11 +29,16 @@ export default class DevicesPanel extends React.Component {
this.state = { this.state = {
devices: undefined, devices: undefined,
deviceLoadError: undefined, deviceLoadError: undefined,
selectedDevices: [],
deleting: false,
}; };
this._unmounted = false; this._unmounted = false;
this._renderDevice = this._renderDevice.bind(this); this._renderDevice = this._renderDevice.bind(this);
this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -82,25 +87,78 @@ export default class DevicesPanel extends React.Component {
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
} }
_onDeviceDeleted(device) { _onDeviceSelectionToggled(device) {
if (this._unmounted) { return; } if (this._unmounted) { return; }
// delete the removed device from our list. const deviceId = device.device_id;
const removed_id = device.device_id;
this.setState((state, props) => { this.setState((state, props) => {
const newDevices = state.devices.filter( // Make a copy of the selected devices, then add or remove the device
(d) => { return d.device_id != removed_id; }, const selectedDevices = state.selectedDevices.slice();
);
return { devices: newDevices }; const i = selectedDevices.indexOf(deviceId);
if (i === -1) {
selectedDevices.push(deviceId);
} else {
selectedDevices.splice(i, 1);
}
return {selectedDevices};
}); });
} }
_onDeleteClick() {
this.setState({
deleting: true,
});
this._makeDeleteRequest(null).catch((error) => {
if (this._unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure
throw error;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest.bind(this),
});
}).catch((e) => {
console.error("Error deleting devices", e);
if (this._unmounted) { return; }
}).finally(() => {
this.setState({
deleting: false,
});
});
}
_makeDeleteRequest(auth) {
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
() => {
// Remove the deleted devices from `devices`, reset selection to []
this.setState({
devices: this.state.devices.filter(
(d) => !this.state.selectedDevices.includes(d.device_id),
),
selectedDevices: [],
});
},
);
}
_renderDevice(device) { _renderDevice(device) {
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return ( return <DevicesPanelEntry
<DevicesPanelEntry key={device.device_id} device={device} key={device.device_id}
onDeleted={()=>{this._onDeviceDeleted(device);}} /> device={device}
); selected={this.state.selectedDevices.includes(device.device_id)}
onDeviceToggled={this._onDeviceSelectionToggled}
/>;
} }
render() { render() {
@ -124,6 +182,12 @@ export default class DevicesPanel extends React.Component {
devices.sort(this._deviceCompare); devices.sort(this._deviceCompare);
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<div className="mx_textButton" onClick={this._onDeleteClick}>
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
</div>;
const classes = classNames(this.props.className, "mx_DevicesPanel"); const classes = classNames(this.props.className, "mx_DevicesPanel");
return ( return (
<div className={classes}> <div className={classes}>
@ -131,7 +195,9 @@ export default class DevicesPanel extends React.Component {
<div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div> <div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div>
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div> <div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div> <div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
<div className="mx_DevicesPanel_deviceButtons"></div> <div className="mx_DevicesPanel_deviceButtons">
{ this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') }
</div>
</div> </div>
{ devices.map(this._renderDevice) } { devices.map(this._renderDevice) }
</div> </div>

View file

@ -19,24 +19,15 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DateUtils from '../../../DateUtils'; import DateUtils from '../../../DateUtils';
const AUTH_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
export default class DevicesPanelEntry extends React.Component { export default class DevicesPanelEntry extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = {
deleting: false,
deleteError: undefined,
};
this._unmounted = false; this._unmounted = false;
this._onDeleteClick = this._onDeleteClick.bind(this); this.onDeviceToggled = this.onDeviceToggled.bind(this);
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
this._makeDeleteRequest = this._makeDeleteRequest.bind(this);
} }
componentWillUnmount() { componentWillUnmount() {
@ -53,56 +44,8 @@ export default class DevicesPanelEntry extends React.Component {
}); });
} }
_onDeleteClick() { onDeviceToggled() {
this.setState({deleting: true}); this.props.onDeviceToggled(this.props.device);
if (this.context.authCache.lastUpdate < Date.now() - AUTH_CACHE_AGE) {
this.context.authCache.auth = null;
}
// try with auth cache (which is null, so no interactive auth, to start off)
this._makeDeleteRequest(this.context.authCache.auth).catch((error) => {
if (this._unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure
throw error;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest,
});
this.setState({
deleting: false,
});
}).catch((e) => {
console.error("Error deleting device", e);
if (this._unmounted) { return; }
this.setState({
deleting: false,
deleteError: _t("Failed to delete device"),
});
}).done();
}
_makeDeleteRequest(auth) {
this.context.authCache.auth = auth;
this.context.authCache.lastUpdate = Date.now();
const device = this.props.device;
return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then(
() => {
this.props.onDeleted();
if (this._unmounted) { return; }
this.setState({ deleting: false });
},
);
} }
render() { render() {
@ -110,16 +53,6 @@ export default class DevicesPanelEntry extends React.Component {
const device = this.props.device; 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 = ""; let lastSeen = "";
if (device.last_seen_ts) { if (device.last_seen_ts) {
const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts)); const lastSeenDate = DateUtils.formatDate(new Date(device.last_seen_ts));
@ -127,18 +60,6 @@ export default class DevicesPanelEntry extends React.Component {
lastSeenDate.toLocaleString(); lastSeenDate.toLocaleString();
} }
let deleteButton;
if (this.state.deleteError) {
deleteButton = <div className="error">{ this.state.deleteError }</div>;
} else {
deleteButton = (
<div className="mx_textButton"
onClick={this._onDeleteClick}>
{ _t("Delete") }
</div>
);
}
let myDeviceClass = ''; let myDeviceClass = '';
if (device.device_id === MatrixClientPeg.get().getDeviceId()) { if (device.device_id === MatrixClientPeg.get().getDeviceId()) {
myDeviceClass = " mx_DevicesPanel_myDevice"; myDeviceClass = " mx_DevicesPanel_myDevice";
@ -159,7 +80,7 @@ export default class DevicesPanelEntry extends React.Component {
{ lastSeen } { lastSeen }
</div> </div>
<div className="mx_DevicesPanel_deviceButtons"> <div className="mx_DevicesPanel_deviceButtons">
{ deleteButton } <input type="checkbox" onClick={this.onDeviceToggled} checked={this.props.selected} />
</div> </div>
</div> </div>
); );
@ -168,13 +89,9 @@ export default class DevicesPanelEntry extends React.Component {
DevicesPanelEntry.propTypes = { DevicesPanelEntry.propTypes = {
device: React.PropTypes.object.isRequired, device: React.PropTypes.object.isRequired,
onDeleted: React.PropTypes.func, onDeviceToggled: React.PropTypes.func,
};
DevicesPanelEntry.contextTypes = {
authCache: React.PropTypes.object,
}; };
DevicesPanelEntry.defaultProps = { DevicesPanelEntry.defaultProps = {
onDeleted: function() {}, onDeviceToggled: function() {},
}; };

View file

@ -218,13 +218,15 @@
"Change Password": "Change Password", "Change Password": "Change Password",
"Your home server does not support device management.": "Your home server does not support device management.", "Your home server does not support device management.": "Your home server does not support device management.",
"Unable to load device list": "Unable to load device list", "Unable to load device list": "Unable to load device list",
"Authentication": "Authentication",
"Failed to delete device": "Failed to delete device",
"Delete %(count)s devices|one": "Delete device",
"Delete %(count)s devices|other": "Delete %(count)s devices",
"Device ID": "Device ID", "Device ID": "Device ID",
"Device Name": "Device Name", "Device Name": "Device Name",
"Last seen": "Last seen", "Last seen": "Last seen",
"Select devices": "Select devices",
"Failed to set display name": "Failed to set display name", "Failed to set display name": "Failed to set display name",
"Authentication": "Authentication",
"Failed to delete device": "Failed to delete device",
"Delete": "Delete",
"Disable Notifications": "Disable Notifications", "Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications", "Enable Notifications": "Enable Notifications",
"Cannot add any more widgets": "Cannot add any more widgets", "Cannot add any more widgets": "Cannot add any more widgets",
@ -537,7 +539,6 @@
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
"You're not currently a member of any communities.": "You're not currently a member of any communities.", "You're not currently a member of any communities.": "You're not currently a member of any communities.",
"Flair": "Flair",
"Unknown Address": "Unknown Address", "Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Do you want to load widget from URL:": "Do you want to load widget from URL:", "Do you want to load widget from URL:": "Do you want to load widget from URL:",
@ -553,6 +554,7 @@
"Unverify": "Unverify", "Unverify": "Unverify",
"Verify...": "Verify...", "Verify...": "Verify...",
"No results": "No results", "No results": "No results",
"Delete": "Delete",
"Communities": "Communities", "Communities": "Communities",
"Home": "Home", "Home": "Home",
"Integrations Error": "Integrations Error", "Integrations Error": "Integrations Error",
@ -870,10 +872,10 @@
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
"The phone number entered looks invalid": "The phone number entered looks invalid", "The phone number entered looks invalid": "The phone number entered looks invalid",
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Login as guest": "Login as guest", "Login as guest": "Login as guest",
"Sign in to get started": "Sign in to get started", "Sign in to get started": "Sign in to get started",
"Failed to fetch avatar URL": "Failed to fetch avatar URL", "Failed to fetch avatar URL": "Failed to fetch avatar URL",