@@ -2639,7 +2628,6 @@ export class RoomView extends React.Component { { private heightUpdateInProgress = false; public divScroll: HTMLDivElement | null = null; - public constructor(props: IProps) { - super(props); + public static contextType = SDKContext; + declare public context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.resetScrollState(); } public componentDidMount(): void { this.unmounted = false; - this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); + this.context?.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); this.checkScroll(); } @@ -217,14 +216,14 @@ export default class ScrollPanel extends React.Component { // (We could use isMounted(), but facebook have deprecated that.) this.unmounted = true; - this.props.resizeNotifier?.removeListener("middlePanelResizedNoisy", this.onResize); + this.context?.resizeNotifier?.removeListener("middlePanelResizedNoisy", this.onResize); this.divScroll = null; } private onScroll = (ev: Event): void => { // skip scroll events caused by resizing - if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; + if (this.context?.resizeNotifier && this.context.resizeNotifier.isResizing) return; debuglog("onScroll called past resize gate; scroll node top:", this.getScrollNode().scrollTop); this.scrollTimeout?.restart(); this.saveScrollState(); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 59ec657b02..c2aa7f2f10 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -763,7 +763,7 @@ export default class SpaceRoomView extends React.PureComponent { return (
- + {this.renderBody()} diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 8346a0ab31..c112d341b3 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -47,7 +47,6 @@ import shouldHideEvent from "../../shouldHideEvent"; import MessagePanel from "./MessagePanel"; import { type IScrollState } from "./ScrollPanel"; import { type ActionPayload } from "../../dispatcher/payloads"; -import type ResizeNotifier from "../../utils/ResizeNotifier"; import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Spinner from "../views/elements/Spinner"; import type EditorStateTransfer from "../../utils/EditorStateTransfer"; @@ -123,7 +122,6 @@ interface IProps { // whether to always show timestamps for an event alwaysShowTimestamps?: boolean; - resizeNotifier?: ResizeNotifier; editState?: EditorStateTransfer; permalinkCreator?: RoomPermalinkCreator; membersLoaded?: boolean; @@ -1849,7 +1847,6 @@ class TimelinePanel extends React.Component { this.state.alwaysShowTimestamps } className={this.props.className} - resizeNotifier={this.props.resizeNotifier} getRelationsForEvent={this.getRelationsForEvent} editState={this.props.editState} showReactions={this.props.showReactions} diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 58aed9932b..ded92b6eb9 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -87,12 +87,7 @@ export default class UserView extends React.Component { /> ); return ( - + ); diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 3983b286f1..a02a807c9b 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -41,7 +41,7 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize
- + { membersLoaded={true} editState={this.state.editState} eventId={this.state.initialEventId} - resizeNotifier={this.props.resizeNotifier} highlightedEventId={highlightedEventId} onScroll={this.onScroll} /> diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index ac6328c3bf..1bfef03963 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -27,11 +27,11 @@ import UIStore from "../../../stores/UIStore"; import { type ActionPayload } from "../../../dispatcher/payloads"; import Spinner from "../elements/Spinner"; import SdkConfig from "../../../SdkConfig"; +import { SDKContext } from "../../../contexts/SDKContext"; interface IProps { userId: string; room: Room; - resizeNotifier: ResizeNotifier; showApps?: boolean; // Should apps be rendered maxHeight: number; role?: AriaRole; @@ -57,8 +57,11 @@ export default class AppsDrawer extends React.Component { showApps: true, }; - public constructor(props: IProps) { - super(props); + public static contextType = SDKContext; + declare public context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { apps: this.getApps(), @@ -73,7 +76,7 @@ export default class AppsDrawer extends React.Component { public componentDidMount(): void { this.unmounted = false; - this.props.resizeNotifier.on("isResizing", this.onIsResizing); + this.context.resizeNotifier.on("isResizing", this.onIsResizing); ScalarMessaging.startListening(); WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); @@ -88,7 +91,7 @@ export default class AppsDrawer extends React.Component { if (this.resizeContainer) { this.resizer.detach(); } - this.props.resizeNotifier.off("isResizing", this.onIsResizing); + this.context.resizeNotifier.off("isResizing", this.onIsResizing); } private onIsResizing = (resizing: boolean): void => { @@ -281,7 +284,7 @@ export default class AppsDrawer extends React.Component { className="mx_AppsDrawer_resizer" handleWrapperClass="mx_AppsDrawer_resizer_container" handleClass="mx_AppsDrawer_resizer_container_handle" - resizeNotifier={this.props.resizeNotifier} + resizeNotifier={this.context.resizeNotifier} > {appContainers} diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 9e34a6f40d..f547c29d2e 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -13,7 +13,6 @@ import AppsDrawer from "./AppsDrawer"; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { UIFeature } from "../../../settings/UIFeature"; -import type ResizeNotifier from "../../../utils/ResizeNotifier"; import LegacyCallViewForRoom from "../voip/LegacyCallViewForRoom"; import { objectHasDiff } from "../../../utils/objects"; @@ -22,7 +21,6 @@ interface IProps { room: Room; userId: string; showApps: boolean; // Render apps - resizeNotifier: ResizeNotifier; children?: ReactNode; } @@ -36,23 +34,12 @@ export default class AuxPanel extends React.Component { } public render(): React.ReactNode { - const callView = ( - - ); + const callView = ; let appsDrawer; if (SettingsStore.getValue(UIFeature.Widgets)) { appsDrawer = ( - + ); } diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index 200b35a73d..552aa2a4b2 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, useEffect, useId, useRef, useState } from "react"; +import React, { type JSX, useContext, useEffect, useId, useRef, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; @@ -25,7 +25,7 @@ import { Action } from "../../../dispatcher/actions"; import MessageEvent from "../messages/MessageEvent"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { EventPreview } from "./EventPreview.tsx"; -import type ResizeNotifier from "../../../utils/ResizeNotifier"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; /** * The props for the {@link PinnedMessageBanner} component. @@ -39,20 +39,12 @@ interface PinnedMessageBannerProps { * The room where the banner is displayed */ room: Room; - /** - * The resize notifier to notify the timeline to resize itself when the banner is displayed or hidden. - */ - resizeNotifier: ResizeNotifier; } /** * A banner that displays the pinned messages in a room. */ -export function PinnedMessageBanner({ - room, - permalinkCreator, - resizeNotifier, -}: PinnedMessageBannerProps): JSX.Element | null { +export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null { const pinnedEventIds = usePinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const eventCount = pinnedEvents.length; @@ -67,7 +59,7 @@ export function PinnedMessageBanner({ const isLastMessage = currentEventIndex === eventCount - 1; const pinnedEvent = pinnedEvents[currentEventIndex]; - useNotifyTimeline(pinnedEvent, resizeNotifier); + useNotifyTimeline(pinnedEvent); const id = useId(); @@ -152,9 +144,10 @@ export function PinnedMessageBanner({ /** * When the banner is displayed or hidden, we want to notify the timeline to resize itself. * @param pinnedEvent - * @param resizeNotifier */ -function useNotifyTimeline(pinnedEvent: MatrixEvent | null, resizeNotifier: ResizeNotifier): void { +function useNotifyTimeline(pinnedEvent: MatrixEvent | null): void { + const resizeNotifier = useContext(SDKContext).resizeNotifier; + const previousEvent = useRef(null); useEffect(() => { // If we switch from a pinned message to no pinned message or the opposite, we want to resize the timeline diff --git a/src/components/views/voip/LegacyCallViewForRoom.tsx b/src/components/views/voip/LegacyCallViewForRoom.tsx index f0bc8ffc22..d996fe98d6 100644 --- a/src/components/views/voip/LegacyCallViewForRoom.tsx +++ b/src/components/views/voip/LegacyCallViewForRoom.tsx @@ -12,14 +12,12 @@ import { Resizable } from "re-resizable"; import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler"; import LegacyCallView from "./LegacyCallView"; -import type ResizeNotifier from "../../../utils/ResizeNotifier"; +import { SDKContext } from "../../../contexts/SDKContext"; interface IProps { // What room we should display the call for roomId: string; - resizeNotifier: ResizeNotifier; - showApps?: boolean; } @@ -33,8 +31,11 @@ interface IState { * or nothing if there is no call in that room. */ export default class LegacyCallViewForRoom extends React.Component { - public constructor(props: IProps) { - super(props); + public static contextType = SDKContext; + declare public context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { + super(props, context); const call = this.getCall(); this.state = { call, @@ -73,15 +74,15 @@ export default class LegacyCallViewForRoom extends React.Component { - this.props.resizeNotifier.startResizing(); + this.context.resizeNotifier.startResizing(); }; private onResize = (): void => { - this.props.resizeNotifier.notifyTimelineHeightChanged(); + this.context.resizeNotifier.notifyTimelineHeightChanged(); }; private onResizeStop = (): void => { - this.props.resizeNotifier.stopResizing(); + this.context.resizeNotifier.stopResizing(); }; private setSidebarShown = (sidebarShown: boolean): void => { diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts index 1b8ede3efd..4e7aa9e94f 100644 --- a/src/contexts/SDKContext.ts +++ b/src/contexts/SDKContext.ts @@ -24,6 +24,7 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore"; import { OidcClientStore } from "../stores/oidc/OidcClientStore"; import WidgetStore from "../stores/WidgetStore"; +import ResizeNotifier from "../utils/ResizeNotifier"; // This context is available to components under MatrixChat, // the context must not be used by components outside a SdkContextClass tree. @@ -64,6 +65,7 @@ export class SdkContextClass { protected _TypingStore?: TypingStore; protected _UserProfilesStore?: UserProfilesStore; protected _OidcClientStore?: OidcClientStore; + protected _ResizeNotifier?: ResizeNotifier; /** * Automatically construct stores which need to be created eagerly so they can register with @@ -171,6 +173,16 @@ export class SdkContextClass { return this._OidcClientStore; } + // This is getting increasingly tenuous to have here but we still have class components so it's + // awkward to consume multiple contexts in them. This should be replaced with ResizeObservers + // anyway really. + public get resizeNotifier(): ResizeNotifier { + if (!this._ResizeNotifier) { + this._ResizeNotifier = new ResizeNotifier(); + } + return this._ResizeNotifier; + } + public onLoggedOut(): void { this._UserProfilesStore = undefined; } diff --git a/test/test-utils/wrappers.tsx b/test/test-utils/wrappers.tsx index 7c4fd4b968..99ceae7f94 100644 --- a/test/test-utils/wrappers.tsx +++ b/test/test-utils/wrappers.tsx @@ -62,3 +62,13 @@ export function withClientContextRenderOptions(client: MatrixClient): RenderOpti ), }; } + +export function clientAndSDKContextRenderOptions(client: MatrixClient, sdkContext: SdkContextClass): RenderOptions { + return { + wrapper: ({ children }) => ( + + {children} + + ), + }; +} diff --git a/test/unit-tests/components/structures/FilePanel-test.tsx b/test/unit-tests/components/structures/FilePanel-test.tsx index 0b9b18c38e..eab6725293 100644 --- a/test/unit-tests/components/structures/FilePanel-test.tsx +++ b/test/unit-tests/components/structures/FilePanel-test.tsx @@ -12,7 +12,6 @@ import { screen, render, waitFor } from "jest-matrix-react"; import { mocked } from "jest-mock"; import FilePanel from "../../../../src/components/structures/FilePanel"; -import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { mkEvent, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -39,9 +38,7 @@ describe("FilePanel", () => { room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); mocked(cli.getRoom).mockReturnValue(room); - const { asFragment } = render( - , - ); + const { asFragment } = render(); await waitFor(() => { expect(screen.getByText("No files visible in this room")).toBeInTheDocument(); }); @@ -64,7 +61,6 @@ describe("FilePanel", () => { { filePanel = ref; }} diff --git a/test/unit-tests/components/structures/MainSplit-test.tsx b/test/unit-tests/components/structures/MainSplit-test.tsx index 2feb9b22f6..da3c29cd36 100644 --- a/test/unit-tests/components/structures/MainSplit-test.tsx +++ b/test/unit-tests/components/structures/MainSplit-test.tsx @@ -10,30 +10,26 @@ import React from "react"; import { render, fireEvent } from "jest-matrix-react"; import MainSplit from "../../../../src/components/structures/MainSplit"; -import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { PosthogAnalytics } from "../../../../src/PosthogAnalytics.ts"; +import { SDKContext, SdkContextClass } from "../../../../src/contexts/SDKContext.ts"; describe("", () => { - const resizeNotifier = new ResizeNotifier(); const children = (
ChildFooBar
); const panel =
Right panel
; + let sdkContext: SdkContextClass; beforeEach(() => { localStorage.clear(); + sdkContext = new SdkContextClass(); }); it("renders", () => { const { asFragment, container } = render( - , + , ); expect(asFragment()).toMatchSnapshot(); // Assert it matches the default width of 320 @@ -42,13 +38,7 @@ describe("", () => { it("respects defaultSize prop", () => { const { asFragment, container } = render( - , + , ); expect(asFragment()).toMatchSnapshot(); // Assert it matches the default width of 350 @@ -59,7 +49,6 @@ describe("", () => { localStorage.setItem("mx_rhs_size_thread", "333"); const { container } = render( ", () => { it("should report to analytics on resize stop", () => { const { container } = render( , + { wrapper: ({ children }) => {children} }, ); const spy = jest.spyOn(PosthogAnalytics.instance, "trackEvent"); const handle = container.querySelector(".mx_ResizeHandle--horizontal")!; + expect(handle).toBeInTheDocument(); fireEvent.mouseDown(handle); fireEvent.resize(handle, { clientX: 0 }); fireEvent.mouseUp(handle); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index 3ebe826b1c..c772f6b743 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -307,6 +307,30 @@ describe("", () => { }); }); + it("should notify resizenotifier when left panel hidden", async () => { + getComponent(); + + jest.spyOn(SdkContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); + + defaultDispatcher.dispatch({ action: "hide_left_panel" }); + + await waitFor(() => + expect(mocked(SdkContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), + ); + }); + + it("should notify resizenotifier when left panel shown", async () => { + getComponent(); + + jest.spyOn(SdkContextClass.instance.resizeNotifier, "notifyLeftHandleResized"); + + defaultDispatcher.dispatch({ action: "show_left_panel" }); + + await waitFor(() => + expect(mocked(SdkContextClass.instance.resizeNotifier.notifyLeftHandleResized)).toHaveBeenCalled(), + ); + }); + describe("when query params have a OIDC params", () => { const issuer = "https://auth.com/"; const homeserverUrl = "https://matrix.org"; diff --git a/test/unit-tests/components/structures/MessagePanel-test.tsx b/test/unit-tests/components/structures/MessagePanel-test.tsx index ae12768c2f..0f3b40e3f0 100644 --- a/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -15,11 +15,11 @@ import { render } from "jest-matrix-react"; import MessagePanel, { shouldFormContinuation } from "../../../../src/components/structures/MessagePanel"; import SettingsStore from "../../../../src/settings/SettingsStore"; -import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; import * as TestUtilsMatrix from "../../../test-utils"; import { + clientAndSDKContextRenderOptions, createTestClient, getMockClientWithEventEmitter, makeBeaconInfoEvent, @@ -32,6 +32,7 @@ import type ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { type IRoomState } from "../../../../src/components/structures/RoomView"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext.ts"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -54,6 +55,7 @@ describe("MessagePanel", function () { getClientWellKnown: jest.fn().mockReturnValue({}), supportsThreads: jest.fn().mockReturnValue(true), }); + let sdkContext: SdkContextClass; jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); const room = new Room(roomId, client, userId); @@ -93,11 +95,9 @@ describe("MessagePanel", function () { } as unknown as IRoomState; const getComponent = (props = {}, roomContext: Partial = {}) => ( - - - - - + + + ); beforeEach(function () { @@ -107,6 +107,8 @@ describe("MessagePanel", function () { return arg === "showDisplaynameChanges"; }); + sdkContext = new SdkContextClass(); + DMRoomMap.makeShared(client); }); @@ -314,7 +316,7 @@ describe("MessagePanel", function () { } it("should show the events", function () { - const { container } = render(getComponent({ events })); + const { container } = render(getComponent({ events }), clientAndSDKContextRenderOptions(client, sdkContext)); // just check we have the right number of tiles for now const tiles = container.getElementsByClassName("mx_EventTile"); @@ -322,7 +324,10 @@ describe("MessagePanel", function () { }); it("should collapse adjacent member events", function () { - const { container } = render(getComponent({ events: mkMelsEvents() })); + const { container } = render( + getComponent({ events: mkMelsEvents() }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); // just check we have the right number of tiles for now const tiles = container.getElementsByClassName("mx_EventTile"); @@ -339,6 +344,7 @@ describe("MessagePanel", function () { readMarkerEventId: events[4].getId(), readMarkerVisible: true, }), + clientAndSDKContextRenderOptions(client, sdkContext), ); const tiles = container.getElementsByClassName("mx_EventTile"); @@ -359,6 +365,7 @@ describe("MessagePanel", function () { readMarkerEventId: melsEvents[4].getId(), readMarkerVisible: true, }), + clientAndSDKContextRenderOptions(client, sdkContext), ); const [summary] = container.getElementsByClassName("mx_GenericEventListSummary"); @@ -381,6 +388,7 @@ describe("MessagePanel", function () { readMarkerEventId: melsEvents[9].getId(), readMarkerVisible: true, }), + clientAndSDKContextRenderOptions(client, sdkContext), ); const [summary] = container.getElementsByClassName("mx_GenericEventListSummary"); @@ -406,6 +414,7 @@ describe("MessagePanel", function () { readMarkerVisible: true, })}
, + clientAndSDKContextRenderOptions(client, sdkContext), ); const tiles = container.getElementsByClassName("mx_EventTile"); @@ -448,7 +457,7 @@ describe("MessagePanel", function () { client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null)); TestUtilsMatrix.upsertRoomStateEvents(room, events); - const { container } = render(getComponent({ events })); + const { container } = render(getComponent({ events }), clientAndSDKContextRenderOptions(client, sdkContext)); // we expect that // - the room creation event, the room encryption event, and Alice inviting Bob, @@ -476,7 +485,10 @@ describe("MessagePanel", function () { }); const combinedEvents = [...events, beaconInfoEvent]; TestUtilsMatrix.upsertRoomStateEvents(room, combinedEvents); - const { container } = render(getComponent({ events: combinedEvents })); + const { container } = render( + getComponent({ events: combinedEvents }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const [summaryTile] = container.getElementsByClassName("mx_GenericEventListSummary"); @@ -498,6 +510,7 @@ describe("MessagePanel", function () { readMarkerEventId: events[5].getId(), readMarkerVisible: true, }), + clientAndSDKContextRenderOptions(client, sdkContext), ); // find the
  • which wraps the read marker @@ -514,7 +527,10 @@ describe("MessagePanel", function () { it("should render Date separators for the events", function () { const events = mkOneDayEvents(); - const { queryAllByRole } = render(getComponent({ events })); + const { queryAllByRole } = render( + getComponent({ events }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const dates = queryAllByRole("separator"); expect(dates.length).toEqual(1); @@ -523,7 +539,10 @@ describe("MessagePanel", function () { it("appends events into summaries during forward pagination without changing key", () => { const events = mkMelsEvents().slice(1, 11); - const { container, rerender } = render(getComponent({ events })); + const { container, rerender } = render( + getComponent({ events }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); let els = container.getElementsByClassName("mx_GenericEventListSummary"); expect(els.length).toEqual(1); expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId()); @@ -553,7 +572,10 @@ describe("MessagePanel", function () { it("prepends events into summaries during backward pagination without changing key", () => { const events = mkMelsEvents().slice(1, 11); - const { container, rerender } = render(getComponent({ events })); + const { container, rerender } = render( + getComponent({ events }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); let els = container.getElementsByClassName("mx_GenericEventListSummary"); expect(els.length).toEqual(1); expect(els[0].getAttribute("data-testid")).toEqual("eventlistsummary-" + events[0].getId()); @@ -583,7 +605,10 @@ describe("MessagePanel", function () { it("assigns different keys to summaries that get split up", () => { const events = mkMelsEvents().slice(1, 11); - const { container, rerender } = render(getComponent({ events })); + const { container, rerender } = render( + getComponent({ events }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); let els = container.getElementsByClassName("mx_GenericEventListSummary"); expect(els.length).toEqual(1); expect(els[0].getAttribute("data-testid")).toEqual(`eventlistsummary-${events[0].getId()}`); @@ -616,7 +641,7 @@ describe("MessagePanel", function () { it("doesn't lookup showHiddenEventsInTimeline while rendering", () => { // We're only interested in the setting lookups that happen on every render, // rather than those happening on first mount, so let's get those out of the way - const { rerender } = render(getComponent({ events: [] })); + const { rerender } = render(getComponent({ events: [] }), clientAndSDKContextRenderOptions(client, sdkContext)); // Set up our spy and re-render with new events const settingsSpy = jest.spyOn(SettingsStore, "getValue").mockClear(); @@ -654,7 +679,10 @@ describe("MessagePanel", function () { ts: 3, }), ]; - const { container } = render(getComponent({ events }, { showHiddenEvents: true })); + const { container } = render( + getComponent({ events }, { showHiddenEvents: true }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const els = container.getElementsByClassName("mx_GenericEventListSummary"); expect(els.length).toEqual(1); @@ -678,7 +706,10 @@ describe("MessagePanel", function () { }), ); } - const { asFragment } = render(getComponent({ events }, { showHiddenEvents: false })); + const { asFragment } = render( + getComponent({ events }, { showHiddenEvents: false }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(asFragment()).toMatchSnapshot(); }); @@ -699,7 +730,10 @@ describe("MessagePanel", function () { }), ); } - const { asFragment } = render(getComponent({ events }, { showHiddenEvents: false })); + const { asFragment } = render( + getComponent({ events }, { showHiddenEvents: false }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(asFragment()).toMatchSnapshot(); }); @@ -720,7 +754,10 @@ describe("MessagePanel", function () { }), ); } - const { asFragment } = render(getComponent({ events }, { showHiddenEvents: true })); + const { asFragment } = render( + getComponent({ events }, { showHiddenEvents: true }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const cpt = asFragment(); // Ignore properties that change every time @@ -751,7 +788,10 @@ describe("MessagePanel", function () { content: { topic: "TOPIC" }, }), ]; - const { container } = render(getComponent({ events, showReadReceipts: true })); + const { container } = render( + getComponent({ events, showReadReceipts: true }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const tiles = container.getElementsByClassName("mx_EventTile"); expect(tiles.length).toEqual(2); @@ -784,7 +824,10 @@ describe("MessagePanel", function () { }, true, ); - const { container } = render(getComponent({ events, showReadReceipts: true })); + const { container } = render( + getComponent({ events, showReadReceipts: true }), + clientAndSDKContextRenderOptions(client, sdkContext), + ); const tiles = container.getElementsByClassName("mx_EventTile"); expect(tiles.length).toEqual(2); diff --git a/test/unit-tests/components/structures/RoomSearchView-test.tsx b/test/unit-tests/components/structures/RoomSearchView-test.tsx index ab52e8012b..ab939a026b 100644 --- a/test/unit-tests/components/structures/RoomSearchView-test.tsx +++ b/test/unit-tests/components/structures/RoomSearchView-test.tsx @@ -20,11 +20,11 @@ import { } from "matrix-js-sdk/src/matrix"; import { RoomSearchView } from "../../../../src/components/structures/RoomSearchView"; -import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { stubClient } from "../../../test-utils"; +import { clientAndSDKContextRenderOptions, stubClient } from "../../../test-utils"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { searchPagination, SearchScope } from "../../../../src/Searching"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; jest.mock("../../../../src/Searching", () => ({ searchPagination: jest.fn(), @@ -33,13 +33,14 @@ jest.mock("../../../../src/Searching", () => ({ describe("", () => { const eventMapper = (obj: Partial) => new MatrixEvent(obj); - const resizeNotifier = new ResizeNotifier(); let client: MatrixClient; + let sdkContext: SdkContextClass; let room: Room; beforeEach(async () => { stubClient(); client = MatrixClientPeg.safeGet(); + sdkContext = new SdkContextClass(); client.supportsThreads = jest.fn().mockReturnValue(true); room = new Room("!room:server", client, client.getSafeUserId()); mocked(client.getRoom).mockReturnValue(room); @@ -60,7 +61,6 @@ describe("", () => { term="search term" scope={SearchScope.All} promise={deferred.promise} - resizeNotifier={resizeNotifier} className="someClass" onUpdate={jest.fn()} />, @@ -71,59 +71,57 @@ describe("", () => { it("should render results when the promise resolves", async () => { render( - - ({ - results: [ - SearchResult.fromJson( - { - rank: 1, - result: { - room_id: room.roomId, - event_id: "$2", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Foo Test Bar", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [ - { - room_id: room.roomId, - event_id: "$1", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Before", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - ], - events_after: [ - { - room_id: room.roomId, - event_id: "$3", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "After", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - ], - }, + ({ + results: [ + SearchResult.fromJson( + { + rank: 1, + result: { + room_id: room.roomId, + event_id: "$2", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Foo Test Bar", msgtype: "m.text" }, + type: EventType.RoomMessage, }, - eventMapper, - ), - ], - highlights: [], - count: 1, - })} - resizeNotifier={resizeNotifier} - className="someClass" - onUpdate={jest.fn()} - /> - , + context: { + profile_info: {}, + events_before: [ + { + room_id: room.roomId, + event_id: "$1", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Before", msgtype: "m.text" }, + type: EventType.RoomMessage, + }, + ], + events_after: [ + { + room_id: room.roomId, + event_id: "$3", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "After", msgtype: "m.text" }, + type: EventType.RoomMessage, + }, + ], + }, + }, + eventMapper, + ), + ], + highlights: [], + count: 1, + })} + className="someClass" + onUpdate={jest.fn()} + />, + clientAndSDKContextRenderOptions(client, sdkContext), ); await screen.findByText("Before"); @@ -133,41 +131,39 @@ describe("", () => { it("should highlight words correctly", async () => { render( - - ({ - results: [ - SearchResult.fromJson( - { - rank: 1, - result: { - room_id: room.roomId, - event_id: "$2", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Foo Test Bar", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [], - events_after: [], - }, + ({ + results: [ + SearchResult.fromJson( + { + rank: 1, + result: { + room_id: room.roomId, + event_id: "$2", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Foo Test Bar", msgtype: "m.text" }, + type: EventType.RoomMessage, }, - eventMapper, - ), - ], - highlights: ["test"], - count: 1, - })} - resizeNotifier={resizeNotifier} - className="someClass" - onUpdate={jest.fn()} - /> - , + context: { + profile_info: {}, + events_before: [], + events_after: [], + }, + }, + eventMapper, + ), + ], + highlights: ["test"], + count: 1, + })} + className="someClass" + onUpdate={jest.fn()} + />, + clientAndSDKContextRenderOptions(client, sdkContext), ); const text = await screen.findByText("Test"); @@ -231,17 +227,15 @@ describe("", () => { const onUpdate = jest.fn(); const { rerender } = render( - - - , + , + clientAndSDKContextRenderOptions(client, sdkContext), ); await screen.findByRole("progressbar"); @@ -249,17 +243,14 @@ describe("", () => { expect(onUpdate).toHaveBeenCalledWith(false, expect.objectContaining({}), null); rerender( - - - , + , ); expect(screen.queryByRole("progressbar")).toBeFalsy(); @@ -275,7 +266,6 @@ describe("", () => { term="search term" scope={SearchScope.All} promise={deferred.promise} - resizeNotifier={resizeNotifier} className="someClass" onUpdate={jest.fn()} /> @@ -299,7 +289,6 @@ describe("", () => { term="search term" scope={SearchScope.All} promise={deferred.promise} - resizeNotifier={resizeNotifier} className="someClass" onUpdate={jest.fn()} /> @@ -324,7 +313,6 @@ describe("", () => { term="search term" scope={SearchScope.All} promise={deferred.promise} - resizeNotifier={resizeNotifier} className="someClass" onUpdate={onUpdate} /> @@ -424,17 +412,15 @@ describe("", () => { }; render( - - - , + , + clientAndSDKContextRenderOptions(client, sdkContext), ); const beforeNode = await screen.findByText("Before"); @@ -459,98 +445,96 @@ describe("", () => { ); render( - - ({ - results: [ - SearchResult.fromJson( - { - rank: 1, - result: { - room_id: room.roomId, - event_id: "$2", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Room 1", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [], - events_after: [], - }, + ({ + results: [ + SearchResult.fromJson( + { + rank: 1, + result: { + room_id: room.roomId, + event_id: "$2", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Room 1", msgtype: "m.text" }, + type: EventType.RoomMessage, }, - eventMapper, - ), - SearchResult.fromJson( - { - rank: 2, - result: { - room_id: room2.roomId, - event_id: "$22", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Room 2", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [], - events_after: [], - }, + context: { + profile_info: {}, + events_before: [], + events_after: [], }, - eventMapper, - ), - SearchResult.fromJson( - { - rank: 2, - result: { - room_id: room2.roomId, - event_id: "$23", - sender: client.getSafeUserId(), - origin_server_ts: 2, - content: { body: "Room 2 message 2", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [], - events_after: [], - }, + }, + eventMapper, + ), + SearchResult.fromJson( + { + rank: 2, + result: { + room_id: room2.roomId, + event_id: "$22", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Room 2", msgtype: "m.text" }, + type: EventType.RoomMessage, }, - eventMapper, - ), - SearchResult.fromJson( - { - rank: 3, - result: { - room_id: room3.roomId, - event_id: "$32", - sender: client.getSafeUserId(), - origin_server_ts: 1, - content: { body: "Room 3", msgtype: "m.text" }, - type: EventType.RoomMessage, - }, - context: { - profile_info: {}, - events_before: [], - events_after: [], - }, + context: { + profile_info: {}, + events_before: [], + events_after: [], }, - eventMapper, - ), - ], - highlights: [], - count: 1, - })} - resizeNotifier={resizeNotifier} - className="someClass" - onUpdate={jest.fn()} - /> - , + }, + eventMapper, + ), + SearchResult.fromJson( + { + rank: 2, + result: { + room_id: room2.roomId, + event_id: "$23", + sender: client.getSafeUserId(), + origin_server_ts: 2, + content: { body: "Room 2 message 2", msgtype: "m.text" }, + type: EventType.RoomMessage, + }, + context: { + profile_info: {}, + events_before: [], + events_after: [], + }, + }, + eventMapper, + ), + SearchResult.fromJson( + { + rank: 3, + result: { + room_id: room3.roomId, + event_id: "$32", + sender: client.getSafeUserId(), + origin_server_ts: 1, + content: { body: "Room 3", msgtype: "m.text" }, + type: EventType.RoomMessage, + }, + context: { + profile_info: {}, + events_before: [], + events_after: [], + }, + }, + eventMapper, + ), + ], + highlights: [], + count: 1, + })} + className="someClass" + onUpdate={jest.fn()} + />, + clientAndSDKContextRenderOptions(client, sdkContext), ); const event1 = await screen.findByText("Room 1"); diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index bb722e6f51..b0efc27dda 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -55,7 +55,6 @@ import { Action } from "../../../../src/dispatcher/actions"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { type ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload"; import { RoomView } from "../../../../src/components/structures/RoomView"; -import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; @@ -157,7 +156,6 @@ describe("RoomView", () => { // threepidInvite should be optional on RoomView props // it is treated as optional in RoomView threepidInvite={undefined as any} - resizeNotifier={new ResizeNotifier()} forceTimeline={false} ref={ref} /> @@ -196,7 +194,6 @@ describe("RoomView", () => { // threepidInvite should be optional on RoomView props // it is treated as optional in RoomView threepidInvite={undefined} - resizeNotifier={new ResizeNotifier()} forceTimeline={false} onRegistered={jest.fn()} /> diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 354036e282..3602dd8322 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -33,15 +33,14 @@ import { type Mocked, mocked } from "jest-mock"; import { forEachRight } from "lodash"; import TimelinePanel from "../../../../src/components/structures/TimelinePanel"; -import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { + clientAndSDKContextRenderOptions, filterConsole, flushPromises, mkMembership, mkRoom, stubClient, - withClientContextRenderOptions, } from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { createMessageEventContent } from "../../../test-utils/events"; @@ -51,6 +50,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; // ScrollPanel calls this, but jsdom doesn't mock it for us HTMLDivElement.prototype.scrollBy = () => {}; @@ -159,6 +159,7 @@ const setupPagination = ( describe("TimelinePanel", () => { let client: Mocked; + let sdkContext: SdkContextClass; let userId: string; filterConsole("checkForPreJoinUISI: showing all messages, skipping check"); @@ -166,6 +167,7 @@ describe("TimelinePanel", () => { beforeEach(() => { client = mocked(stubClient()); userId = client.getSafeUserId(); + sdkContext = new SdkContextClass(); }); describe("read receipts and markers", () => { @@ -200,7 +202,7 @@ describe("TimelinePanel", () => { timelinePanel = ref; }} />, - withClientContextRenderOptions(MatrixClientPeg.safeGet()), + clientAndSDKContextRenderOptions(client, sdkContext), ); await flushPromises(); await waitFor(() => expect(timelinePanel).toBeTruthy()); @@ -396,7 +398,7 @@ describe("TimelinePanel", () => { await withScrollPanelMountSpy(async (mountSpy) => { const { container } = render( , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), + clientAndSDKContextRenderOptions(client, sdkContext), ); await waitFor(() => expectEvents(container, [events[1]])); @@ -416,7 +418,7 @@ describe("TimelinePanel", () => { await withScrollPanelMountSpy(async (mountSpy) => { const { container } = render( , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), + clientAndSDKContextRenderOptions(client, sdkContext), ); await waitFor(() => expectEvents(container, [events[0], events[1]])); @@ -493,7 +495,7 @@ describe("TimelinePanel", () => { const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - render(); + render(, clientAndSDKContextRenderOptions(client, sdkContext)); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true }; @@ -590,9 +592,8 @@ describe("TimelinePanel", () => { const replyToEvent = jest.spyOn(thread, "replyToEvent", "get"); const dom = render( - - - , + , + clientAndSDKContextRenderOptions(client, sdkContext), ); await dom.findByText("RootEvent"); await dom.findByText("ReplyEvent1"); @@ -645,9 +646,8 @@ describe("TimelinePanel", () => { }; const dom = render( - - - , + , + clientAndSDKContextRenderOptions(client, sdkContext), ); await dom.findByText("RootEvent"); await dom.findByText("ReplyEvent1"); @@ -718,9 +718,8 @@ describe("TimelinePanel", () => { } const { container } = render( - - - , + , + clientAndSDKContextRenderOptions(client, sdkContext), ); await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull()); @@ -740,7 +739,7 @@ describe("TimelinePanel", () => { await withScrollPanelMountSpy(async () => { const { container } = render( , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), + clientAndSDKContextRenderOptions(client, sdkContext), ); await waitFor(() => expectEvents(container, [events[1]])); diff --git a/test/unit-tests/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/unit-tests/components/views/dialogs/MessageEditHistoryDialog-test.tsx index 2f0b0a4974..028c415932 100644 --- a/test/unit-tests/components/views/dialogs/MessageEditHistoryDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/MessageEditHistoryDialog-test.tsx @@ -13,10 +13,12 @@ import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { flushPromises, mkMessage, stubClient } from "../../../../test-utils"; import MessageEditHistoryDialog from "../../../../../src/components/views/dialogs/MessageEditHistoryDialog"; +import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; describe("", () => { const roomId = "!aroom:example.com"; let client: jest.Mocked; + let sdkContext: SdkContextClass; let event: MatrixEvent; beforeEach(() => { @@ -27,10 +29,13 @@ describe("", () => { room: "!room:example.com", msg: "My Great Message", }); + sdkContext = new SdkContextClass(); }); async function renderComponent(): Promise { - const result = render(); + const result = render(, { + wrapper: ({ children }) => {children}, + }); await waitForElementToBeRemoved(() => result.queryByRole("progressbar")); await flushPromises(); return result; diff --git a/test/unit-tests/components/views/elements/AppTile-test.tsx b/test/unit-tests/components/views/elements/AppTile-test.tsx index 039cd09631..e62d1adab9 100644 --- a/test/unit-tests/components/views/elements/AppTile-test.tsx +++ b/test/unit-tests/components/views/elements/AppTile-test.tsx @@ -21,7 +21,7 @@ import { import RightPanel from "../../../../../src/components/structures/RightPanel"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; -import { stubClient } from "../../../../test-utils"; +import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-utils"; import { Action } from "../../../../../src/dispatcher/actions"; import dis from "../../../../../src/dispatcher/dispatcher"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; @@ -39,6 +39,7 @@ import { ElementWidget } from "../../../../../src/stores/widgets/StopGapWidget"; import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore"; import { ModuleRunner } from "../../../../../src/modules/ModuleRunner"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; +import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; jest.mock("../../../../../src/stores/OwnProfileStore", () => ({ OwnProfileStore: { @@ -53,6 +54,7 @@ jest.mock("../../../../../src/stores/OwnProfileStore", () => ({ describe("AppTile", () => { let cli: MatrixClient; + let sdkContext: SdkContextClass; let r1: Room; let r2: Room; const resizeNotifier = new ResizeNotifier(); @@ -116,6 +118,7 @@ describe("AppTile", () => { }); beforeEach(() => { + sdkContext = new SdkContextClass(); jest.spyOn(SettingsStore, "getValue").mockRestore(); }); @@ -299,9 +302,8 @@ describe("AppTile", () => { // Run initial render with room 1, and also running lifecycle methods const renderResult = render( - - - , + , + clientAndSDKContextRenderOptions(cli, sdkContext), ); expect(renderResult.getByText("Example 1")).toBeInTheDocument(); diff --git a/test/unit-tests/components/views/rooms/AppsDrawer-test.tsx b/test/unit-tests/components/views/rooms/AppsDrawer-test.tsx index 14c8bde3a2..38efbc1eb8 100644 --- a/test/unit-tests/components/views/rooms/AppsDrawer-test.tsx +++ b/test/unit-tests/components/views/rooms/AppsDrawer-test.tsx @@ -13,23 +13,23 @@ import { render } from "jest-matrix-react"; import { stubClient } from "../../../../test-utils"; import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer"; import SdkConfig from "../../../../../src/SdkConfig"; -import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import { WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; const ROOM_ID = "!room:id"; describe("AppsDrawer", () => { let client: MatrixClient; let room: Room; - let dummyResizeNotifier: ResizeNotifier; + let sdkContext: SdkContextClass; beforeEach(async () => { client = stubClient(); room = new Room(ROOM_ID, client, client.getUserId()!, { pendingEventOrdering: PendingEventOrdering.Detached, }); - dummyResizeNotifier = new ResizeNotifier(); + sdkContext = new SdkContextClass(); }); afterEach(() => { @@ -58,17 +58,13 @@ describe("AppsDrawer", () => { return []; }); - const { container } = render( - , - { - wrapper: ({ ...rest }) => , - }, - ); + const { container } = render(, { + wrapper: ({ ...rest }) => ( + + + + ), + }); const appsDrawerResizer = container.getElementsByClassName("mx_AppsDrawer_resizer")[0] as HTMLElement; expect(appsDrawerResizer.style.height).toBe("500px"); diff --git a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx index 68a850affd..da6ba71e90 100644 --- a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx +++ b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx @@ -14,13 +14,13 @@ import userEvent from "@testing-library/user-event"; import * as pinnedEventHooks from "../../../../../src/hooks/usePinnedEvents"; import { PinnedMessageBanner } from "../../../../../src/components/views/rooms/PinnedMessageBanner"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; -import { makePollStartEvent, stubClient, withClientContextRenderOptions } from "../../../../test-utils"; +import { makePollStartEvent, stubClient, clientAndSDKContextRenderOptions } from "../../../../test-utils"; import dis from "../../../../../src/dispatcher/dispatcher"; import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore"; import { Action } from "../../../../../src/dispatcher/actions"; -import ResizeNotifier from "../../../../../src/utils/ResizeNotifier.ts"; +import { SdkContextClass } from "../../../../../src/contexts/SDKContext.ts"; describe("", () => { const userId = "@alice:server.org"; @@ -29,12 +29,12 @@ describe("", () => { let mockClient: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; - let resizeNotifier: ResizeNotifier; + let sdkContext: SdkContextClass; beforeEach(() => { mockClient = stubClient(); room = new Room(roomId, mockClient, userId); permalinkCreator = new RoomPermalinkCreator(room); - resizeNotifier = new ResizeNotifier(); + sdkContext = new SdkContextClass(); jest.spyOn(dis, "dispatch").mockReturnValue(undefined); }); @@ -80,8 +80,8 @@ describe("", () => { */ function renderBanner() { return render( - , - withClientContextRenderOptions(mockClient), + , + clientAndSDKContextRenderOptions(mockClient, sdkContext), ); } @@ -153,9 +153,7 @@ describe("", () => { event3.getId()!, ]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]); - rerender( - , - ); + rerender(); await expect(screen.findByText("Third pinned message")).resolves.toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); @@ -226,7 +224,7 @@ describe("", () => { describe("Notify the timeline to resize", () => { beforeEach(() => { - jest.spyOn(resizeNotifier, "notifyTimelineHeightChanged"); + jest.spyOn(sdkContext.resizeNotifier, "notifyTimelineHeightChanged"); jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]); }); @@ -235,7 +233,7 @@ describe("", () => { renderBanner(); await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); // The banner is displayed, so we need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + expect(sdkContext.resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); await userEvent.click( screen.getByRole("button", { @@ -244,23 +242,21 @@ describe("", () => { ); await expect(screen.findByText("First pinned message")).resolves.toBeVisible(); // The banner is already displayed, so we don't need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + expect(sdkContext.resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); }); it("should notify the timeline to resize when we hide the banner", async () => { const { rerender } = renderBanner(); await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); // The banner is displayed, so we need to resize the timeline - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + expect(sdkContext.resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); // The banner has no event to display and is hidden jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([]); - rerender( - , - ); + rerender(); // The timeline should be resized - expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2); + expect(sdkContext.resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2); }); }); diff --git a/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx b/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx index c3514d1aae..2739c31257 100644 --- a/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx +++ b/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { render } from "jest-matrix-react"; +import { fireEvent, render, waitFor } from "jest-matrix-react"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; @@ -15,19 +15,22 @@ import LegacyCallViewForRoom from "../../../../../src/components/views/voip/Lega import { mkStubRoom, stubClient } from "../../../../test-utils"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import ResizeNotifier from "../../../../../src/utils/ResizeNotifier"; import LegacyCallHandler from "../../../../../src/LegacyCallHandler"; +import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext"; jest.mock("../../../../../src/components/views/voip/LegacyCallView", () => jest.fn(() => "LegacyCallView")); describe("LegacyCallViewForRoom", () => { const LegacyCallViewMock = LegacyCallView as unknown as jest.Mock; + let sdkContext: SdkContextClass; + beforeEach(() => { + stubClient(); + sdkContext = new SdkContextClass(); LegacyCallViewMock.mockClear(); }); - it("should remember sidebar state, defaulting to shown", async () => { - stubClient(); + it("should remember sidebar state, defaulting to shown", async () => { const callHandler = new LegacyCallHandler(); callHandler.start(); jest.spyOn(LegacyCallHandler, "instance", "get").mockImplementation(() => callHandler); @@ -45,16 +48,14 @@ describe("LegacyCallViewForRoom", () => { const cli = MatrixClientPeg.safeGet(); cli.emit(CallEventHandlerEvent.Incoming, call); - const { rerender } = render( - , - ); + const { rerender } = render(); let props = LegacyCallViewMock.mock.lastCall![0]; expect(props.sidebarShown).toBeTruthy(); // Sidebar defaults to shown props.setSidebarShown(false); // Hide the sidebar - rerender(); + rerender(); console.log(LegacyCallViewMock.mock); @@ -64,9 +65,47 @@ describe("LegacyCallViewForRoom", () => { rerender(
    ); // Destroy the LegacyCallViewForRoom and LegacyCallView LegacyCallViewMock.mockClear(); // Drop stored LegacyCallView props - rerender(); + rerender(); props = LegacyCallViewMock.mock.lastCall![0]; expect(props.sidebarShown).toBeFalsy(); // Value was remembered }); + + it("should notify on resize start events", async () => { + const call = new MatrixCall({ + client: MatrixClientPeg.safeGet(), + roomId: "test-room", + }); + + const callHandler = { + getCallForRoom: jest.fn().mockReturnValue(call), + isCallSidebarShown: jest.fn().mockReturnValue(true), + addListener: jest.fn(), + removeListener: jest.fn(), + }; + jest.spyOn(LegacyCallHandler, "instance", "get").mockImplementation( + () => callHandler as unknown as LegacyCallHandler, + ); + + jest.spyOn(sdkContext.resizeNotifier, "startResizing"); + jest.spyOn(sdkContext.resizeNotifier, "stopResizing"); + jest.spyOn(sdkContext.resizeNotifier, "notifyTimelineHeightChanged"); + + const { container } = render(, { + wrapper: ({ children }) => {children}, + }); + + const resizer = container.querySelector(".mx_LegacyCallViewForRoom_ResizeHandle"); + await waitFor(() => { + expect(resizer).toBeInTheDocument(); + }); + + fireEvent.mouseDown(resizer!); + fireEvent.mouseMove(resizer!, { clientY: 100 }); + fireEvent.mouseUp(resizer!); + + expect(sdkContext.resizeNotifier.startResizing).toHaveBeenCalled(); + expect(sdkContext.resizeNotifier.stopResizing).toHaveBeenCalled(); + expect(sdkContext.resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled(); + }); }); From 34fc921cd3def9c8928bbf15beadf060d4a00965 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 6 Oct 2025 15:17:31 +0100 Subject: [PATCH 11/30] Hide calling buttons in room header before a room is created (#30816) * Hide call buttons until room has been created. * lint * lint * Update snapshot * update snaps --- playwright/e2e/invite/invite-dialog.spec.ts | 2 + .../send-your-first-message-view-linux.png | Bin 0 -> 27946 bytes .../views/rooms/RoomHeader/RoomHeader.tsx | 3 +- src/hooks/room/useRoomCall.tsx | 12 +- .../__snapshots__/RoomView-test.tsx.snap | 244 +----------------- 5 files changed, 27 insertions(+), 234 deletions(-) create mode 100644 playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts index cdf877f95e..49139af334 100644 --- a/playwright/e2e/invite/invite-dialog.spec.ts +++ b/playwright/e2e/invite/invite-dialog.spec.ts @@ -113,6 +113,8 @@ test.describe("Invite dialog", function () { "rgba(0, 0, 0, 0)", ); + await expect(page.locator(".mx_RoomView")).toMatchScreenshot("send_your_first_message_view.png"); + // Send a message to invite the bots const composer = app.getComposer().locator("[contenteditable]"); await composer.fill("Hello}"); diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ec2d8d5443e05bcff3ea0bb3f03e625b0c751e75 GIT binary patch literal 27946 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 GZ~qPZzii_G literal 0 HcmV?d00001 diff --git a/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/src/components/views/rooms/RoomHeader/RoomHeader.tsx index 7e88a31eac..5a8bd77cf8 100644 --- a/src/components/views/rooms/RoomHeader/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader/RoomHeader.tsx @@ -54,13 +54,14 @@ import { RoomSettingsTab } from "../../dialogs/RoomSettingsDialog.tsx"; import { useScopedRoomContext } from "../../../../contexts/ScopedRoomContext.tsx"; import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx"; import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx"; +import { type LocalRoom } from "../../../../models/LocalRoom.ts"; export default function RoomHeader({ room, additionalButtons, oobData, }: { - room: Room; + room: Room | LocalRoom; additionalButtons?: ViewRoomOpts["buttons"]; oobData?: IOOBData; }): JSX.Element { diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index 39e7725659..92f68d02f8 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -36,6 +36,7 @@ import { isVideoRoom } from "../../utils/video-rooms"; import { UIFeature } from "../../settings/UIFeature"; import { type InteractionName } from "../../PosthogTrackers"; import { ElementCallMemberEventType } from "../../call-types"; +import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; export enum PlatformCallType { ElementCall, @@ -83,7 +84,7 @@ const enum State { * @returns the call button attributes for the given room */ export const useRoomCall = ( - room: Room, + room: Room | LocalRoom, ): { voiceCallDisabledReason: string | null; voiceCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void; @@ -274,11 +275,16 @@ export const useRoomCall = ( }); }, [isViewingCall, room.roomId]); + const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED; + // We hide the voice call button if it'd have the same effect as the video call button let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall); let hideVideoCallButton = false; - // We hide both buttons if they require widgets but widgets are disabled, or if the Voip feature is disabled. - if ((memberCount > 2 && !widgetsFeatureEnabled) || !voipFeatureEnabled) { + // We hide both buttons if: + // - they require widgets but widgets are disabled + // - if the Voip feature is disabled. + // - The room is not created yet (rendering "send first message view") + if ((memberCount > 2 && !widgetsFeatureEnabled) || !voipFeatureEnabled || roomDoesNotExist) { hideVoiceCallButton = true; hideVideoCallButton = true; } diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index c36a22d066..350f3d9b60 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `
    @@ -45,63 +45,9 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1