Skip to content

Commit d4cf388

Browse files
authored
Switch away from deprecated ReactDOM findDOMNode (#28259)
* Remove unused method getVisibleDecryptionFailures Signed-off-by: Michael Telatynski <[email protected]> * Switch away from ReactDOM findDOMNode Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent 19ef326 commit d4cf388

File tree

7 files changed

+77
-81
lines changed

7 files changed

+77
-81
lines changed

src/NodeAnimator.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
66
Please see LICENSE files in the repository root for full details.
77
*/
88

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

1211
interface IChildProps {
1312
style: React.CSSProperties;
14-
ref: (node: React.ReactInstance) => void;
13+
ref: RefCallback<HTMLElement>;
1514
}
1615

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

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

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

112110
// and then we animate to the resting state
113111
window.setTimeout(() => {
114-
this.applyStyles(domNode as HTMLElement, restingStyle);
112+
this.applyStyles(domNode, restingStyle);
115113
}, 0);
116114
}
117-
this.nodes[key] = node;
115+
if (domNode) {
116+
this.nodes[key] = domNode;
117+
} else {
118+
delete this.nodes[key];
119+
}
118120

119121
if (this.props.innerRef) {
120-
this.props.innerRef.current = node;
122+
this.props.innerRef.current = domNode;
121123
}
122124
}
123125

src/components/structures/MessagePanel.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

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

246245
private readMarkerNode = createRef<HTMLLIElement>();
247246
private whoIsTyping = createRef<WhoIsTypingTile>();
248-
private scrollPanel = createRef<ScrollPanel>();
247+
public scrollPanel = createRef<ScrollPanel>();
249248

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

381380
if (!readMarker || !messageWrapper) {
382381
return null;
383382
}
384383

385-
const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect();
384+
const wrapperRect = messageWrapper.getBoundingClientRect();
386385
const readMarkerRect = readMarker.getBoundingClientRect();
387386

388387
// the read-marker pretends to have zero height when it is actually

src/components/structures/ScrollPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export default class ScrollPanel extends React.Component<IProps> {
186186
private bottomGrowth!: number;
187187
private minListHeight!: number;
188188
private heightUpdateInProgress = false;
189-
private divScroll: HTMLDivElement | null = null;
189+
public divScroll: HTMLDivElement | null = null;
190190

191191
public constructor(props: IProps) {
192192
super(props);

src/components/structures/TimelinePanel.tsx

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

99
import React, { createRef, ReactNode } from "react";
10-
import ReactDOM from "react-dom";
1110
import {
1211
Room,
1312
RoomEvent,
@@ -67,9 +66,6 @@ const READ_RECEIPT_INTERVAL_MS = 500;
6766

6867
const READ_MARKER_DEBOUNCE_MS = 100;
6968

70-
// How far off-screen a decryption failure can be for it to still count as "visible"
71-
const VISIBLE_DECRYPTION_FAILURE_MARGIN = 100;
72-
7369
const debuglog = (...args: any[]): void => {
7470
if (SettingsStore.getValue("debug_timeline_panel")) {
7571
logger.log.call(console, "TimelinePanel debuglog:", ...args);
@@ -398,6 +394,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
398394
}
399395
}
400396

397+
private get messagePanelDiv(): HTMLDivElement | null {
398+
return this.messagePanel.current?.scrollPanel.current?.divScroll ?? null;
399+
}
400+
401401
/**
402402
* Logs out debug info to describe the state of the TimelinePanel and the
403403
* 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> {
418418
// And we can suss out any corrupted React `key` problems.
419419
let renderedEventIds: string[] | undefined;
420420
try {
421-
const messagePanel = this.messagePanel.current;
422-
if (messagePanel) {
423-
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
424-
if (messagePanelNode) {
425-
const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]");
426-
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
427-
return renderedEvent.getAttribute("data-event-id")!;
428-
});
429-
}
421+
const messagePanelNode = this.messagePanelDiv;
422+
if (messagePanelNode) {
423+
const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]");
424+
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
425+
return renderedEvent.getAttribute("data-event-id")!;
426+
});
430427
}
431428
} catch (err) {
432429
logger.error(`onDumpDebugLogs: Failed to get the actual event ID's in the DOM`, err);
@@ -1766,53 +1763,14 @@ class TimelinePanel extends React.Component<IProps, IState> {
17661763
return index > -1 ? index : null;
17671764
}
17681765

1769-
/**
1770-
* Get a list of undecryptable events currently visible on-screen.
1771-
*
1772-
* @param {boolean} addMargin Whether to add an extra margin beyond the viewport
1773-
* where events are still considered "visible"
1774-
*
1775-
* @returns {MatrixEvent[] | null} A list of undecryptable events, or null if
1776-
* the list of events could not be determined.
1777-
*/
1778-
public getVisibleDecryptionFailures(addMargin?: boolean): MatrixEvent[] | null {
1779-
const messagePanel = this.messagePanel.current;
1780-
if (!messagePanel) return null;
1781-
1782-
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
1783-
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
1784-
const wrapperRect = messagePanelNode.getBoundingClientRect();
1785-
const margin = addMargin ? VISIBLE_DECRYPTION_FAILURE_MARGIN : 0;
1786-
const screenTop = wrapperRect.top - margin;
1787-
const screenBottom = wrapperRect.bottom + margin;
1788-
1789-
const result: MatrixEvent[] = [];
1790-
for (const ev of this.state.liveEvents) {
1791-
const eventId = ev.getId();
1792-
if (!eventId) continue;
1793-
const node = messagePanel.getNodeForEventId(eventId);
1794-
if (!node) continue;
1795-
1796-
const boundingRect = node.getBoundingClientRect();
1797-
if (boundingRect.top > screenBottom) {
1798-
// we have gone past the visible section of timeline
1799-
break;
1800-
} else if (boundingRect.bottom >= screenTop) {
1801-
// the tile for this event is in the visible part of the screen (or just above/below it).
1802-
if (ev.isDecryptionFailure()) result.push(ev);
1803-
}
1804-
}
1805-
return result;
1806-
}
1807-
18081766
private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
18091767
const ignoreOwn = opts.ignoreOwn || false;
18101768
const allowPartial = opts.allowPartial || false;
18111769

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

1815-
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
1773+
const messagePanelNode = this.messagePanelDiv;
18161774
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
18171775
const wrapperRect = messagePanelNode.getBoundingClientRect();
18181776
const myUserId = MatrixClientPeg.safeGet().credentials.userId;

src/components/views/messages/TextualBody.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
5252
private tooltips: Element[] = [];
5353
private reactRoots: Element[] = [];
5454

55+
private ref = createRef<HTMLDivElement>();
56+
5557
public static contextType = RoomContext;
5658
public declare context: React.ContextType<typeof RoomContext>;
5759

@@ -84,8 +86,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
8486

8587
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
8688
// Handle expansion and add buttons
87-
const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre");
88-
if (pres.length > 0) {
89+
const pres = this.ref.current?.getElementsByTagName("pre");
90+
if (pres && pres.length > 0) {
8991
for (let i = 0; i < pres.length; i++) {
9092
// If there already is a div wrapping the codeblock we want to skip this.
9193
// This happens after the codeblock was edited.
@@ -477,7 +479,12 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
477479

478480
if (isEmote) {
479481
return (
480-
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
482+
<div
483+
className="mx_MEmoteBody mx_EventTile_content"
484+
onClick={this.onBodyLinkClick}
485+
dir="auto"
486+
ref={this.ref}
487+
>
481488
*&nbsp;
482489
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
483490
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
@@ -490,22 +497,22 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
490497
}
491498
if (isNotice) {
492499
return (
493-
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
500+
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}>
494501
{body}
495502
{widgets}
496503
</div>
497504
);
498505
}
499506
if (isCaption) {
500507
return (
501-
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
508+
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick} ref={this.ref}>
502509
{body}
503510
{widgets}
504511
</div>
505512
);
506513
}
507514
return (
508-
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
515+
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}>
509516
{body}
510517
{widgets}
511518
</div>

src/components/views/rooms/Autocomplete.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React, { createRef, KeyboardEvent } from "react";
9+
import React, { createRef, KeyboardEvent, RefObject } from "react";
1010
import classNames from "classnames";
1111
import { flatMap } from "lodash";
1212
import { Room } from "matrix-js-sdk/src/matrix";
@@ -45,6 +45,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
4545
public queryRequested?: string;
4646
public debounceCompletionsRequest?: number;
4747
private containerRef = createRef<HTMLDivElement>();
48+
private completionRefs: Record<string, RefObject<HTMLElement>> = {};
4849

4950
public static contextType = RoomContext;
5051
public declare context: React.ContextType<typeof RoomContext>;
@@ -260,7 +261,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
260261
public componentDidUpdate(prevProps: IProps): void {
261262
this.applyNewProps(prevProps.query, prevProps.room);
262263
// this is the selected completion, so scroll it into view if needed
263-
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`] as HTMLElement;
264+
const selectedCompletion = this.completionRefs[`completion${this.state.selectionOffset}`]?.current;
264265

265266
if (selectedCompletion) {
266267
selectedCompletion.scrollIntoView({
@@ -286,9 +287,13 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
286287
this.onCompletionClicked(componentPosition);
287288
};
288289

290+
const refId = `completion${componentPosition}`;
291+
if (!this.completionRefs[refId]) {
292+
this.completionRefs[refId] = createRef();
293+
}
289294
return React.cloneElement(completion.component, {
290295
"key": j,
291-
"ref": `completion${componentPosition}`,
296+
"ref": this.completionRefs[refId],
292297
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
293298
className,
294299
onClick,

test/unit-tests/components/structures/TimelinePanel-test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { mkThread } from "../../../test-utils/threads";
4141
import { createMessageEventContent } from "../../../test-utils/events";
4242
import SettingsStore from "../../../../src/settings/SettingsStore";
4343
import ScrollPanel from "../../../../src/components/structures/ScrollPanel";
44+
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
45+
import { Action } from "../../../../src/dispatcher/actions";
4446

4547
// ScrollPanel calls this, but jsdom doesn't mock it for us
4648
HTMLDivElement.prototype.scrollBy = () => {};
@@ -1002,4 +1004,27 @@ describe("TimelinePanel", () => {
10021004
await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull());
10031005
await waitFor(() => expect(container.querySelector(".mx_RoomView_MessageList")).not.toBeEmptyDOMElement());
10041006
});
1007+
1008+
it("should dump debug logs on Action.DumpDebugLogs", async () => {
1009+
const spy = jest.spyOn(console, "debug");
1010+
1011+
const [, room, events] = setupTestData();
1012+
const eventsPage2 = events.slice(1, 2);
1013+
1014+
// Start with only page 2 of the main events in the window
1015+
const [, timelineSet] = mkTimeline(room, eventsPage2);
1016+
room.getTimelineSets = jest.fn().mockReturnValue([timelineSet]);
1017+
1018+
await withScrollPanelMountSpy(async () => {
1019+
const { container } = render(<TimelinePanel {...getProps(room, events)} timelineSet={timelineSet} />);
1020+
1021+
await waitFor(() => expectEvents(container, [events[1]]));
1022+
});
1023+
1024+
defaultDispatcher.fire(Action.DumpDebugLogs);
1025+
1026+
await waitFor(() =>
1027+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("TimelinePanel(Room): Debugging info for roomId")),
1028+
);
1029+
});
10051030
});

0 commit comments

Comments
 (0)