mirror of
https://github.com/element-hq/element-web.git
synced 2024-12-17 08:02:10 +03:00
230 lines
8.3 KiB
TypeScript
230 lines
8.3 KiB
TypeScript
/*
|
|
Copyright 2016 OpenMarket Ltd
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
Copyright 2021 New Vector 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 { IMyDevice } from "matrix-js-sdk/src/client";
|
|
|
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
import { _t } from '../../../languageHandler';
|
|
import Modal from '../../../Modal';
|
|
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
|
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|
import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
|
|
|
|
interface IProps {
|
|
className?: string;
|
|
}
|
|
|
|
interface IState {
|
|
devices: IMyDevice[];
|
|
deviceLoadError?: string;
|
|
selectedDevices?: string[];
|
|
deleting?: boolean;
|
|
}
|
|
|
|
@replaceableComponent("views.settings.DevicesPanel")
|
|
export default class DevicesPanel extends React.Component<IProps, IState> {
|
|
private unmounted = false;
|
|
|
|
public componentDidMount(): void {
|
|
this.loadDevices();
|
|
}
|
|
|
|
public componentWillUnmount(): void {
|
|
this.unmounted = true;
|
|
}
|
|
|
|
private loadDevices(): void {
|
|
MatrixClientPeg.get().getDevices().then(
|
|
(resp) => {
|
|
if (this.unmounted) { return; }
|
|
this.setState({ devices: resp.devices || [] });
|
|
},
|
|
(error) => {
|
|
if (this.unmounted) { return; }
|
|
let errtxt;
|
|
if (error.httpStatus == 404) {
|
|
// 404 probably means the HS doesn't yet support the API.
|
|
errtxt = _t("Your homeserver does not support session management.");
|
|
} else {
|
|
console.error("Error loading sessions:", error);
|
|
errtxt = _t("Unable to load session list");
|
|
}
|
|
this.setState({ deviceLoadError: errtxt });
|
|
},
|
|
);
|
|
}
|
|
|
|
/*
|
|
* compare two devices, sorting from most-recently-seen to least-recently-seen
|
|
* (and then, for stability, by device id)
|
|
*/
|
|
private deviceCompare(a: IMyDevice, b: IMyDevice): number {
|
|
// 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;
|
|
}
|
|
|
|
private onDeviceSelectionToggled = (device: IMyDevice): void => {
|
|
if (this.unmounted) { return; }
|
|
|
|
const deviceId = device.device_id;
|
|
this.setState((state, props) => {
|
|
// Make a copy of the selected devices, then add or remove the device
|
|
const selectedDevices = state.selectedDevices.slice();
|
|
|
|
const i = selectedDevices.indexOf(deviceId);
|
|
if (i === -1) {
|
|
selectedDevices.push(deviceId);
|
|
} else {
|
|
selectedDevices.splice(i, 1);
|
|
}
|
|
|
|
return { selectedDevices };
|
|
});
|
|
};
|
|
|
|
private onDeleteClick = (): void => {
|
|
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 numDevices = this.state.selectedDevices.length;
|
|
const dialogAesthetics = {
|
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
|
title: _t("Use Single Sign On to continue"),
|
|
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity.", {
|
|
count: numDevices,
|
|
}),
|
|
continueText: _t("Single Sign On"),
|
|
continueKind: "primary",
|
|
},
|
|
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
|
title: _t("Confirm deleting these sessions"),
|
|
body: _t("Click the button below to confirm deleting these sessions.", {
|
|
count: numDevices,
|
|
}),
|
|
continueText: _t("Delete sessions", { count: numDevices }),
|
|
continueKind: "danger",
|
|
},
|
|
};
|
|
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
|
|
title: _t("Authentication"),
|
|
matrixClient: MatrixClientPeg.get(),
|
|
authData: error.data,
|
|
makeRequest: this.makeDeleteRequest.bind(this),
|
|
aestheticsForStagePhases: {
|
|
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
|
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
|
},
|
|
});
|
|
}).catch((e) => {
|
|
console.error("Error deleting sessions", e);
|
|
if (this.unmounted) { return; }
|
|
}).finally(() => {
|
|
this.setState({
|
|
deleting: false,
|
|
});
|
|
});
|
|
};
|
|
|
|
// TODO: proper typing for auth
|
|
private makeDeleteRequest(auth?: any): Promise<any> {
|
|
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: [],
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
private renderDevice = (device: IMyDevice): JSX.Element => {
|
|
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
|
return <DevicesPanelEntry
|
|
key={device.device_id}
|
|
device={device}
|
|
selected={this.state.selectedDevices.includes(device.device_id)}
|
|
onDeviceToggled={this.onDeviceSelectionToggled}
|
|
/>;
|
|
};
|
|
|
|
public render(): JSX.Element {
|
|
const Spinner = sdk.getComponent("elements.Spinner");
|
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
|
|
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 deleteButton = this.state.deleting ?
|
|
<Spinner w={22} h={22} /> :
|
|
<AccessibleButton onClick={this.onDeleteClick} kind="danger_sm">
|
|
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) }
|
|
</AccessibleButton>;
|
|
|
|
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
|
return (
|
|
<div className={classes}>
|
|
<div className="mx_DevicesPanel_header">
|
|
<div className="mx_DevicesPanel_deviceId">{ _t("ID") }</div>
|
|
<div className="mx_DevicesPanel_deviceName">{ _t("Public Name") }</div>
|
|
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
|
|
<div className="mx_DevicesPanel_deviceButtons">
|
|
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
|
</div>
|
|
</div>
|
|
{ devices.map(this.renderDevice) }
|
|
</div>
|
|
);
|
|
}
|
|
}
|