Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Element-R: Populate device list for right-panel #10671

Merged
merged 18 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1299226
Use `getUserDeviceInfo` instead of `downloadKeys` and `getStoredDevic…
florianduros Apr 20, 2023
0d5d7e4
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 24, 2023
90f3d19
Use new `getUserDeviceInfo` api in `UserInfo.tsx` and `UserInfo-test.…
florianduros Apr 24, 2023
9fb0058
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 24, 2023
f34b443
Fix missing fields
florianduros Apr 24, 2023
ff107f1
Use `getUserDeviceInfo` instead of `downloadKeys`
florianduros Apr 24, 2023
f6af384
Move `ManualDeviceKeyVerificationDialog.tsx` from class to functional…
florianduros Apr 25, 2023
1caee6d
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 25, 2023
0d622df
Fix strict errors
florianduros Apr 25, 2023
169a4e3
Update snapshot
florianduros Apr 25, 2023
29aa12a
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 25, 2023
1845e92
Add snapshot test to `UserInfo-test.tsx`
florianduros Apr 25, 2023
acdf7eb
Add test for <BasicUserInfo />
florianduros Apr 25, 2023
ccbbe71
Remove useless TODO comment
florianduros Apr 25, 2023
4bf5ab5
Add test for ambiguous device
florianduros Apr 25, 2023
74c8ed1
Rework `<BasicUserInfo />` test
florianduros Apr 25, 2023
ad00fff
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 25, 2023
f2b438e
Merge branch 'develop' into florianduros/feat/getUserDeviceInfo-right…
florianduros Apr 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 64 additions & 56 deletions src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,72 +18,80 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import React, { useCallback } from "react";
import { Device } from "matrix-js-sdk/src/models/device";

import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as FormattingUtils from "../../../utils/FormattingUtils";
import { _t } from "../../../languageHandler";
import QuestionDialog from "./QuestionDialog";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";

interface IProps {
interface IManualDeviceKeyVerificationDialogProps {
userId: string;
device: DeviceInfo;
onFinished(confirm?: boolean): void;
device: Device;
onFinished?(confirm?: boolean): void;
}

export default class ManualDeviceKeyVerificationDialog extends React.Component<IProps> {
private onLegacyFinished = (confirm: boolean): void => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(this.props.userId, this.props.device.deviceId, true);
}
this.props.onFinished(confirm);
};
export function ManualDeviceKeyVerificationDialog({
userId,
device,
onFinished,
}: IManualDeviceKeyVerificationDialogProps): JSX.Element {
const mxClient = useMatrixClientContext();

public render(): React.ReactNode {
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:");
} else {
text = _t("Confirm this user's session by comparing the following with their User Settings:");
}
const onLegacyFinished = useCallback(
(confirm: boolean) => {
if (confirm && mxClient) {
mxClient.setDeviceVerified(userId, device.deviceId, true);
}
onFinished?.(confirm);
},
[mxClient, userId, device, onFinished],
);

const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<p>{text}</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li>
<label>{_t("Session name")}:</label> <span>{this.props.device.getDisplayName()}</span>
</li>
<li>
<label>{_t("Session ID")}:</label>{" "}
<span>
<code>{this.props.device.deviceId}</code>
</span>
</li>
<li>
<label>{_t("Session key")}:</label>{" "}
<span>
<code>
<b>{key}</b>
</code>
</span>
</li>
</ul>
</div>
<p>{_t("If they don't match, the security of your communication may be compromised.")}</p>
let text;
if (mxClient?.getUserId() === userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:");
} else {
text = _t("Confirm this user's session by comparing the following with their User Settings:");
}

const fingerprint = device.getFingerprint();
const key = fingerprint && FormattingUtils.formatCryptoKey(fingerprint);
const body = (
<div>
<p>{text}</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li>
<label>{_t("Session name")}:</label> <span>{device.displayName}</span>
</li>
<li>
<label>{_t("Session ID")}:</label>{" "}
<span>
<code>{device.deviceId}</code>
</span>
</li>
<li>
<label>{_t("Session key")}:</label>{" "}
<span>
<code>
<b>{key}</b>
</code>
</span>
</li>
</ul>
</div>
);
<p>{_t("If they don't match, the security of your communication may be compromised.")}</p>
</div>
);

return (
<QuestionDialog
title={_t("Verify session")}
description={body}
button={_t("Verify session")}
onFinished={this.onLegacyFinished}
/>
);
}
return (
<QuestionDialog
title={_t("Verify session")}
description={body}
button={_t("Verify session")}
onFinished={onLegacyFinished}
/>
);
}
2 changes: 1 addition & 1 deletion src/components/views/dialogs/UntrustedDeviceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
<div className="mx_Dialog_content" id="mx_Dialog_content">
<p>{newSessionText}</p>
<p>
{device.getDisplayName()} ({device.deviceId})
{device.displayName} ({device.deviceId})
</p>
<p>{askToVerifyText}</p>
</div>
Expand Down
37 changes: 24 additions & 13 deletions src/components/views/right_panel/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { Device } from "matrix-js-sdk/src/models/device";

