Switch away from deprecated ReactDOM findDOMNode (#28259)

* Remove unused method getVisibleDecryptionFailures

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch away from ReactDOM findDOMNode

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-22 12:58:45 +01:00 committed by GitHub
parent 19ef3267c0
commit d4cf3881bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 77 additions and 81 deletions

View file

@ -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 {
@ -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: [],
@ -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.
@ -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;
}
}

View file

@ -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";
@ -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> = {};
@ -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

View file

@ -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);

View file

@ -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,
@ -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);
@ -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
@ -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);
@ -1766,45 +1763,6 @@ 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;
@ -1812,7 +1770,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
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;

View file

@ -52,6 +52,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private tooltips: Element[] = [];
private reactRoots: Element[] = [];
private ref = createRef<HTMLDivElement>();
public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>;
@ -84,8 +86,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons
const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre");
if (pres.length > 0) {
const pres = this.ref.current?.getElementsByTagName("pre");
if (pres && pres.length > 0) {
for (let i = 0; i < pres.length; i++) {
// If there already is a div wrapping the codeblock we want to skip this.
// This happens after the codeblock was edited.
@ -477,7 +479,12 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (isEmote) {
return (
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
<div
className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
dir="auto"
ref={this.ref}
>
*&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
@ -490,7 +497,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
if (isNotice) {
return (
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}>
{body}
{widgets}
</div>
@ -498,14 +505,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
if (isCaption) {
return (
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick} ref={this.ref}>
{body}
{widgets}
</div>
);
}
return (
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}>
{body}
{widgets}
</div>

View file

@ -6,7 +6,7 @@ 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, { createRef, KeyboardEvent } from "react";
import React, { createRef, KeyboardEvent, RefObject } from "react";
import classNames from "classnames";
import { flatMap } from "lodash";
import { Room } from "matrix-js-sdk/src/matrix";
@ -45,6 +45,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
public queryRequested?: string;
public debounceCompletionsRequest?: number;
private containerRef = createRef<HTMLDivElement>();
private completionRefs: Record<string, RefObject<HTMLElement>> = {};
public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>;
@ -260,7 +261,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
public componentDidUpdate(prevProps: IProps): void {
this.applyNewProps(prevProps.query, prevProps.room);
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`] as HTMLElement;
const selectedCompletion = this.completionRefs[`completion${this.state.selectionOffset}`]?.current;
if (selectedCompletion) {
selectedCompletion.scrollIntoView({
@ -286,9 +287,13 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
this.onCompletionClicked(componentPosition);
};
const refId = `completion${componentPosition}`;
if (!this.completionRefs[refId]) {
this.completionRefs[refId] = createRef();
}
return React.cloneElement(completion.component, {
"key": j,
"ref": `completion${componentPosition}`,
"ref": this.completionRefs[refId],
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
className,
onClick,

View file

@ -41,6 +41,8 @@ import { mkThread } from "../../../test-utils/threads";
import { createMessageEventContent } from "../../../test-utils/events";
import SettingsStore from "../../../../src/settings/SettingsStore";
import ScrollPanel from "../../../../src/components/structures/ScrollPanel";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
// ScrollPanel calls this, but jsdom doesn't mock it for us
HTMLDivElement.prototype.scrollBy = () => {};
@ -1002,4 +1004,27 @@ describe("TimelinePanel", () => {
await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull());
await waitFor(() => expect(container.querySelector(".mx_RoomView_MessageList")).not.toBeEmptyDOMElement());
});
it("should dump debug logs on Action.DumpDebugLogs", async () => {
const spy = jest.spyOn(console, "debug");
const [, room, events] = setupTestData();
const eventsPage2 = events.slice(1, 2);
// Start with only page 2 of the main events in the window
const [, timelineSet] = mkTimeline(room, eventsPage2);
room.getTimelineSets = jest.fn().mockReturnValue([timelineSet]);
await withScrollPanelMountSpy(async () => {
const { container } = render(<TimelinePanel {...getProps(room, events)} timelineSet={timelineSet} />);
await waitFor(() => expectEvents(container, [events[1]]));
});
defaultDispatcher.fire(Action.DumpDebugLogs);
await waitFor(() =>
expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")),
);
});
});