Move session manager out of beta (#10968)
* remove old device manager * undo type fix for cypress crypto * update test case
This commit is contained in:
parent
e326526c10
commit
530197bfcd
21 changed files with 450 additions and 1673 deletions
|
@ -19,12 +19,11 @@ import { Action } from "../../dispatcher/actions";
|
|||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
/**
|
||||
* Redirect to the correct device manager section
|
||||
* Based on the labs setting
|
||||
* Open user device manager settings
|
||||
*/
|
||||
export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => {
|
||||
export const viewUserDeviceSettings = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security,
|
||||
initialTabId: UserTab.SessionManager,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -700,7 +700,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case Action.ViewUserDeviceSettings: {
|
||||
viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager"));
|
||||
viewUserDeviceSettings();
|
||||
break;
|
||||
}
|
||||
case Action.ViewUserSettings: {
|
||||
|
|
|
@ -45,7 +45,6 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
mjolnirEnabled: boolean;
|
||||
newSessionManagerEnabled: boolean;
|
||||
}
|
||||
|
||||
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||
|
@ -56,15 +55,11 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
|
||||
this.state = {
|
||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||
newSessionManagerEnabled: SettingsStore.getValue("feature_new_device_manager"),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.settingsWatchers = [
|
||||
SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged),
|
||||
SettingsStore.watchSetting("feature_new_device_manager", null, this.sessionManagerChanged),
|
||||
];
|
||||
this.settingsWatchers = [SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged)];
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -76,11 +71,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
this.setState({ mjolnirEnabled: newValue });
|
||||
};
|
||||
|
||||
private sessionManagerChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
|
||||
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
||||
this.setState({ newSessionManagerEnabled: newValue });
|
||||
};
|
||||
|
||||
private getTabs(): NonEmptyArray<Tab<UserTab>> {
|
||||
const tabs: Tab<UserTab>[] = [];
|
||||
|
||||
|
@ -160,18 +150,16 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
"UserSettingsSecurityPrivacy",
|
||||
),
|
||||
);
|
||||
if (this.state.newSessionManagerEnabled) {
|
||||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.SessionManager,
|
||||
_td("Sessions"),
|
||||
"mx_UserSettingsDialog_sessionsIcon",
|
||||
<SessionManagerTab />,
|
||||
// don't track with posthog while under construction
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
tabs.push(
|
||||
new Tab(
|
||||
UserTab.SessionManager,
|
||||
_td("Sessions"),
|
||||
"mx_UserSettingsDialog_sessionsIcon",
|
||||
<SessionManagerTab />,
|
||||
// don't track with posthog while under construction
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
// Show the Labs tab if enabled or if there are any active betas
|
||||
if (
|
||||
SdkConfig.get("show_labs_settings") ||
|
||||
|
|
|
@ -1,365 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 - 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { logger } from "matrix-js-sdk/src/logger";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import DevicesPanelEntry from "./DevicesPanelEntry";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { fetchExtendedDeviceInformation } from "./devices/useOwnDevices";
|
||||
import { DevicesDictionary, ExtendedDevice } from "./devices/types";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
devices?: DevicesDictionary;
|
||||
deviceLoadError?: string;
|
||||
selectedDevices: string[];
|
||||
deleting?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesPanel extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
private unmounted = false;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedDevices: [],
|
||||
};
|
||||
this.loadDevices = this.loadDevices.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onDevicesUpdated = (users: string[]): void => {
|
||||
if (!users.includes(this.context.getUserId()!)) return;
|
||||
this.loadDevices();
|
||||
};
|
||||
|
||||
private loadDevices(): void {
|
||||
const cli = this.context;
|
||||
fetchExtendedDeviceInformation(cli).then(
|
||||
(devices) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState((state, props) => {
|
||||
return {
|
||||
devices: devices,
|
||||
selectedDevices: state.selectedDevices.filter((deviceId) => devices.hasOwnProperty(deviceId)),
|
||||
};
|
||||
});
|
||||
},
|
||||
(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 device management.");
|
||||
} else {
|
||||
logger.error("Error loading sessions:", error);
|
||||
errtxt = _t("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)
|
||||
*/
|
||||
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 selectAll = (devices: IMyDevice[]): void => {
|
||||
this.setState((state, props) => {
|
||||
const selectedDevices = state.selectedDevices.slice();
|
||||
|
||||
for (const device of devices) {
|
||||
const deviceId = device.device_id;
|
||||
if (!selectedDevices.includes(deviceId)) {
|
||||
selectedDevices.push(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedDevices };
|
||||
});
|
||||
};
|
||||
|
||||
private deselectAll = (devices: IMyDevice[]): void => {
|
||||
this.setState((state, props) => {
|
||||
const selectedDevices = state.selectedDevices.slice();
|
||||
|
||||
for (const device of devices) {
|
||||
const deviceId = device.device_id;
|
||||
const i = selectedDevices.indexOf(deviceId);
|
||||
if (i !== -1) {
|
||||
selectedDevices.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedDevices };
|
||||
});
|
||||
};
|
||||
|
||||
private onDeleteClick = async (): Promise<void> => {
|
||||
if (this.state.selectedDevices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
deleting: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await deleteDevicesWithInteractiveAuth(this.context, this.state.selectedDevices, (success) => {
|
||||
if (success) {
|
||||
// Reset selection to [], update device list
|
||||
this.setState({
|
||||
selectedDevices: [],
|
||||
});
|
||||
this.loadDevices();
|
||||
}
|
||||
this.setState({
|
||||
deleting: false,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting sessions", error);
|
||||
this.setState({
|
||||
deleting: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private renderDevice = (device: ExtendedDevice): JSX.Element => {
|
||||
const myDeviceId = this.context.getDeviceId()!;
|
||||
const myDevice = this.state.devices?.[myDeviceId];
|
||||
|
||||
const isOwnDevice = device.device_id === myDeviceId;
|
||||
|
||||
// If our own device is unverified, it can't verify other
|
||||
// devices, it can only request verification for itself
|
||||
const canBeVerified = (myDevice && myDevice.isVerified) || isOwnDevice;
|
||||
|
||||
return (
|
||||
<DevicesPanelEntry
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
selected={this.state.selectedDevices.includes(device.device_id)}
|
||||
isOwnDevice={isOwnDevice}
|
||||
verified={device.isVerified}
|
||||
canBeVerified={canBeVerified}
|
||||
onDeviceChange={this.loadDevices}
|
||||
onDeviceToggled={this.onDeviceSelectionToggled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const loadError = <div className={classNames(this.props.className, "error")}>{this.state.deviceLoadError}</div>;
|
||||
|
||||
if (this.state.deviceLoadError !== undefined) {
|
||||
return loadError;
|
||||
}
|
||||
|
||||
const devices = this.state.devices;
|
||||
if (devices === undefined) {
|
||||
// still loading
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const myDeviceId = this.context.getDeviceId()!;
|
||||
const myDevice = devices[myDeviceId];
|
||||
|
||||
if (!myDevice) {
|
||||
return loadError;
|
||||
}
|
||||
|
||||
const otherDevices = Object.values(devices).filter((device) => device.device_id !== myDeviceId);
|
||||
otherDevices.sort(this.deviceCompare);
|
||||
|
||||
const verifiedDevices: ExtendedDevice[] = [];
|
||||
const unverifiedDevices: ExtendedDevice[] = [];
|
||||
const nonCryptoDevices: ExtendedDevice[] = [];
|
||||
for (const device of otherDevices) {
|
||||
const verified = device.isVerified;
|
||||
if (verified === true) {
|
||||
verifiedDevices.push(device);
|
||||
} else if (verified === false) {
|
||||
unverifiedDevices.push(device);
|
||||
} else {
|
||||
nonCryptoDevices.push(device);
|
||||
}
|
||||
}
|
||||
|
||||
const section = (trustIcon: JSX.Element, title: string, deviceList: ExtendedDevice[]): JSX.Element => {
|
||||
if (deviceList.length === 0) {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
let selectButton: JSX.Element | undefined;
|
||||
if (deviceList.length > 1) {
|
||||
const anySelected = deviceList.some((device) => this.state.selectedDevices.includes(device.device_id));
|
||||
const buttonAction = anySelected
|
||||
? () => {
|
||||
this.deselectAll(deviceList);
|
||||
}
|
||||
: () => {
|
||||
this.selectAll(deviceList);
|
||||
};
|
||||
const buttonText = anySelected ? _t("Deselect all") : _t("Select all");
|
||||
selectButton = (
|
||||
<div className="mx_DevicesPanel_header_button">
|
||||
<AccessibleButton
|
||||
className="mx_DevicesPanel_selectButton"
|
||||
kind="secondary"
|
||||
onClick={buttonAction}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_DevicesPanel_header">
|
||||
<div className="mx_DevicesPanel_header_trust">{trustIcon}</div>
|
||||
<div className="mx_DevicesPanel_header_title">{title}</div>
|
||||
{selectButton}
|
||||
</div>
|
||||
{deviceList.map(this.renderDevice)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const verifiedDevicesSection = section(
|
||||
<span className="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_verified" />,
|
||||
_t("Verified devices"),
|
||||
verifiedDevices,
|
||||
);
|
||||
|
||||
const unverifiedDevicesSection = section(
|
||||
<span className="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_warning" />,
|
||||
_t("Unverified devices"),
|
||||
unverifiedDevices,
|
||||
);
|
||||
|
||||
const nonCryptoDevicesSection = section(
|
||||
<React.Fragment />,
|
||||
_t("Devices without encryption support"),
|
||||
nonCryptoDevices,
|
||||
);
|
||||
|
||||
const deleteButton = this.state.deleting ? (
|
||||
<Spinner w={22} h={22} />
|
||||
) : (
|
||||
<AccessibleButton
|
||||
className="mx_DevicesPanel_deleteButton"
|
||||
onClick={this.onDeleteClick}
|
||||
kind="danger_outline"
|
||||
disabled={this.state.selectedDevices.length === 0}
|
||||
data-testid="sign-out-devices-btn"
|
||||
>
|
||||
{_t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length })}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
const otherDevicesSection =
|
||||
otherDevices.length > 0 ? (
|
||||
<React.Fragment>
|
||||
{verifiedDevicesSection}
|
||||
{unverifiedDevicesSection}
|
||||
{nonCryptoDevicesSection}
|
||||
{deleteButton}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_DevicesPanel_noOtherDevices">
|
||||
{_t("You aren't signed into any other devices.")}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_DevicesPanel_header">
|
||||
<div className="mx_DevicesPanel_header_title">{_t("This device")}</div>
|
||||
</div>
|
||||
{this.renderDevice(myDevice)}
|
||||
{otherDevicesSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import Modal from "../../../Modal";
|
||||
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
|
||||
import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog";
|
||||
import LogoutDialog from "../dialogs/LogoutDialog";
|
||||
import DeviceTile from "./devices/DeviceTile";
|
||||
import SelectableDeviceTile from "./devices/SelectableDeviceTile";
|
||||
import { DeviceType } from "../../../utils/device/parseUserAgent";
|
||||
|
||||
interface IProps {
|
||||
device: IMyDevice;
|
||||
isOwnDevice: boolean;
|
||||
verified: boolean | null;
|
||||
canBeVerified: boolean;
|
||||
onDeviceChange: () => void;
|
||||
onDeviceToggled: (device: IMyDevice) => void;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
renaming: boolean;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
renaming: false,
|
||||
displayName: props.device.display_name ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
private onDeviceToggled = (): void => {
|
||||
this.props.onDeviceToggled(this.props.device);
|
||||
};
|
||||
|
||||
private onRename = (): void => {
|
||||
this.setState({ renaming: true });
|
||||
};
|
||||
|
||||
private onChangeDisplayName = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
displayName: ev.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onRenameSubmit = async (): Promise<void> => {
|
||||
this.setState({ renaming: false });
|
||||
await MatrixClientPeg.get()
|
||||
.setDeviceDetails(this.props.device.device_id, {
|
||||
display_name: this.state.displayName,
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Error setting session display name", e);
|
||||
throw new Error(_t("Failed to set display name"));
|
||||
});
|
||||
this.props.onDeviceChange();
|
||||
};
|
||||
|
||||
private onRenameCancel = (): void => {
|
||||
this.setState({ renaming: false });
|
||||
};
|
||||
|
||||
private onOwnDeviceSignOut = (): void => {
|
||||
Modal.createDialog(
|
||||
LogoutDialog,
|
||||
/* props= */ {},
|
||||
/* className= */ undefined,
|
||||
/* isPriority= */ false,
|
||||
/* isStatic= */ true,
|
||||
);
|
||||
};
|
||||
|
||||
private verify = async (): Promise<void> => {
|
||||
if (this.props.isOwnDevice) {
|
||||
Modal.createDialog(SetupEncryptionDialog, {
|
||||
onFinished: this.props.onDeviceChange,
|
||||
});
|
||||
} else {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getSafeUserId();
|
||||
const verificationRequestPromise = cli.requestVerification(userId, [this.props.device.device_id]);
|
||||
Modal.createDialog(VerificationRequestDialog, {
|
||||
verificationRequestPromise,
|
||||
member: cli.getUser(userId) ?? undefined,
|
||||
onFinished: async (): Promise<void> => {
|
||||
const request = await verificationRequestPromise;
|
||||
request.cancel();
|
||||
this.props.onDeviceChange();
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let iconClass = "";
|
||||
let verifyButton: JSX.Element | undefined;
|
||||
if (this.props.verified !== null) {
|
||||
iconClass = this.props.verified ? "mx_E2EIcon_verified" : "mx_E2EIcon_warning";
|
||||
if (!this.props.verified && this.props.canBeVerified) {
|
||||
verifyButton = (
|
||||
<AccessibleButton kind="primary" onClick={this.verify}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let signOutButton: JSX.Element | undefined;
|
||||
if (this.props.isOwnDevice) {
|
||||
signOutButton = (
|
||||
<AccessibleButton kind="danger_outline" onClick={this.onOwnDeviceSignOut}>
|
||||
{_t("Sign Out")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const buttons = this.state.renaming ? (
|
||||
<form className="mx_DevicesPanel_renameForm" onSubmit={this.onRenameSubmit}>
|
||||
<Field
|
||||
label={_t("Display Name")}
|
||||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this.onChangeDisplayName}
|
||||
autoFocus
|
||||
/>
|
||||
<AccessibleButton onClick={this.onRenameSubmit} kind="confirm_sm" />
|
||||
<AccessibleButton onClick={this.onRenameCancel} kind="cancel_sm" />
|
||||
</form>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{signOutButton}
|
||||
{verifyButton}
|
||||
<AccessibleButton kind="primary_outline" onClick={this.onRename}>
|
||||
{_t("Rename")}
|
||||
</AccessibleButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const extendedDevice = {
|
||||
...this.props.device,
|
||||
isVerified: this.props.verified,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
|
||||
if (this.props.isOwnDevice) {
|
||||
return (
|
||||
<div className={classNames("mx_DevicesPanel_device", "mx_DevicesPanel_myDevice")}>
|
||||
<div className="mx_DevicesPanel_deviceTrust">
|
||||
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
|
||||
</div>
|
||||
<DeviceTile device={extendedDevice}>{buttons}</DeviceTile>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<SelectableDeviceTile
|
||||
device={extendedDevice}
|
||||
onSelect={this.onDeviceToggled}
|
||||
isSelected={this.props.selected}
|
||||
>
|
||||
{buttons}
|
||||
</SelectableDeviceTile>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -30,7 +30,6 @@ import { UIFeature } from "../../../../../settings/UIFeature";
|
|||
import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||
import CryptographyPanel from "../../CryptographyPanel";
|
||||
import DevicesPanel from "../../DevicesPanel";
|
||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||
import CrossSigningPanel from "../../CrossSigningPanel";
|
||||
import EventIndexPanel from "../../EventIndexPanel";
|
||||
|
@ -38,8 +37,6 @@ import InlineSpinner from "../../../elements/InlineSpinner";
|
|||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
|
||||
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
|
||||
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
|
||||
import LoginWithQRSection from "../../devices/LoginWithQRSection";
|
||||
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
|
@ -83,10 +80,7 @@ interface IState {
|
|||
waitingUnignored: string[];
|
||||
managingInvites: boolean;
|
||||
invitedRoomIds: Set<string>;
|
||||
showLoginWithQR: Mode | null;
|
||||
versions?: IServerVersions;
|
||||
// we can't use the capabilities type from the js-sdk because it isn't exported
|
||||
capabilities?: Record<string, any>;
|
||||
}
|
||||
|
||||
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
|
||||
|
@ -103,7 +97,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
waitingUnignored: [],
|
||||
managingInvites: false,
|
||||
invitedRoomIds,
|
||||
showLoginWithQR: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,9 +114,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
MatrixClientPeg.get()
|
||||
.getVersions()
|
||||
.then((versions) => this.setState({ versions }));
|
||||
MatrixClientPeg.get()
|
||||
.getCapabilities()
|
||||
.then((capabilities) => this.setState({ capabilities }));
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -284,14 +274,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
private onShowQRClicked = (): void => {
|
||||
this.setState({ showLoginWithQR: Mode.Show });
|
||||
};
|
||||
|
||||
private onLoginWithQRFinished = (): void => {
|
||||
this.setState({ showLoginWithQR: null });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const secureBackup = (
|
||||
<SettingsSubsection heading={_t("Secure Backup")}>
|
||||
|
@ -374,42 +356,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
}
|
||||
|
||||
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
|
||||
const devicesSection = useNewSessionManager ? null : (
|
||||
<SettingsSection heading={_t("Where you're signed in")} data-testid="devices-section">
|
||||
<SettingsSubsectionText>
|
||||
{_t(
|
||||
"Manage your signed-in devices below. " +
|
||||
"A device's name is visible to people you communicate with.",
|
||||
)}
|
||||
</SettingsSubsectionText>
|
||||
<DevicesPanel />
|
||||
<LoginWithQRSection
|
||||
onShowQr={this.onShowQRClicked}
|
||||
versions={this.state.versions}
|
||||
capabilities={this.state.capabilities}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
if (this.state.showLoginWithQR) {
|
||||
return (
|
||||
<SettingsTab>
|
||||
<LoginWithQR
|
||||
onFinished={this.onLoginWithQRFinished}
|
||||
mode={this.state.showLoginWithQR}
|
||||
client={client}
|
||||
/>
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab>
|
||||
{warning}
|
||||
{devicesSection}
|
||||
<SettingsSection heading={_t("Encryption")}>
|
||||
{secureBackup}
|
||||
{eventIndex}
|
||||
|
|
|
@ -987,10 +987,6 @@
|
|||
"Favourite Messages": "Favourite Messages",
|
||||
"Under active development.": "Under active development.",
|
||||
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
|
||||
"Use new session manager": "Use new session manager",
|
||||
"New session manager": "New session manager",
|
||||
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
|
||||
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.",
|
||||
"Rust cryptography implementation": "Rust cryptography implementation",
|
||||
"Font size": "Font size",
|
||||
"Use custom size": "Use custom size",
|
||||
|
@ -1382,21 +1378,6 @@
|
|||
"Cryptography": "Cryptography",
|
||||
"Session ID:": "Session ID:",
|
||||
"Session key:": "Session key:",
|
||||
"Your homeserver does not support device management.": "Your homeserver does not support device management.",
|
||||
"Unable to load device list": "Unable to load device list",
|
||||
"Deselect all": "Deselect all",
|
||||
"Select all": "Select all",
|
||||
"Verified devices": "Verified devices",
|
||||
"Unverified devices": "Unverified devices",
|
||||
"Devices without encryption support": "Devices without encryption support",
|
||||
"Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices",
|
||||
"Sign out %(count)s selected devices|one": "Sign out %(count)s selected device",
|
||||
"You aren't signed into any other devices.": "You aren't signed into any other devices.",
|
||||
"This device": "This device",
|
||||
"Failed to set display name": "Failed to set display name",
|
||||
"Sign Out": "Sign Out",
|
||||
"Display Name": "Display Name",
|
||||
"Rename": "Rename",
|
||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
|
||||
"Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.",
|
||||
|
@ -1463,6 +1444,7 @@
|
|||
"There was an error loading your notification settings.": "There was an error loading your notification settings.",
|
||||
"Failed to save your profile": "Failed to save your profile",
|
||||
"The operation could not be completed": "The operation could not be completed",
|
||||
"Display Name": "Display Name",
|
||||
"Profile picture": "Profile picture",
|
||||
"Save": "Save",
|
||||
"Delete Backup": "Delete Backup",
|
||||
|
@ -1660,8 +1642,6 @@
|
|||
"Privacy": "Privacy",
|
||||
"Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.",
|
||||
"Sessions": "Sessions",
|
||||
"Where you're signed in": "Where you're signed in",
|
||||
"Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.",
|
||||
"Sign out": "Sign out",
|
||||
"Are you sure you want to sign out of %(count)s sessions?|other": "Are you sure you want to sign out of %(count)s sessions?",
|
||||
"Are you sure you want to sign out of %(count)s sessions?|one": "Are you sure you want to sign out of %(count)s session?",
|
||||
|
@ -1814,11 +1794,13 @@
|
|||
"Sign out devices|other": "Sign out devices",
|
||||
"Sign out devices|one": "Sign out device",
|
||||
"Authentication": "Authentication",
|
||||
"Failed to set display name": "Failed to set display name",
|
||||
"Rename session": "Rename session",
|
||||
"Please be aware that session names are also visible to people you communicate with.": "Please be aware that session names are also visible to people you communicate with.",
|
||||
"Renaming sessions": "Renaming sessions",
|
||||
"Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.",
|
||||
"This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.",
|
||||
"Rename": "Rename",
|
||||
"Session ID": "Session ID",
|
||||
"Last activity": "Last activity",
|
||||
"Application": "Application",
|
||||
|
@ -1877,6 +1859,8 @@
|
|||
"Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer",
|
||||
"Filter devices": "Filter devices",
|
||||
"Show": "Show",
|
||||
"Deselect all": "Deselect all",
|
||||
"Select all": "Select all",
|
||||
"%(count)s sessions selected|other": "%(count)s sessions selected",
|
||||
"%(count)s sessions selected|one": "%(count)s session selected",
|
||||
"Sign in with QR code": "Sign in with QR code",
|
||||
|
|
|
@ -445,27 +445,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
|||
displayName: _td("Force 15s voice broadcast chunk length"),
|
||||
default: false,
|
||||
},
|
||||
"feature_new_device_manager": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Experimental,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("Use new session manager"),
|
||||
default: false,
|
||||
betaInfo: {
|
||||
title: _td("New session manager"),
|
||||
caption: () => (
|
||||
<>
|
||||
<p>{_t("Have greater visibility and control over all your sessions.")}</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Our new sessions manager provides better visibility of all your sessions, " +
|
||||
"and greater control over them including the ability to remotely toggle push notifications.",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
"feature_rust_crypto": {
|
||||
// use the rust matrix-sdk-crypto-js for crypto.
|
||||
isFeature: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue