Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/vector-im/element-web in…
Browse files Browse the repository at this point in the history
…to t3chguy/react18/createRoot
  • Loading branch information
t3chguy committed Oct 22, 2024
2 parents d8aba7f + d4cf388 commit e0bef93
Show file tree
Hide file tree
Showing 21 changed files with 507 additions and 280 deletions.
9 changes: 3 additions & 6 deletions playwright/e2e/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,10 @@ test.describe("Cryptography", function () {
await dialog.getByRole("button", { name: "Continue" }).click();
await copyAndContinue(page);

// When the device is verified, the `Setting up keys` step is skipped
if (!isDeviceVerified) {
const uiaDialogTitle = page.locator(".mx_InteractiveAuthDialog .mx_Dialog_title");
await expect(uiaDialogTitle.getByText("Setting up keys")).toBeVisible();
await expect(uiaDialogTitle.getByText("Setting up keys")).not.toBeVisible();
}
// If the device is unverified, there should be a "Setting up keys" step; however, it
// can be quite quick, and playwright can miss it, so we can't test for it.

// Either way, we end up at a success dialog:
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
await dialog.getByRole("button", { name: "Done" }).click();
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
Expand Down
9 changes: 8 additions & 1 deletion playwright/e2e/pinned-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,14 @@ export class Helpers {
*/
async assertEmptyPinnedMessagesList() {
const rightPanel = this.getRightPanel();
await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`);
await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`, {
css: `
// hide the tooltip "Room information" to avoid flakiness
[data-floating-ui-portal] {
display: none !important;
}
`,
});
}

/**
Expand Down
6 changes: 6 additions & 0 deletions playwright/e2e/pinned-messages/pinned-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ test.describe("Pinned messages", () => {
const tile = util.getEventTile("Msg1");
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
mask: [tile.locator(".mx_MessageTimestamp")],
css: `
// Hide the jump to bottom button in the timeline to avoid flakiness
.mx_JumpToBottomButton {
display: none !important;
}
`,
});
});

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions src/CreateCrossSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { logger } from "matrix-js-sdk/src/logger";
import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";

import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";

/**
* Determine if the homeserver allows uploading device keys with only password auth.
* @param cli The Matrix Client to use
* @returns True if the homeserver allows uploading device keys with only password auth, otherwise false
*/
async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> {
try {
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
return false;
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
logger.log("uploadDeviceSigningKeys advertised no flows!");
return false;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
return f.stages.length === 1 && f.stages[0] === "m.login.password";
});
return canUploadKeysWithPasswordOnly;
}
}

/**
* Ensures that cross signing keys are created and uploaded for the user.
* The homeserver may require user-interactive auth to upload the keys, in
* which case the user will be prompted to authenticate. If the homeserver
* allows uploading keys with just an account password and one is provided,
* the keys will be uploaded without user interaction.
*
* This function does not set up backups of the created cross-signing keys
* (or message keys): the cross-signing keys are stored locally and will be
* lost requiring a crypto reset, if the user logs out or loses their session.
*
* @param cli The Matrix Client to use
* @param isTokenLogin True if the user logged in via a token login, otherwise false
* @param accountPassword The password that the user logged in with
*/
export async function createCrossSigning(
cli: MatrixClient,
isTokenLogin: boolean,
accountPassword?: string,
): Promise<void> {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) {
throw new Error("No crypto API found!");
}

const doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => {
if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) {
await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: cli.getUserId(),
},
password: accountPassword,
});
} else if (isTokenLogin) {
// We are hoping the grace period is active
await makeRequest({});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
body: _t("auth|uia|sso_preauth_body"),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("encryption|confirm_encryption_setup_title"),
body: _t("encryption|confirm_encryption_setup_body"),
continueText: _t("action|confirm"),
continueKind: "primary",
},
};

const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"),
matrixClient: cli,
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
};

await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: doBootstrapUIAuth,
});
}
30 changes: 16 additions & 14 deletions src/NodeAnimator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import React, { Key, MutableRefObject, ReactElement, ReactInstance } from "react";
import ReactDom from "react-dom";
import React, { Key, MutableRefObject, ReactElement, RefCallback } from "react";

interface IChildProps {
style: React.CSSProperties;
ref: (node: React.ReactInstance) => void;
ref: RefCallback<HTMLElement>;
}

