mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 01:05:42 +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
|
// check that the device considers itself verified
|
||||||
cy.findByRole("button", { name: "User menu" }).click();
|
cy.findByRole("button", { name: "User menu" }).click();
|
||||||
cy.findByRole("menuitem", { name: "Security & Privacy" }).click();
|
cy.findByRole("menuitem", { name: "All settings" }).click();
|
||||||
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
|
cy.findByRole("tab", { name: "Sessions" }).click();
|
||||||
"have.class",
|
cy.findByTestId("current-session-section").within(() => {
|
||||||
"mx_E2EIcon_verified",
|
cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified");
|
||||||
);
|
});
|
||||||
|
|
||||||
// check that cross-signing keys have been uploaded.
|
// check that cross-signing keys have been uploaded.
|
||||||
checkDeviceIsCrossSigned();
|
checkDeviceIsCrossSigned();
|
||||||
|
|
|
@ -24,7 +24,6 @@ describe("Device manager", () => {
|
||||||
let user: UserCredentials | undefined;
|
let user: UserCredentials | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.enableLabsFeature("feature_new_device_manager");
|
|
||||||
cy.startHomeserver("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
homeserver = data;
|
homeserver = data;
|
||||||
|
|
||||||
|
|
|
@ -316,7 +316,6 @@
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
@import "./views/settings/_CrossSigningPanel.pcss";
|
@import "./views/settings/_CrossSigningPanel.pcss";
|
||||||
@import "./views/settings/_CryptographyPanel.pcss";
|
@import "./views/settings/_CryptographyPanel.pcss";
|
||||||
@import "./views/settings/_DevicesPanel.pcss";
|
|
||||||
@import "./views/settings/_FontScalingPanel.pcss";
|
@import "./views/settings/_FontScalingPanel.pcss";
|
||||||
@import "./views/settings/_ImageSizePanel.pcss";
|
@import "./views/settings/_ImageSizePanel.pcss";
|
||||||
@import "./views/settings/_IntegrationManager.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";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the correct device manager section
|
* Open user device manager settings
|
||||||
* Based on the labs setting
|
|
||||||
*/
|
*/
|
||||||
export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => {
|
export const viewUserDeviceSettings = (): void => {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case Action.ViewUserDeviceSettings: {
|
case Action.ViewUserDeviceSettings: {
|
||||||
viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager"));
|
viewUserDeviceSettings();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.ViewUserSettings: {
|
case Action.ViewUserSettings: {
|
||||||
|
|
|
@ -45,7 +45,6 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
mjolnirEnabled: boolean;
|
mjolnirEnabled: boolean;
|
||||||
newSessionManagerEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||||
|
@ -56,15 +55,11 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||||
newSessionManagerEnabled: SettingsStore.getValue("feature_new_device_manager"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.settingsWatchers = [
|
this.settingsWatchers = [SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged)];
|
||||||
SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged),
|
|
||||||
SettingsStore.watchSetting("feature_new_device_manager", null, this.sessionManagerChanged),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
|
@ -76,11 +71,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
this.setState({ mjolnirEnabled: newValue });
|
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>> {
|
private getTabs(): NonEmptyArray<Tab<UserTab>> {
|
||||||
const tabs: Tab<UserTab>[] = [];
|
const tabs: Tab<UserTab>[] = [];
|
||||||
|
|
||||||
|
@ -160,18 +150,16 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
"UserSettingsSecurityPrivacy",
|
"UserSettingsSecurityPrivacy",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (this.state.newSessionManagerEnabled) {
|
tabs.push(
|
||||||
tabs.push(
|
new Tab(
|
||||||
new Tab(
|
UserTab.SessionManager,
|
||||||
UserTab.SessionManager,
|
_td("Sessions"),
|
||||||
_td("Sessions"),
|
"mx_UserSettingsDialog_sessionsIcon",
|
||||||
"mx_UserSettingsDialog_sessionsIcon",
|
<SessionManagerTab />,
|
||||||
<SessionManagerTab />,
|
// don't track with posthog while under construction
|
||||||
// don't track with posthog while under construction
|
undefined,
|
||||||
undefined,
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
// Show the Labs tab if enabled or if there are any active betas
|
// Show the Labs tab if enabled or if there are any active betas
|
||||||
if (
|
if (
|
||||||
SdkConfig.get("show_labs_settings") ||
|
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 E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||||
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../../../dispatcher/payloads";
|
||||||
import CryptographyPanel from "../../CryptographyPanel";
|
import CryptographyPanel from "../../CryptographyPanel";
|
||||||
import DevicesPanel from "../../DevicesPanel";
|
|
||||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
import CrossSigningPanel from "../../CrossSigningPanel";
|
import CrossSigningPanel from "../../CrossSigningPanel";
|
||||||
import EventIndexPanel from "../../EventIndexPanel";
|
import EventIndexPanel from "../../EventIndexPanel";
|
||||||
|
@ -38,8 +37,6 @@ import InlineSpinner from "../../../elements/InlineSpinner";
|
||||||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||||
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
|
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
|
||||||
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
|
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 type { IServerVersions } from "matrix-js-sdk/src/matrix";
|
||||||
import SettingsTab from "../SettingsTab";
|
import SettingsTab from "../SettingsTab";
|
||||||
import { SettingsSection } from "../../shared/SettingsSection";
|
import { SettingsSection } from "../../shared/SettingsSection";
|
||||||
|
@ -83,10 +80,7 @@ interface IState {
|
||||||
waitingUnignored: string[];
|
waitingUnignored: string[];
|
||||||
managingInvites: boolean;
|
managingInvites: boolean;
|
||||||
invitedRoomIds: Set<string>;
|
invitedRoomIds: Set<string>;
|
||||||
showLoginWithQR: Mode | null;
|
|
||||||
versions?: IServerVersions;
|
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> {
|
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
|
@ -103,7 +97,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
waitingUnignored: [],
|
waitingUnignored: [],
|
||||||
managingInvites: false,
|
managingInvites: false,
|
||||||
invitedRoomIds,
|
invitedRoomIds,
|
||||||
showLoginWithQR: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +114,6 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
MatrixClientPeg.get()
|
MatrixClientPeg.get()
|
||||||
.getVersions()
|
.getVersions()
|
||||||
.then((versions) => this.setState({ versions }));
|
.then((versions) => this.setState({ versions }));
|
||||||
MatrixClientPeg.get()
|
|
||||||
.getCapabilities()
|
|
||||||
.then((capabilities) => this.setState({ capabilities }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
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 {
|
public render(): React.ReactNode {
|
||||||
const secureBackup = (
|
const secureBackup = (
|
||||||
<SettingsSubsection heading={_t("Secure Backup")}>
|
<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 (
|
return (
|
||||||
<SettingsTab>
|
<SettingsTab>
|
||||||
{warning}
|
{warning}
|
||||||
{devicesSection}
|
|
||||||
<SettingsSection heading={_t("Encryption")}>
|
<SettingsSection heading={_t("Encryption")}>
|
||||||
{secureBackup}
|
{secureBackup}
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
|
|
|
@ -987,10 +987,6 @@
|
||||||
"Favourite Messages": "Favourite Messages",
|
"Favourite Messages": "Favourite Messages",
|
||||||
"Under active development.": "Under active development.",
|
"Under active development.": "Under active development.",
|
||||||
"Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length",
|
"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",
|
"Rust cryptography implementation": "Rust cryptography implementation",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
|
@ -1382,21 +1378,6 @@
|
||||||
"Cryptography": "Cryptography",
|
"Cryptography": "Cryptography",
|
||||||
"Session ID:": "Session ID:",
|
"Session ID:": "Session ID:",
|
||||||
"Session key:": "Session key:",
|
"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.",
|
"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.|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.",
|
"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.",
|
"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",
|
"Failed to save your profile": "Failed to save your profile",
|
||||||
"The operation could not be completed": "The operation could not be completed",
|
"The operation could not be completed": "The operation could not be completed",
|
||||||
|
"Display Name": "Display Name",
|
||||||
"Profile picture": "Profile picture",
|
"Profile picture": "Profile picture",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Delete Backup": "Delete Backup",
|
"Delete Backup": "Delete Backup",
|
||||||
|
@ -1660,8 +1642,6 @@
|
||||||
"Privacy": "Privacy",
|
"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.",
|
"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",
|
"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",
|
"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?|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?",
|
"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|other": "Sign out devices",
|
||||||
"Sign out devices|one": "Sign out device",
|
"Sign out devices|one": "Sign out device",
|
||||||
"Authentication": "Authentication",
|
"Authentication": "Authentication",
|
||||||
|
"Failed to set display name": "Failed to set display name",
|
||||||
"Rename session": "Rename session",
|
"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.",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"Session ID": "Session ID",
|
||||||
"Last activity": "Last activity",
|
"Last activity": "Last activity",
|
||||||
"Application": "Application",
|
"Application": "Application",
|
||||||
|
@ -1877,6 +1859,8 @@
|
||||||
"Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer",
|
"Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer",
|
||||||
"Filter devices": "Filter devices",
|
"Filter devices": "Filter devices",
|
||||||
"Show": "Show",
|
"Show": "Show",
|
||||||
|
"Deselect all": "Deselect all",
|
||||||
|
"Select all": "Select all",
|
||||||
"%(count)s sessions selected|other": "%(count)s sessions selected",
|
"%(count)s sessions selected|other": "%(count)s sessions selected",
|
||||||
"%(count)s sessions selected|one": "%(count)s session selected",
|
"%(count)s sessions selected|one": "%(count)s session selected",
|
||||||
"Sign in with QR code": "Sign in with QR code",
|
"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"),
|
displayName: _td("Force 15s voice broadcast chunk length"),
|
||||||
default: false,
|
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": {
|
"feature_rust_crypto": {
|
||||||
// use the rust matrix-sdk-crypto-js for crypto.
|
// use the rust matrix-sdk-crypto-js for crypto.
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
|
|
|
@ -26,23 +26,12 @@ describe("viewUserDeviceSettings()", () => {
|
||||||
dispatchSpy.mockClear();
|
dispatchSpy.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dispatches action to view new session manager when enabled", () => {
|
it("dispatches action to view session manager", () => {
|
||||||
const isNewDeviceManagerEnabled = true;
|
viewUserDeviceSettings();
|
||||||
viewUserDeviceSettings(isNewDeviceManagerEnabled);
|
|
||||||
|
|
||||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.SessionManager,
|
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({
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.Security,
|
initialTabId: UserTab.SessionManager,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -117,10 +117,7 @@ describe("<UserSettingsDialog />", () => {
|
||||||
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
|
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders session manager tab when enabled", () => {
|
it("renders session manager tab", () => {
|
||||||
mockSettingsStore.getValue.mockImplementation((settingName): any => {
|
|
||||||
return settingName === "feature_new_device_manager";
|
|
||||||
});
|
|
||||||
const { getByTestId } = render(getComponent());
|
const { getByTestId } = render(getComponent());
|
||||||
expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
|
expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -153,28 +150,15 @@ describe("<UserSettingsDialog />", () => {
|
||||||
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy();
|
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy();
|
||||||
|
|
||||||
expect(mockSettingsStore.watchSetting.mock.calls[0][0]).toEqual("feature_mjolnir");
|
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
|
// call the watch setting callback
|
||||||
watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true);
|
watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true);
|
||||||
// tab is rendered now
|
// tab is rendered now
|
||||||
expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy();
|
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();
|
unmount();
|
||||||
|
|
||||||
// unwatches settings on unmount
|
// unwatches settings on unmount
|
||||||
expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir");
|
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
|
Security & Privacy
|
||||||
</span>
|
</span>
|
||||||
</li>,
|
</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
|
<li
|
||||||
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
aria-controls="mx_tabpanel_USER_LABS_TAB"
|
||||||
aria-selected="false"
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { fireEvent, render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { UNSTABLE_MSC3882_CAPABILITY } from "matrix-js-sdk/src/matrix";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
|
import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
|
||||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
|
||||||
import {
|
import {
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
mockClientMethodsServer,
|
mockClientMethodsServer,
|
||||||
|
@ -27,7 +25,6 @@ import {
|
||||||
mockClientMethodsCrypto,
|
mockClientMethodsCrypto,
|
||||||
mockClientMethodsDevice,
|
mockClientMethodsDevice,
|
||||||
mockPlatformPeg,
|
mockPlatformPeg,
|
||||||
flushPromises,
|
|
||||||
} from "../../../../../test-utils";
|
} from "../../../../../test-utils";
|
||||||
|
|
||||||
describe("<SecurityUserSettingsTab />", () => {
|
describe("<SecurityUserSettingsTab />", () => {
|
||||||
|
@ -44,16 +41,6 @@ describe("<SecurityUserSettingsTab />", () => {
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
getIgnoredUsers: jest.fn(),
|
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 = () => (
|
const getComponent = () => (
|
||||||
|
@ -62,44 +49,14 @@ describe("<SecurityUserSettingsTab />", () => {
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const settingsValueSpy = jest.spyOn(SettingsStore, "getValue");
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
settingsValueSpy.mockReturnValue(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders sessions section when new session manager is disabled", () => {
|
it("renders security section", () => {
|
||||||
settingsValueSpy.mockReturnValue(false);
|
const { container } = render(getComponent());
|
||||||
const { getByTestId } = render(getComponent());
|
|
||||||
|
|
||||||
expect(getByTestId("devices-section")).toBeTruthy();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -79,59 +79,6 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</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