import dis from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
Expand Down Expand Up @@ -81,14 +81,14 @@ import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-me
import { SdkContextClass } from "../../../contexts/SDKContext";
import { asyncSome } from "../../../utils/arrays";

export interface IDevice extends DeviceInfo {
export interface IDevice extends Device {
ambiguous?: boolean;
}

export const disambiguateDevices = (devices: IDevice[]): void => {
const names = Object.create(null);
for (let i = 0; i < devices.length; i++) {
const name = devices[i].getDisplayName() ?? "";
const name = devices[i].displayName ?? "";
const indexList = names[name] || [];
indexList.push(i);
names[name] = indexList;
Expand Down Expand Up @@ -149,7 +149,8 @@ function useHasCrossSigningKeys(
}
setUpdating(true);
try {
await cli.downloadKeys([member.userId]);
// We call it to populate the user keys and devices
await cli.getCrypto()?.getUserDeviceInfo([member.userId], true);
const xsi = cli.getStoredCrossSigningForUser(member.userId);
const key = xsi && xsi.getId();
return !!key;
Expand Down Expand Up @@ -195,12 +196,10 @@ export function DeviceItem({ userId, device }: { userId: string; device: IDevice
};

let deviceName;
if (!device.getDisplayName()?.trim()) {
if (!device.displayName?.trim()) {
deviceName = device.deviceId;
} else {
deviceName = device.ambiguous
? device.getDisplayName() + " (" + device.deviceId + ")"
: device.getDisplayName();
deviceName = device.ambiguous ? device.displayName + " (" + device.deviceId + ")" : device.displayName;
}

let trustedLabel: string | undefined;
Expand Down Expand Up @@ -1190,6 +1189,19 @@ export const PowerLevelEditor: React.FC<{
);
};

async function getUserDeviceInfo(
userId: string,
cli: MatrixClient,
downloadUncached = false,
): Promise<Device[] | undefined> {
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], downloadUncached);
const devicesMap = userDeviceMap?.get(userId);

if (!devicesMap) return;

return Array.from(devicesMap.values());
}

export const useDevices = (userId: string): IDevice[] | undefined | null => {
const cli = useContext(MatrixClientContext);

Expand All @@ -1203,10 +1215,9 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => {

async function downloadDeviceList(): Promise<void> {
try {
await cli.downloadKeys([userId], true);
const devices = cli.getStoredDevicesForUser(userId);
const devices = await getUserDeviceInfo(userId, cli, true);

if (cancelled) {
if (cancelled || !devices) {
// we got cancelled - presumably a different user now
return;
}
Expand All @@ -1229,8 +1240,8 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => {
useEffect(() => {
let cancel = false;
const updateDevices = async (): Promise<void> => {
const newDevices = cli.getStoredDevicesForUser(userId);
if (cancel) return;
const newDevices = await getUserDeviceInfo(userId, cli);
if (cancel || !newDevices) return;
setDevices(newDevices);
};
const onDevicesUpdated = (users: string[]): void => {
Expand Down
2 changes: 1 addition & 1 deletion src/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
import { accessSecretStorage } from "./SecurityManager";
import UntrustedDeviceDialog from "./components/views/dialogs/UntrustedDeviceDialog";
import { IDevice } from "./components/views/right_panel/UserInfo";
import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog";
import { ManualDeviceKeyVerificationDialog } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog";
import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState";
import { findDMForUser } from "./utils/dm/findDMForUser";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 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 { render, screen } from "@testing-library/react";
import { Device } from "matrix-js-sdk/src/models/device";
import { MatrixClient } from "matrix-js-sdk/src/client";

import { stubClient } from "../../../test-utils";
import { ManualDeviceKeyVerificationDialog } from "../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";

describe("ManualDeviceKeyVerificationDialog", () => {
let mockClient: MatrixClient;

function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) {
return render(
<MatrixClientContext.Provider value={mockClient}>
<ManualDeviceKeyVerificationDialog userId={userId} device={device} onFinished={onLegacyFinished} />
</MatrixClientContext.Provider>,
);
}

beforeEach(() => {
mockClient = stubClient();
});

it("should display the device", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn());

// Then
expect(container).toMatchSnapshot();
});

it("should display the device of another user", () => {
// When
const userId = "@alice:example.com";
const deviceId = "XYZ";
const device = new Device({
userId,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(userId, device, jest.fn());

// Then
expect(container).toMatchSnapshot();
});

it("should call onFinished and matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);

screen.getByRole("button", { name: "Verify session" }).click();

// Then
expect(onFinished).toHaveBeenCalledWith(true);
expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true);
});

it("should call onFinished and not matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);

screen.getByRole("button", { name: "Cancel" }).click();

// Then
expect(onFinished).toHaveBeenCalledWith(false);
expect(mockClient.setDeviceVerified).not.toHaveBeenCalled();
});
});
Loading