mirror of
https://github.com/element-hq/element-web
synced 2024-11-21 16:55:34 +03:00
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
|
@ -89,11 +89,11 @@ describe("Registration", () => {
|
|||
|
||||
// check that the device considers itself verified
|
||||
cy.findByRole("button", { name: "User menu" }).click();
|
||||
cy.findByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
|
||||
"have.class",
|
||||
"mx_E2EIcon_verified",
|
||||
);
|
||||
cy.findByRole("menuitem", { name: "All settings" }).click();
|
||||
cy.findByRole("tab", { name: "Sessions" }).click();
|
||||
cy.findByTestId("current-session-section").within(() => {
|
||||
cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified");
|
||||
});
|
||||
|
||||
// check that cross-signing keys have been uploaded.
|
||||
checkDeviceIsCrossSigned();
|
||||
|
|
|
@ -24,7 +24,6 @@ describe("Device manager", () => {
|
|||
let user: UserCredentials | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_new_device_manager");
|
||||
cy.startHomeserver("default").then((data) => {
|
||||
homeserver = data;
|
||||
|
||||
|
|
|
@ -316,7 +316,6 @@
|
|||
@import "./views/settings/_AvatarSetting.pcss";
|
||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||
@import "./views/settings/_CryptographyPanel.pcss";
|
||||
@import "./views/settings/_DevicesPanel.pcss";
|
||||
@import "./views/settings/_FontScalingPanel.pcss";
|
||||
@import "./views/settings/_ImageSizePanel.pcss";
|
||||
@import "./views/settings/_IntegrationManager.pcss";
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_DevicesPanel {
|
||||
width: auto;
|
||||
max-width: 880px;
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-block: 10px;
|
||||
|
||||
.mx_DevicesPanel_header_title {
|
||||
font-size: $font-18px;
|
||||
font-weight: var(--font-semi-bold);
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_selectButton {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.mx_E2EIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_deleteButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_device {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-block: 10px;
|
||||
min-height: 35px;
|
||||
padding: 0 $spacing-8;
|
||||
|
||||
.mx_DeviceTypeIcon {
|
||||
/* hide the new device type in legacy device list
|
||||
for backwards compat reasons */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_icon {
|
||||
margin-left: 0px;
|
||||
margin-right: $spacing-16;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_deviceInfo {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_deviceName {
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_lastSeen {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_deviceButtons {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.mx_DevicesPanel_renameForm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.mx_Field_input {
|
||||
width: 240px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -26,23 +26,12 @@ describe("viewUserDeviceSettings()", () => {
|
|||
dispatchSpy.mockClear();
|
||||
});
|
||||
|
||||
it("dispatches action to view new session manager when enabled", () => {
|
||||
const isNewDeviceManagerEnabled = true;
|
||||
viewUserDeviceSettings(isNewDeviceManagerEnabled);
|
||||
it("dispatches action to view session manager", () => {
|
||||
viewUserDeviceSettings();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.SessionManager,
|
||||
});
|
||||
});
|
||||
|
||||
it("dispatches action to view old session manager when disabled", () => {
|
||||
const isNewDeviceManagerEnabled = false;
|
||||
viewUserDeviceSettings(isNewDeviceManagerEnabled);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -173,7 +173,7 @@ describe("<MatrixChat />", () => {
|
|||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
initialTabId: UserTab.SessionManager,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -117,10 +117,7 @@ describe("<UserSettingsDialog />", () => {
|
|||
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders session manager tab when enabled", () => {
|
||||
mockSettingsStore.getValue.mockImplementation((settingName): any => {
|
||||
return settingName === "feature_new_device_manager";
|
||||
});
|
||||
it("renders session manager tab", () => {
|
||||
const { getByTestId } = render(getComponent());
|
||||
expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
|
||||
});
|
||||
|
@ -153,28 +150,15 @@ describe("<UserSettingsDialog />", () => {
|
|||
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy();
|
||||
|
||||
expect(mockSettingsStore.watchSetting.mock.calls[0][0]).toEqual("feature_mjolnir");
|
||||
expect(mockSettingsStore.watchSetting.mock.calls[1][0]).toEqual("feature_new_device_manager");
|
||||
|
||||
// call the watch setting callback
|
||||
watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true);
|
||||
// tab is rendered now
|
||||
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy();
|
||||
|
||||
// call the watch setting callback
|
||||
watchSettingCallbacks["feature_new_device_manager"](
|
||||
"feature_new_device_manager",
|
||||
"",
|
||||
SettingLevel.ACCOUNT,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
// tab is rendered now
|
||||
expect(queryByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
// unwatches settings on unmount
|
||||
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir");
|
||||
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_new_device_manager");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -128,6 +128,24 @@ NodeList [
|
|||
Security & Privacy
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_SESSION_MANAGER_TAB"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_TabbedView_tabLabel"
|
||||
data-testid="settings-tab-USER_SESSION_MANAGER_TAB"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_TabbedView_maskedIcon mx_UserSettingsDialog_sessionsIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_TabbedView_tabLabel_text"
|
||||
id="mx_tabpanel_USER_SESSION_MANAGER_TAB_label"
|
||||
>
|
||||
Sessions
|
||||
</span>
|
||||
</li>,
|
||||
<li
|
||||
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
||||
aria-selected="false"
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 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 { act, fireEvent, render } from "@testing-library/react";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { PUSHER_DEVICE_ID, PUSHER_ENABLED } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import DevicesPanel from "../../../../src/components/views/settings/DevicesPanel";
|
||||
import { flushPromises, getMockClientWithEventEmitter, mkPusher, mockClientMethodsUser } from "../../../test-utils";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
describe("<DevicesPanel />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
|
||||
// the local device
|
||||
const ownDevice = { device_id: "device_1" };
|
||||
|
||||
// a device which we have verified via cross-signing
|
||||
const verifiedDevice = { device_id: "device_2" };
|
||||
|
||||
// a device which we have *not* verified via cross-signing
|
||||
const unverifiedDevice = { device_id: "device_3" };
|
||||
|
||||
// a device which is returned by `getDevices` but getDeviceVerificationStatus returns `null` for
|
||||
// (as it would for a device with no E2E keys).
|
||||
const nonCryptoDevice = { device_id: "non_crypto" };
|
||||
|
||||
const mockCrypto = {
|
||||
getDeviceVerificationStatus: jest.fn().mockImplementation((_userId, deviceId) => {
|
||||
if (_userId !== userId) {
|
||||
throw new Error(`bad user id ${_userId}`);
|
||||
}
|
||||
if (deviceId === ownDevice.device_id || deviceId === verifiedDevice.device_id) {
|
||||
return { crossSigningVerified: true };
|
||||
} else if (deviceId === unverifiedDevice.device_id) {
|
||||
return {
|
||||
crossSigningVerified: false,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
};
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getDevices: jest.fn(),
|
||||
getDeviceId: jest.fn().mockReturnValue(ownDevice.device_id),
|
||||
deleteMultipleDevices: jest.fn(),
|
||||
getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")),
|
||||
generateClientSecret: jest.fn(),
|
||||
getPushers: jest.fn(),
|
||||
setPusher: jest.fn(),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
});
|
||||
|
||||
const getComponent = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DevicesPanel />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockClient.getDevices
|
||||
.mockReset()
|
||||
.mockResolvedValue({ devices: [ownDevice, verifiedDevice, unverifiedDevice, nonCryptoDevice] });
|
||||
|
||||
mockClient.getPushers.mockReset().mockResolvedValue({
|
||||
pushers: [
|
||||
mkPusher({
|
||||
[PUSHER_DEVICE_ID.name]: ownDevice.device_id,
|
||||
[PUSHER_ENABLED.name]: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders device panel with devices", async () => {
|
||||
const { container } = render(getComponent());
|
||||
await flushPromises();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("device deletion", () => {
|
||||
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
|
||||
|
||||
const toggleDeviceSelection = (container: HTMLElement, deviceId: string) =>
|
||||
act(() => {
|
||||
const checkbox = container.querySelector(`#device-tile-checkbox-${deviceId}`)!;
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.deleteMultipleDevices.mockReset();
|
||||
});
|
||||
|
||||
it("deletes selected devices when interactive auth is not required", async () => {
|
||||
mockClient.deleteMultipleDevices.mockResolvedValue({});
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
|
||||
|
||||
const { container, getByTestId } = render(getComponent());
|
||||
await flushPromises();
|
||||
|
||||
expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(3);
|
||||
|
||||
toggleDeviceSelection(container, verifiedDevice.device_id);
|
||||
|
||||
mockClient.getDevices.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("sign-out-devices-btn"));
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// devices refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
// and rerendered
|
||||
expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2);
|
||||
});
|
||||
|
||||
it("deletes selected devices when interactive auth is required", async () => {
|
||||
mockClient.deleteMultipleDevices
|
||||
// require auth
|
||||
.mockRejectedValueOnce(interactiveAuthError)
|
||||
// then succeed
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
|
||||
|
||||
const { container, getByTestId, getByLabelText } = render(getComponent());
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// reset mock count after initial load
|
||||
mockClient.getDevices.mockClear();
|
||||
|
||||
toggleDeviceSelection(container, verifiedDevice.device_id);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("sign-out-devices-btn"));
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
// modal rendering has some weird sleeps
|
||||
await sleep(100);
|
||||
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined);
|
||||
|
||||
const modal = document.getElementsByClassName("mx_Dialog");
|
||||
expect(modal).toMatchSnapshot();
|
||||
|
||||
// fill password and submit for interactive auth
|
||||
act(() => {
|
||||
fireEvent.change(getByLabelText("Password"), { target: { value: "topsecret" } });
|
||||
fireEvent.submit(getByLabelText("Password"));
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// called again with auth
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], {
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: userId,
|
||||
},
|
||||
password: "",
|
||||
type: "m.login.password",
|
||||
user: userId,
|
||||
});
|
||||
// devices refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
// and rerendered
|
||||
expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2);
|
||||
});
|
||||
|
||||
it("clears loading state when interactive auth fail is cancelled", async () => {
|
||||
mockClient.deleteMultipleDevices
|
||||
// require auth
|
||||
.mockRejectedValueOnce(interactiveAuthError)
|
||||
// then succeed
|
||||
.mockResolvedValueOnce({});
|
||||
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] })
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] });
|
||||
|
||||
const { container, getByTestId } = render(getComponent());
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// reset mock count after initial load
|
||||
mockClient.getDevices.mockClear();
|
||||
|
||||
toggleDeviceSelection(container, verifiedDevice.device_id);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId("sign-out-devices-btn"));
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
|
||||
await flushPromises();
|
||||
// modal rendering has some weird sleeps
|
||||
await sleep(20);
|
||||
|
||||
// close the modal without submission
|
||||
act(() => {
|
||||
const modalCloseButton = document.querySelector('[aria-label="Close dialog"]')!;
|
||||
fireEvent.click(modalCloseButton);
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// not refreshed
|
||||
expect(mockClient.getDevices).not.toHaveBeenCalled();
|
||||
// spinner removed
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,506 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DevicesPanel /> device deletion deletes selected devices when interactive auth is required 1`] = `
|
||||
HTMLCollection [
|
||||
<div
|
||||
class="mx_Dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_Dialog_content"
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_InteractiveAuthDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header mx_Dialog_headerWithCancel"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Authentication
|
||||
</h2>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="mx_Dialog_content"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
Confirm your identity by entering your account password below.
|
||||
</p>
|
||||
<form
|
||||
class="mx_InteractiveAuthEntryComponents_passwordSection"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Password"
|
||||
name="passwordField"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_button_row"
|
||||
>
|
||||
<input
|
||||
class="mx_Dialog_primary"
|
||||
disabled=""
|
||||
type="submit"
|
||||
value="Continue"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`<DevicesPanel /> renders device panel with devices 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DevicesPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_header"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_title"
|
||||
>
|
||||
This device
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_device mx_DevicesPanel_myDevice"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_deviceTrust"
|
||||
>
|
||||
<span
|
||||
class="mx_DevicesPanel_icon mx_E2EIcon mx_E2EIcon_verified"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-device_1"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Verified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon verified"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_info"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
device_1
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Verified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
device_1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign Out
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
class="mx_DevicesPanel_header"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_trust"
|
||||
>
|
||||
<span
|
||||
class="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_verified"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_title"
|
||||
>
|
||||
Verified devices
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_device"
|
||||
>
|
||||
<div
|
||||
class="mx_SelectableDeviceTile"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_SelectableDeviceTile_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
data-testid="device-tile-checkbox-device_2"
|
||||
id="device-tile-checkbox-device_2"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-tile-checkbox-device_2"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-device_2"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Verified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon verified"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_info"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
device_2
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Verified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
device_2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
class="mx_DevicesPanel_header"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_trust"
|
||||
>
|
||||
<span
|
||||
class="mx_DevicesPanel_header_icon mx_E2EIcon mx_E2EIcon_warning"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_title"
|
||||
>
|
||||
Unverified devices
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_device"
|
||||
>
|
||||
<div
|
||||
class="mx_SelectableDeviceTile"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_SelectableDeviceTile_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
data-testid="device-tile-checkbox-device_3"
|
||||
id="device-tile-checkbox-device_3"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-tile-checkbox-device_3"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-device_3"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_info"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
device_3
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
device_3
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
class="mx_DevicesPanel_header"
|
||||
>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_trust"
|
||||
/>
|
||||
<div
|
||||
class="mx_DevicesPanel_header_title"
|
||||
>
|
||||
Devices without encryption support
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DevicesPanel_device"
|
||||
>
|
||||
<div
|
||||
class="mx_SelectableDeviceTile"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_SelectableDeviceTile_checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
data-testid="device-tile-checkbox-non_crypto"
|
||||
id="device-tile-checkbox-non_crypto"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-tile-checkbox-non_crypto"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_DeviceTile"
|
||||
data-testid="device-tile-non_crypto"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon"
|
||||
>
|
||||
<div
|
||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="Unknown session type"
|
||||
class="mx_DeviceTypeIcon_deviceIcon"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Unverified"
|
||||
class="mx_DeviceTypeIcon_verificationIcon unverified"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_info"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
non_crypto
|
||||
</h4>
|
||||
<div
|
||||
class="mx_DeviceTile_metadata"
|
||||
>
|
||||
<span
|
||||
data-testid="device-metadata-isVerified"
|
||||
>
|
||||
Unverified
|
||||
</span>
|
||||
·
|
||||
<span
|
||||
data-testid="device-metadata-deviceId"
|
||||
>
|
||||
non_crypto
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DeviceTile_actions"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-disabled="true"
|
||||
class="mx_AccessibleButton mx_DevicesPanel_deleteButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_outline mx_AccessibleButton_disabled"
|
||||
data-testid="sign-out-devices-btn"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Sign out 0 selected devices
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -13,13 +13,11 @@ 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 { fireEvent, render } from "@testing-library/react";
|
||||
import { UNSTABLE_MSC3882_CAPABILITY } from "matrix-js-sdk/src/matrix";
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsServer,
|
||||
|
@ -27,7 +25,6 @@ import {
|
|||
mockClientMethodsCrypto,
|
||||
mockClientMethodsDevice,
|
||||
mockPlatformPeg,
|
||||
flushPromises,
|
||||
} from "../../../../../test-utils";
|
||||
|
||||
describe("<SecurityUserSettingsTab />", () => {
|
||||
|
@ -44,16 +41,6 @@ describe("<SecurityUserSettingsTab />", () => {
|
|||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3886": true,
|
||||
},
|
||||
}),
|
||||
getCapabilities: jest.fn().mockResolvedValue({
|
||||
[UNSTABLE_MSC3882_CAPABILITY.name]: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const getComponent = () => (
|
||||
|
@ -62,44 +49,14 @@ describe("<SecurityUserSettingsTab />", () => {
|
|||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg();
|
||||
jest.clearAllMocks();
|
||||
settingsValueSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("renders sessions section when new session manager is disabled", () => {
|
||||
settingsValueSpy.mockReturnValue(false);
|
||||
const { getByTestId } = render(getComponent());
|
||||
it("renders security section", () => {
|
||||
const { container } = render(getComponent());
|
||||
|
||||
expect(getByTestId("devices-section")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render sessions section when new session manager is enabled", () => {
|
||||
settingsValueSpy.mockReturnValue(true);
|
||||
const { queryByTestId } = render(getComponent());
|
||||
|
||||
expect(queryByTestId("devices-section")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("renders qr code login section", async () => {
|
||||
const { getByText } = render(getComponent());
|
||||
|
||||
// wait for versions call to settle
|
||||
await flushPromises();
|
||||
|
||||
expect(getByText("Sign in with QR code")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("enters qr code login section when show QR code button clicked", async () => {
|
||||
const { getByText, getByTestId } = render(getComponent());
|
||||
// wait for versions call to settle
|
||||
await flushPromises();
|
||||
|
||||
fireEvent.click(getByText("Show QR code"));
|
||||
|
||||
expect(getByTestId("login-with-qr")).toBeTruthy();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -79,59 +79,6 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BetaCard_columns"
|
||||
>
|
||||
<div
|
||||
class="mx_BetaCard_columns_description"
|
||||
>
|
||||
<h3
|
||||
class="mx_BetaCard_title"
|
||||
>
|
||||
<span>
|
||||
New session manager
|
||||
</span>
|
||||
<span
|
||||
class="mx_BetaCard_betaPill"
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="mx_BetaCard_caption"
|
||||
>
|
||||
<p>
|
||||
Have greater visibility and control over all your sessions.
|
||||
</p>
|
||||
<p>
|
||||
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>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard_buttons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Join the beta
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard_columns_image_wrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mx_BetaCard_columns_image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SecurityUserSettingsTab /> renders security section 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_SettingsTab"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsTab_sections"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSection"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2"
|
||||
>
|
||||
Encryption
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSection_subSections"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Secure Backup
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.
|
||||
</div>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<details>
|
||||
<summary>
|
||||
Advanced
|
||||
</summary>
|
||||
<table
|
||||
class="mx_SecureBackupPanel_statusList"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Backup key stored:
|
||||
</th>
|
||||
<td>
|
||||
not stored
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Backup key cached:
|
||||
</th>
|
||||
<td>
|
||||
not found locally
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Secret storage public key:
|
||||
</th>
|
||||
<td>
|
||||
not found
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Secret storage:
|
||||
</th>
|
||||
<td>
|
||||
not ready
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Message search
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<span>
|
||||
Element can't securely cache encrypted messages locally while running in a web browser. Use
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="https://element.io/get-started"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Element Desktop
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
for encrypted messages to appear in search results.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Cross-signing
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<details>
|
||||
<summary>
|
||||
Advanced
|
||||
</summary>
|
||||
<table
|
||||
class="mx_CrossSigningPanel_statusList"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Cross-signing public keys:
|
||||
</th>
|
||||
<td>
|
||||
not found
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Cross-signing private keys:
|
||||
</th>
|
||||
<td>
|
||||
not found in storage
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Master private key:
|
||||
</th>
|
||||
<td>
|
||||
not found locally
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Self signing private key:
|
||||
</th>
|
||||
<td>
|
||||
not found locally
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
User signing private key:
|
||||
</th>
|
||||
<td>
|
||||
not found locally
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Homeserver feature support:
|
||||
</th>
|
||||
<td>
|
||||
not found
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Cryptography
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
<table
|
||||
class="mx_CryptographyPanel_sessionInfo"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Session ID:
|
||||
</th>
|
||||
<td>
|
||||
<code />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
scope="row"
|
||||
>
|
||||
Session key:
|
||||
</th>
|
||||
<td>
|
||||
<code>
|
||||
<b>
|
||||
<not supported>
|
||||
</b>
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Never send encrypted messages to unverified sessions from this session
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Never send encrypted messages to unverified sessions from this session"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSection"
|
||||
>
|
||||
<h2
|
||||
class="mx_Heading_h2"
|
||||
>
|
||||
Advanced
|
||||
</h2>
|
||||
<div
|
||||
class="mx_SettingsSection_subSections"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Ignored users
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
You have no ignored users.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsSubsectionHeading"
|
||||
>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_SettingsSubsectionHeading_heading"
|
||||
>
|
||||
Encryption
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_content"
|
||||
>
|
||||
<div
|
||||
class="mx_SettingsFlag"
|
||||
>
|
||||
<label
|
||||
class="mx_SettingsFlag_label"
|
||||
>
|
||||
<span
|
||||
class="mx_SettingsFlag_labelText"
|
||||
>
|
||||
Manually verify all remote sessions
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
aria-checked="false"
|
||||
aria-disabled="false"
|
||||
aria-label="Manually verify all remote sessions"
|
||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_SettingsSubsection_text"
|
||||
>
|
||||
Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
Loading…
Reference in a new issue