diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 84e7614e8e..52dd113314 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -50,7 +50,7 @@ test.describe("FilePanel", () => { test.describe("render", () => { test("should render empty state", async ({ page }) => { // Wait until the information about the empty state is rendered - await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); // Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332 await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts index 6223c1c13f..aa7dedf73a 100644 --- a/playwright/e2e/right-panel/notification-panel.spec.ts +++ b/playwright/e2e/right-panel/notification-panel.spec.ts @@ -35,7 +35,7 @@ test.describe("NotificationPanel", () => { await page.getByRole("button", { name: "Notifications" }).click(); // Wait until the information about the empty state is rendered - await expect(page.locator(".mx_NotificationPanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); // Take a snapshot of RightPanel await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 51ebb3df92..632f709245 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -104,7 +104,7 @@ test.describe("RightPanel", () => { await page.getByRole("menuitem", { name: "Files" }).click(); await expect(page.locator(".mx_FilePanel")).toBeVisible(); - await expect(page.locator(".mx_FilePanel_empty")).toBeVisible(); + await expect(page.locator(".mx_EmptyState")).toBeVisible(); await page.getByTestId("base-card-back-button").click(); await checkRoomSummaryCard(page, ROOM_NAME); diff --git a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png index fb6d85ae52..fdbec37b70 100644 Binary files a/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/file-panel.spec.ts/empty-linux.png differ diff --git a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png index 5547e7621b..d18266534d 100644 Binary files a/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png and b/playwright/snapshots/right-panel/notification-panel.spec.ts/empty-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index fc929f3d97..20a6d2fe54 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -73,7 +73,6 @@ @import "./structures/_MatrixChat.pcss"; @import "./structures/_MessagePanel.pcss"; @import "./structures/_NonUrgentToastContainer.pcss"; -@import "./structures/_NotificationPanel.pcss"; @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; @@ -259,6 +258,7 @@ @import "./views/polls/pollHistory/_PollHistory.pcss"; @import "./views/polls/pollHistory/_PollHistoryList.pcss"; @import "./views/right_panel/_BaseCard.pcss"; +@import "./views/right_panel/_EmptyState.pcss"; @import "./views/right_panel/_EncryptionInfo.pcss"; @import "./views/right_panel/_PinnedMessagesCard.pcss"; @import "./views/right_panel/_RightPanelTabs.pcss"; diff --git a/res/css/structures/_FilePanel.pcss b/res/css/structures/_FilePanel.pcss index 1c80cde901..186893b24b 100644 --- a/res/css/structures/_FilePanel.pcss +++ b/res/css/structures/_FilePanel.pcss @@ -102,7 +102,3 @@ limitations under the License. padding-inline-start: 0; } } - -.mx_FilePanel_empty::before { - --maskImage: url("$(res)/img/element-icons/room/files.svg"); /* See: _RightPanel.pcss */ -} diff --git a/res/css/structures/_NotificationPanel.pcss b/res/css/structures/_NotificationPanel.pcss deleted file mode 100644 index 7a3ede9e50..0000000000 --- a/res/css/structures/_NotificationPanel.pcss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_NotificationPanel_empty::before { - --maskImage: url("$(res)/img/element-icons/notifications.svg"); /* See: _RightPanel.pcss */ -} diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index f8b5cb4408..d21c435b24 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -72,30 +72,3 @@ limitations under the License. order: 2; margin: auto; } - -.mx_RightPanel_empty { - margin-right: -28px; - - h2 { - font-weight: 700; - margin: 16px 0; - } - - h2, - p { - font: var(--cpd-font-body-md-regular); - } - - &::before { - content: ""; - display: block; - margin: 11px auto 29px auto; - height: 42px; - width: 42px; - background-color: $header-panel-text-primary-color; - mask-image: var(--maskImage); - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } -} diff --git a/res/css/views/right_panel/_EmptyState.pcss b/res/css/views/right_panel/_EmptyState.pcss new file mode 100644 index 0000000000..4bf620bae3 --- /dev/null +++ b/res/css/views/right_panel/_EmptyState.pcss @@ -0,0 +1,45 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EmptyState { + height: 100%; + box-sizing: border-box; + padding: var(--cpd-space-4x); + text-align: center; + + svg { + width: 56px; + height: 56px; + box-sizing: border-box; + border-radius: 8px; + padding: var(--cpd-space-3x); + background-color: $panel-actions; + } + + &::before { + /* Bloom using magic numbers directly out of Figma */ + content: ""; + position: absolute; + z-index: -1; + width: 642px; + height: 775px; + right: -253.77px; + top: 0; + background: radial-gradient(49.95% 49.95% at 50% 50%, rgba(13, 189, 139, 0.12) 0%, rgba(18, 115, 235, 0) 100%); + transform: rotate(-89.69deg); + overflow: hidden; + } +} diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index 104430c190..612fd96747 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -106,10 +106,17 @@ limitations under the License. } .mx_RoomView_messagePanel { - /* To avoid the rule from being applied to .mx_ThreadPanel_empty */ + &.mx_RoomView_messageListWrapper { + position: initial; + } + .mx_RoomView_messageListWrapper { width: calc(100% + 6px); /* 8px - 2px */ } + + .mx_RoomView_empty { + display: contents; + } } .mx_RoomView_MessageList { @@ -168,72 +175,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/link.svg"); } -.mx_ThreadPanel_empty { - border-radius: 8px; - background: $background; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: absolute; - top: 0; - bottom: 0; - left: 0; - padding: 20px; - box-sizing: border-box; /* Include padding and border */ - width: 100%; - - h2 { - color: $primary-content; - font-weight: var(--cpd-font-weight-semibold); - font-size: $font-18px; - margin-top: 24px; - margin-bottom: 10px; - } - - p { - font-size: $font-15px; - color: $secondary-content; - margin: 10px 0; - } - - button { - border: none; - background: none; - color: $accent; - font-size: $font-15px; - - &:hover, - &:active { - text-decoration: underline; - cursor: pointer; - } - } - - .mx_ThreadPanel_empty_tip { - font-size: $font-12px; - line-height: $font-15px; - - > b { - font-weight: var(--cpd-font-weight-semibold); - } - } -} - -.mx_ThreadPanel_largeIcon { - width: 28px; - height: 28px; - padding: 18px; - background: $system; - border-radius: 50%; - - &::after { - @mixin ThreadSummaryIcon; - width: inherit; - height: inherit; - } -} - .mx_ContextualMenu_wrapper { .mx_ThreadPanel_Header_FilterOptionItem { display: flex; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 3836863431..c4bf2c3ff7 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -28,6 +28,7 @@ import { TimelineWindow, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import EventIndexPeg from "../../indexing/EventIndexPeg"; @@ -40,6 +41,7 @@ import Spinner from "../views/elements/Spinner"; import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; +import EmptyState from "../views/right_panel/EmptyState"; interface IProps { roomId: string; @@ -255,10 +257,11 @@ class FilePanel extends React.Component { // wrap a TimelinePanel with the jump-to-event bits turned off. const emptyState = ( -
-

{_t("file_panel|empty_heading")}

-

{_t("file_panel|empty_description")}

-
+ ); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId); diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 0da27a19b1..9c56da9609 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications.svg"; import { _t } from "../../languageHandler"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -26,6 +27,7 @@ import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; import Heading from "../views/typography/Heading"; +import EmptyState from "../views/right_panel/EmptyState"; interface IProps { onClose(): void; @@ -57,10 +59,11 @@ export default class NotificationPanel extends React.PureComponent -

{_t("notif_panel|empty_heading")}

-

{_t("notif_panel|empty_description")}

- + ); let content: JSX.Element; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index d9e1cf2aa8..7c7761368d 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -19,6 +19,7 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; import { IconButton, Tooltip } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; +import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads.svg"; import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import BaseCard from "../views/right_panel/BaseCard"; @@ -37,6 +38,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import Heading from "../views/typography/Heading"; import { clearRoomNotification } from "../../utils/notifications"; +import EmptyState from "../views/right_panel/EmptyState"; interface IProps { roomId: string; @@ -73,8 +75,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC< export const ThreadPanelHeader: React.FC<{ filterOption: ThreadFilterType; setFilterOption: (filterOption: ThreadFilterType) => void; - empty: boolean; -}> = ({ filterOption, setFilterOption, empty }) => { +}> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); const roomContext = useRoomContext(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -140,86 +141,24 @@ export const ThreadPanelHeader: React.FC<{ {_t("common|threads")} - {!empty && ( - <> - - - - - -
- { - openMenu(); - PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev); - }} - > - {`${_t("threads|show_thread_filter")} ${value?.label}`} - - {contextMenu} - - )} -
- ); -}; - -interface EmptyThreadIProps { - hasThreads: boolean; - filterOption: ThreadFilterType; - showAllThreadsCallback: () => void; -} - -const EmptyThread: React.FC = ({ hasThreads, filterOption, showAllThreadsCallback }) => { - let body: JSX.Element; - if (hasThreads) { - body = ( - <> -

- {_t("threads|empty_has_threads_tip", { - replyInThread: _t("action|reply_in_thread"), - })} -

-

- {/* Always display that paragraph to prevent layout shift when hiding the button */} - {filterOption === ThreadFilterType.My ? ( - - ) : ( - <>  - )} -

- - ); - } else { - body = ( - <> -

{_t("threads|empty_explainer")}

-

- {_t( - "threads|empty_tip", - { - replyInThread: _t("action|reply_in_thread"), - }, - { - b: (sub) => {sub}, - }, - )} -

- - ); - } - - return ( -
-
-

{_t("threads|empty_heading")}

- {body} + + + + + +
+ { + openMenu(); + PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev); + }} + > + {`${_t("threads|show_thread_filter")} ${value?.label}`} + + {contextMenu}
); }; @@ -268,11 +207,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => + hasThreads && } id="thread-panel" className="mx_ThreadPanel" @@ -295,10 +230,12 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => timelineSet={timelineSet} showUrlPreview={false} // No URL previews at the threads list level empty={ - setFilterOption(ThreadFilterType.All)} + } alwaysShowTimestamps={true} diff --git a/src/components/views/right_panel/EmptyState.tsx b/src/components/views/right_panel/EmptyState.tsx new file mode 100644 index 0000000000..7189cb8b3a --- /dev/null +++ b/src/components/views/right_panel/EmptyState.tsx @@ -0,0 +1,42 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ComponentType } from "react"; +import { Text } from "@vector-im/compound-web"; + +import { Flex } from "../../utils/Flex"; + +interface Props { + Icon: ComponentType>; + title: string; + description: string; +} + +const EmptyState: React.FC = ({ Icon, title, description }) => { + return ( + + + + {title} + + + {description} + + + ); +}; + +export default EmptyState; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 03da4e7811..12ad79607d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3193,16 +3193,13 @@ "one": "%(count)s reply", "other": "%(count)s replies" }, - "empty_explainer": "Threads help keep your conversations on-topic and easy to track.", - "empty_has_threads_tip": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.", - "empty_heading": "Keep discussions organised with threads", - "empty_tip": "Tip: Use “%(replyInThread)s” when hovering over a message.", + "empty_description": "Use “%(replyInThread)s” when hovering over a message.", + "empty_title": "Threads help keep your conversations on-topic and easy to track.", "error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation", "mark_all_read": "Mark all as read", "my_threads": "My threads", "my_threads_description": "Shows all threads you've participated in", "open_thread": "Open thread", - "show_all_threads": "Show all threads", "show_thread_filter": "Show:" }, "threads_activity_centre": { diff --git a/test/components/structures/FilePanel-test.tsx b/test/components/structures/FilePanel-test.tsx new file mode 100644 index 0000000000..2b53c9c86c --- /dev/null +++ b/test/components/structures/FilePanel-test.tsx @@ -0,0 +1,58 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; +import { screen, render, waitFor } from "@testing-library/react"; +import { mocked } from "jest-mock"; + +import FilePanel from "../../../src/components/structures/FilePanel"; +import ResizeNotifier from "../../../src/utils/ResizeNotifier"; +import { stubClient } from "../../test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; + +jest.mock("matrix-js-sdk/src/matrix", () => ({ + ...jest.requireActual("matrix-js-sdk/src/matrix"), + TimelineWindow: jest.fn().mockReturnValue({ + load: jest.fn().mockResolvedValue(null), + getEvents: jest.fn().mockReturnValue([]), + canPaginate: jest.fn().mockReturnValue(false), + }), +})); + +describe("FilePanel", () => { + beforeEach(() => { + stubClient(); + }); + + it("renders empty state", async () => { + const cli = MatrixClientPeg.safeGet(); + const room = new Room("!room:server", cli, cli.getSafeUserId(), { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + const timelineSet = new EventTimelineSet(room); + room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); + mocked(cli.getRoom).mockReturnValue(room); + + const { asFragment } = render( + , + ); + await waitFor(() => { + expect(screen.getByText("No files visible in this room")).toBeInTheDocument(); + }); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index 19122e4cce..f66f58ed84 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -43,44 +43,21 @@ describe("ThreadPanel", () => { describe("Header", () => { it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => { const { asFragment } = render( - undefined} - />, + undefined} />, ); expect(asFragment()).toMatchSnapshot(); }); it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => { const { asFragment } = render( - undefined} - />, - ); - expect(asFragment()).toMatchSnapshot(); - }); - - it("matches snapshot when no threads", () => { - const { asFragment } = render( - undefined} - />, + undefined} />, ); expect(asFragment()).toMatchSnapshot(); }); it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => { const { container } = render( - undefined} - />, + undefined} />, ); const found = container.querySelector(".mx_ThreadPanel_dropdown"); expect(found).toBeTruthy(); @@ -91,11 +68,7 @@ describe("ThreadPanel", () => { it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => { const { container } = render( - undefined} - />, + undefined} />, ); fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!); const found = screen.queryAllByRole("menuitemradio"); @@ -118,11 +91,7 @@ describe("ThreadPanel", () => { const { container } = render( - undefined} - /> + undefined} /> , ); @@ -136,11 +105,7 @@ describe("ThreadPanel", () => { const mockClient = createTestClient(); const { container } = render( - undefined} - /> + undefined} /> , ); fireEvent.click(getByRole(container, "button", { name: "Mark all as read" })); diff --git a/test/components/structures/__snapshots__/FilePanel-test.tsx.snap b/test/components/structures/__snapshots__/FilePanel-test.tsx.snap new file mode 100644 index 0000000000..87ffc5da2e --- /dev/null +++ b/test/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilePanel renders empty state 1`] = ` + +
+
+
+ +
+
+
+
+
+

+ No files visible in this room +

+

+ Attach files from chat or just drag and drop them anywhere in a room. +

+
+
+
+
+ +`; diff --git a/test/components/structures/__snapshots__/ThreadPanel-test.tsx.snap b/test/components/structures/__snapshots__/ThreadPanel-test.tsx.snap index 09bf4ae39b..0a219b8403 100644 --- a/test/components/structures/__snapshots__/ThreadPanel-test.tsx.snap +++ b/test/components/structures/__snapshots__/ThreadPanel-test.tsx.snap @@ -95,17 +95,3 @@ exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option
`; - -exports[`ThreadPanel Header matches snapshot when no threads 1`] = ` - -
-

- Threads -

-
-
-`; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 5370f72b18..856d9b17b7 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -274,6 +274,7 @@ export function createTestClient(): MatrixClient { matrixRTC: createStubMatrixRTC(), isFallbackICEServerAllowed: jest.fn().mockReturnValue(false), getAuthIssuer: jest.fn(), + getOrCreateFilter: jest.fn(), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client);