interface IProps {
Expand All @@ -36,7 +35,7 @@ function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number
* automatic positional animation, look at react-shuffle or similar libraries.
*/
export default class NodeAnimator extends React.Component<IProps> {
private nodes: Record<string, ReactInstance> = {};
private nodes: Record<string, HTMLElement> = {};
private children: { [key: string]: ReactElement } = {};
public static defaultProps: Partial<IProps> = {
startStyles: [],
Expand Down Expand Up @@ -71,10 +70,10 @@ export default class NodeAnimator extends React.Component<IProps> {
if (!isReactElement(c)) return;
if (oldChildren[c.key!]) {
const old = oldChildren[c.key!];
const oldNode = ReactDom.findDOMNode(this.nodes[old.key!]);
const oldNode = this.nodes[old.key!];

if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) {
this.applyStyles(oldNode as HTMLElement, { left: c.props.style.left });
if (oldNode && oldNode.style.left !== c.props.style.left) {
this.applyStyles(oldNode, { left: c.props.style.left });
}
// clone the old element with the props (and children) of the new element
// so prop updates are still received by the children.
Expand All @@ -98,26 +97,29 @@ export default class NodeAnimator extends React.Component<IProps> {
});
}

private collectNode(k: Key, node: React.ReactInstance, restingStyle: React.CSSProperties): void {
private collectNode(k: Key, domNode: HTMLElement | null, restingStyle: React.CSSProperties): void {
const key = typeof k === "bigint" ? Number(k) : k;
if (node && this.nodes[key] === undefined && this.props.startStyles.length > 0) {
if (domNode && this.nodes[key] === undefined && this.props.startStyles.length > 0) {
const startStyles = this.props.startStyles;
const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc.
for (let i = 1; i < startStyles.length; ++i) {
this.applyStyles(domNode as HTMLElement, startStyles[i]);
this.applyStyles(domNode, startStyles[i]);
}

// and then we animate to the resting state
window.setTimeout(() => {
this.applyStyles(domNode as HTMLElement, restingStyle);
this.applyStyles(domNode, restingStyle);
}, 0);
}
this.nodes[key] = node;
if (domNode) {
this.nodes[key] = domNode;
} else {
delete this.nodes[key];
}

if (this.props.innerRef) {
this.props.innerRef.current = node;
this.props.innerRef.current = domNode;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.E2E_SETUP) {
view = (
<E2eSetup
matrixClient={MatrixClientPeg.safeGet()}
onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.stores.accountPasswordStore.getPassword()}
tokenLogin={!!this.tokenLogin}
Expand Down
7 changes: 3 additions & 4 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { createRef, ReactNode, TransitionEvent } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
Expand Down Expand Up @@ -245,7 +244,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {

private readMarkerNode = createRef<HTMLLIElement>();
private whoIsTyping = createRef<WhoIsTypingTile>();
private scrollPanel = createRef<ScrollPanel>();
public scrollPanel = createRef<ScrollPanel>();

private readonly showTypingNotificationsWatcherRef: string;
private eventTiles: Record<string, UnwrappedEventTile> = {};
Expand Down Expand Up @@ -376,13 +375,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// +1: read marker is below the window
public getReadMarkerPosition(): number | null {
const readMarker = this.readMarkerNode.current;
const messageWrapper = this.scrollPanel.current;
const messageWrapper = this.scrollPanel.current?.divScroll;

if (!readMarker || !messageWrapper) {
return null;
}

const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect();
const wrapperRect = messageWrapper.getBoundingClientRect();
const readMarkerRect = readMarker.getBoundingClientRect();

// the read-marker pretends to have zero height when it is actually
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/ScrollPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default class ScrollPanel extends React.Component<IProps> {
private bottomGrowth!: number;
private minListHeight!: number;
private heightUpdateInProgress = false;
private divScroll: HTMLDivElement | null = null;
public divScroll: HTMLDivElement | null = null;

public constructor(props: IProps) {
super(props);
Expand Down
64 changes: 11 additions & 53 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { createRef, ReactNode } from "react";
import ReactDOM from "react-dom";
import {
Room,
RoomEvent,
Expand Down Expand Up @@ -67,9 +66,6 @@ const READ_RECEIPT_INTERVAL_MS = 500;

const READ_MARKER_DEBOUNCE_MS = 100;

// How far off-screen a decryption failure can be for it to still count as "visible"
const VISIBLE_DECRYPTION_FAILURE_MARGIN = 100;

const debuglog = (...args: any[]): void => {
if (SettingsStore.getValue("debug_timeline_panel")) {
logger.log.call(console, "TimelinePanel debuglog:", ...args);
Expand Down Expand Up @@ -398,6 +394,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
}
}

private get messagePanelDiv(): HTMLDivElement | null {
return this.messagePanel.current?.scrollPanel.current?.divScroll ?? null;
}

/**
* Logs out debug info to describe the state of the TimelinePanel and the
* events in the room according to the matrix-js-sdk. This is useful when
Expand All @@ -418,15 +418,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
// And we can suss out any corrupted React `key` problems.
let renderedEventIds: string[] | undefined;
try {
const messagePanel = this.messagePanel.current;
if (messagePanel) {
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
if (messagePanelNode) {
const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]");
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
return renderedEvent.getAttribute("data-event-id")!;
});
}
const messagePanelNode = this.messagePanelDiv;
if (messagePanelNode) {
const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]");
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
return renderedEvent.getAttribute("data-event-id")!;
});
}
} catch (err) {
logger.error(`onDumpDebugLogs: Failed to get the actual event ID's in the DOM`, err);
Expand Down Expand Up @@ -1766,53 +1763,14 @@ class TimelinePanel extends React.Component<IProps, IState> {
return index > -1 ? index : null;
}

/**
* Get a list of undecryptable events currently visible on-screen.
*
* @param {boolean} addMargin Whether to add an extra margin beyond the viewport
* where events are still considered "visible"
*
* @returns {MatrixEvent[] | null} A list of undecryptable events, or null if
* the list of events could not be determined.
*/
public getVisibleDecryptionFailures(addMargin?: boolean): MatrixEvent[] | null {
const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;

const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const margin = addMargin ? VISIBLE_DECRYPTION_FAILURE_MARGIN : 0;
const screenTop = wrapperRect.top - margin;
const screenBottom = wrapperRect.bottom + margin;

const result: MatrixEvent[] = [];
for (const ev of this.state.liveEvents) {
const eventId = ev.getId();
if (!eventId) continue;
const node = messagePanel.getNodeForEventId(eventId);
if (!node) continue;

const boundingRect = node.getBoundingClientRect();
if (boundingRect.top > screenBottom) {
// we have gone past the visible section of timeline
break;
} else if (boundingRect.bottom >= screenTop) {
// the tile for this event is in the visible part of the screen (or just above/below it).
if (ev.isDecryptionFailure()) result.push(ev);
}
}
return result;
}

private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;

const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;

const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
const messagePanelNode = this.messagePanelDiv;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.safeGet().credentials.userId;
Expand Down
Loading

0 comments on commit e0bef93

Please sign in to comment.