diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx index f6572a05e8..a775017c20 100644 --- a/src/components/structures/FileDropTarget.tsx +++ b/src/components/structures/FileDropTarget.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react"; import { _t } from "../../languageHandler"; interface IProps { - parent: HTMLElement; + parent: HTMLElement | null; onFileDrop(dataTransfer: DataTransfer): void; } @@ -90,20 +90,20 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { })); }; - parent.addEventListener("drop", onDrop); - parent.addEventListener("dragover", onDragOver); - parent.addEventListener("dragenter", onDragEnter); - parent.addEventListener("dragleave", onDragLeave); + parent?.addEventListener("drop", onDrop); + parent?.addEventListener("dragover", onDragOver); + parent?.addEventListener("dragenter", onDragEnter); + parent?.addEventListener("dragleave", onDragLeave); return () => { // disconnect the D&D event listeners from the room view. This // is really just for hygiene - we're going to be // deleted anyway, so it doesn't matter if the event listeners // don't get cleaned up. - parent.removeEventListener("drop", onDrop); - parent.removeEventListener("dragover", onDragOver); - parent.removeEventListener("dragenter", onDragEnter); - parent.removeEventListener("dragleave", onDragLeave); + parent?.removeEventListener("drop", onDrop); + parent?.removeEventListener("dragover", onDragOver); + parent?.removeEventListener("dragenter", onDragEnter); + parent?.removeEventListener("dragleave", onDragLeave); }; }, [parent, onFileDrop]); diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index a7b4ab10c8..8350b5e734 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { createRef, KeyboardEvent } from 'react'; import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; -import { Room } from 'matrix-js-sdk/src/models/room'; +import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room'; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; @@ -70,6 +70,7 @@ interface IProps { interface IState { thread?: Thread; + lastReply?: MatrixEvent | null; layout: Layout; editState?: EditorStateTransfer; replyToEvent?: MatrixEvent; @@ -88,9 +89,16 @@ export default class ThreadView extends React.Component { constructor(props: IProps) { super(props); + const thread = this.props.room.getThread(this.props.mxEvent.getId()); + + this.setupThreadListeners(thread); this.state = { layout: SettingsStore.getValue("layout"), narrow: false, + thread, + lastReply: thread?.lastReply((ev: MatrixEvent) => { + return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status; + }), }; this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) => @@ -99,6 +107,9 @@ export default class ThreadView extends React.Component { } public componentDidMount(): void { + if (this.state.thread) { + this.postThreadUpdate(this.state.thread); + } this.setupThread(this.props.mxEvent); this.dispatcherRef = dis.register(this.onAction); @@ -189,19 +200,49 @@ export default class ThreadView extends React.Component { } }; + private updateThreadRelation = (): void => { + this.setState({ + lastReply: this.threadLastReply, + }); + }; + + private get threadLastReply(): MatrixEvent | undefined { + return this.state.thread?.lastReply((ev: MatrixEvent) => { + return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status; + }); + } + private updateThread = (thread?: Thread) => { - if (thread && this.state.thread !== thread) { + if (this.state.thread === thread) return; + + this.setupThreadListeners(thread, this.state.thread); + if (thread) { this.setState({ thread, - }, async () => { - thread.emit(ThreadEvent.ViewThread); - await thread.fetchInitialEvents(); - this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward); - this.timelinePanel.current?.refreshTimeline(); - }); + lastReply: this.threadLastReply, + }, async () => this.postThreadUpdate(thread)); } }; + private async postThreadUpdate(thread: Thread): Promise { + thread.emit(ThreadEvent.ViewThread); + await thread.fetchInitialEvents(); + this.updateThreadRelation(); + this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward); + this.timelinePanel.current?.refreshTimeline(); + } + + private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void { + if (oldThread) { + this.state.thread.off(ThreadEvent.NewReply, this.updateThreadRelation); + this.props.room.off(RoomEvent.LocalEchoUpdated, this.updateThreadRelation); + } + if (thread) { + thread.on(ThreadEvent.NewReply, this.updateThreadRelation); + this.props.room.on(RoomEvent.LocalEchoUpdated, this.updateThreadRelation); + } + } + private resetJumpToEvent = (event?: string): void => { if (this.props.initialEvent && this.props.initialEventScrollIntoView && event === this.props.initialEvent?.getId()) { @@ -242,14 +283,14 @@ export default class ThreadView extends React.Component { } }; - private nextBatch: string; + private nextBatch: string | undefined | null = null; private onPaginationRequest = async ( timelineWindow: TimelineWindow | null, direction = Direction.Backward, limit = 20, ): Promise => { - if (!Thread.hasServerSideSupport) { + if (!Thread.hasServerSideSupport && timelineWindow) { timelineWindow.extend(direction, limit); return true; } @@ -262,40 +303,50 @@ export default class ThreadView extends React.Component { opts.from = this.nextBatch; } - const { nextBatch } = await this.state.thread.fetchEvents(opts); - - this.nextBatch = nextBatch; + let nextBatch: string | null | undefined = null; + if (this.state.thread) { + const response = await this.state.thread.fetchEvents(opts); + nextBatch = response.nextBatch; + this.nextBatch = nextBatch; + } // Advances the marker on the TimelineWindow to define the correct // window of events to display on screen - timelineWindow.extend(direction, limit); + timelineWindow?.extend(direction, limit); return !!nextBatch; }; private onFileDrop = (dataTransfer: DataTransfer) => { - ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(dataTransfer.files), - this.props.mxEvent.getRoomId(), - this.threadRelation, - MatrixClientPeg.get(), - TimelineRenderingType.Thread, - ); + const roomId = this.props.mxEvent.getRoomId(); + if (roomId) { + ContentMessages.sharedInstance().sendContentListToRoom( + Array.from(dataTransfer.files), + roomId, + this.threadRelation, + MatrixClientPeg.get(), + TimelineRenderingType.Thread, + ); + } else { + console.warn("Unknwon roomId for event", this.props.mxEvent); + } }; private get threadRelation(): IEventRelation { - const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => { - return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status; - }); - - return { + const relation = { "rel_type": THREAD_RELATION_TYPE.name, "event_id": this.state.thread?.id, "is_falling_back": true, - "m.in_reply_to": { - "event_id": lastThreadReply?.getId() ?? this.state.thread?.id, - }, }; + + const fallbackEventId = this.state.lastReply?.getId() ?? this.state.thread?.id; + if (fallbackEventId) { + relation["m.in_reply_to"] = { + "event_id": fallbackEventId, + }; + } + + return relation; } private renderThreadViewHeader = (): JSX.Element => { @@ -314,7 +365,7 @@ export default class ThreadView extends React.Component { const threadRelation = this.threadRelation; - let timeline: JSX.Element; + let timeline: JSX.Element | null; if (this.state.thread) { if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) { logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent", diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index 73fa52ef3c..3740327ca5 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -interface IProps { +export interface ThreadListContextMenuProps { mxEvent: MatrixEvent; - permalinkCreator: RoomPermalinkCreator; + permalinkCreator?: RoomPermalinkCreator; onMenuToggle?: (open: boolean) => void; } @@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => { return { left, top, chevronFace }; }; -const ThreadListContextMenu: React.FC = ({ +const ThreadListContextMenu: React.FC = ({ mxEvent, permalinkCreator, onMenuToggle, @@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC = ({ closeThreadOptions(); }, [mxEvent, closeThreadOptions]); - const copyLinkToThread = useCallback(async (evt: ButtonEvent) => { - evt.preventDefault(); - evt.stopPropagation(); - const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()); - await copyPlaintext(matrixToUrl); - closeThreadOptions(); + const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => { + if (permalinkCreator) { + evt?.preventDefault(); + evt?.stopPropagation(); + const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()); + await copyPlaintext(matrixToUrl); + closeThreadOptions(); + } }, [mxEvent, closeThreadOptions, permalinkCreator]); useEffect(() => { @@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC = ({ title={_t("Thread options")} isExpanded={menuDisplayed} inputRef={button} + data-testid="threadlist-dropdown-button" /> { menuDisplayed && ( = ({ label={_t("View in room")} iconClassName="mx_ThreadPanel_viewInRoom" /> } - copyLinkToThread(e)} - label={_t("Copy link to thread")} - iconClassName="mx_ThreadPanel_copyLinkToThread" - /> + { permalinkCreator && + copyLinkToThread(e)} + label={_t("Copy link to thread")} + iconClassName="mx_ThreadPanel_copyLinkToThread" + /> + } ) } ; diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx index 410d1b69cb..8677884c33 100644 --- a/src/components/views/elements/Spinner.tsx +++ b/src/components/views/elements/Spinner.tsx @@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent { style={{ width: w, height: h }} aria-label={_t("Loading...")} role="progressbar" + data-testid="spinner" /> ); diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index ab27f4f9d8..12013d58fc 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -57,7 +57,7 @@ type State = Partial { private static container: HTMLElement; - private parent: Element; + private parent: Element | null = null; // XXX: This is because some components (Field) are unable to `import` the Tooltip class, // so we expose the Alignment options off of us statically. @@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent { capture: true, }); - this.parent = ReactDOM.findDOMNode(this).parentNode as Element; + this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null; this.updatePosition(); } @@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent { // positioned, also taking into account any window zoom private updatePosition = (): void => { // When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance) - if (!this.props.visible) return; + if (!this.props.visible || !this.parent) return; const parentBox = this.parent.getBoundingClientRect(); const width = UIStore.instance.windowWidth; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 4c2201a628..4fb72cd65a 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component aria-activedescendant={activeDescendant} dir="auto" aria-disabled={this.props.disabled} + data-testid="basicmessagecomposer" /> ); } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 6c83b75b87..d1521c7b0c 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -73,6 +73,7 @@ function SendButton(props: ISendButtonProps) { className="mx_MessageComposer_sendMessage" onClick={props.onClick} title={props.title ?? _t('Send message')} + data-testid="sendmessagebtn" /> ); } diff --git a/test/components/structures/ThreadView-test.tsx b/test/components/structures/ThreadView-test.tsx new file mode 100644 index 0000000000..2516aad082 --- /dev/null +++ b/test/components/structures/ThreadView-test.tsx @@ -0,0 +1,158 @@ +/* +Copyright 2022 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 { getByTestId, render, RenderResult, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; +import React, { useState } from "react"; +import { act } from "react-dom/test-utils"; + +import ThreadView from "../../../src/components/structures/ThreadView"; +import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../src/contexts/RoomContext"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; +import ResizeNotifier from "../../../src/utils/ResizeNotifier"; +import { mockPlatformPeg } from "../../test-utils/platform"; +import { getRoomContext } from "../../test-utils/room"; +import { stubClient } from "../../test-utils/test-utils"; +import { mkThread } from "../../test-utils/threads"; + +describe("ThreadView", () => { + const ROOM_ID = "!roomId:example.org"; + const SENDER = "@alice:example.org"; + + let mockClient: MatrixClient; + let room: Room; + let rootEvent: MatrixEvent; + + let changeEvent: (event: MatrixEvent) => void; + + function TestThreadView() { + const [event, setEvent] = useState(rootEvent); + changeEvent = setEvent; + + return + + + , + ; + } + + async function getComponent(): Promise { + const renderResult = render( + , + ); + + await waitFor(() => { + expect(() => getByTestId(renderResult.container, 'spinner')).toThrow(); + }); + + return renderResult; + } + + async function sendMessage(container, text): Promise { + const composer = getByTestId(container, "basicmessagecomposer"); + await userEvent.click(composer); + await userEvent.keyboard(text); + const sendMessageBtn = getByTestId(container, "sendmessagebtn"); + await userEvent.click(sendMessageBtn); + } + + function expectedMessageBody(rootEvent, message) { + return { + "body": message, + "m.relates_to": { + "event_id": rootEvent.getId(), + "is_falling_back": true, + "m.in_reply_to": { + "event_id": rootEvent.getThread().lastReply((ev: MatrixEvent) => { + return ev.isRelation(THREAD_RELATION_TYPE.name); + }).getId(), + }, + "rel_type": RelationType.Thread, + }, + "msgtype": MsgType.Text, + }; + } + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + mockPlatformPeg(); + mockClient = mocked(MatrixClientPeg.get()); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + const res = mkThread({ + room, + client: mockClient, + authorId: mockClient.getUserId(), + participantUserIds: [mockClient.getUserId()], + }); + + rootEvent = res.rootEvent; + + DMRoomMap.makeShared(); + jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(SENDER); + }); + + it("sends a message with the correct fallback", async () => { + const { container } = await getComponent(); + + await sendMessage(container, "Hello world!"); + + expect(mockClient.sendMessage).toHaveBeenCalledWith( + ROOM_ID, rootEvent.getId(), expectedMessageBody(rootEvent, "Hello world!"), + ); + }); + + it("sends a message with the correct fallback", async () => { + const { container } = await getComponent(); + + const { rootEvent: rootEvent2 } = mkThread({ + room, + client: mockClient, + authorId: mockClient.getUserId(), + participantUserIds: [mockClient.getUserId()], + }); + + act(() => { + changeEvent(rootEvent2); + }); + + await sendMessage(container, "yolo"); + + expect(mockClient.sendMessage).toHaveBeenCalledWith( + ROOM_ID, rootEvent2.getId(), expectedMessageBody(rootEvent2, "yolo"), + ); + }); +}); diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index bf62c38ace..9b5c415f11 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
We're creating a room with @user:example.com
"`; +exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
We're creating a room with @user:example.com
"`; exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.
    U\\"\\"

    @user:example.com

    Send your first message to invite @user:example.com to chat

!
Some of your messages have not been sent
Retry
"`; -exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.
    U\\"\\"

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; +exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.
    U\\"\\"

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; -exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
    Encryption enabled
    Messages in this chat will be end-to-end encrypted.
  1. U\\"\\"

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; +exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"
U\\"\\"
@user:example.com
    Encryption enabled
    Messages in this chat will be end-to-end encrypted.
  1. U\\"\\"

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; diff --git a/test/components/views/context_menus/ThreadListContextMenu-test.tsx b/test/components/views/context_menus/ThreadListContextMenu-test.tsx new file mode 100644 index 0000000000..53d0fb7dd7 --- /dev/null +++ b/test/components/views/context_menus/ThreadListContextMenu-test.tsx @@ -0,0 +1,84 @@ +/* +Copyright 2022 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 { getByTestId, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import React from "react"; + +import ThreadListContextMenu, { + ThreadListContextMenuProps, +} from "../../../../src/components/views/context_menus/ThreadListContextMenu"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; +import { stubClient } from "../../../test-utils/test-utils"; +import { mkThread } from "../../../test-utils/threads"; + +describe("ThreadListContextMenu", () => { + const ROOM_ID = "!123:matrix.org"; + + let room: Room; + let mockClient: MatrixClient; + let event: MatrixEvent; + + function getComponent(props: Partial) { + return render(); + } + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + mockClient = mocked(MatrixClientPeg.get()); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + const res = mkThread({ + room, + client: mockClient, + authorId: mockClient.getUserId(), + participantUserIds: [mockClient.getUserId()], + }); + + event = res.rootEvent; + }); + + it("does not render the permalink", async () => { + const { container } = getComponent({}); + + const btn = getByTestId(container, "threadlist-dropdown-button"); + await userEvent.click(btn); + expect(screen.queryByTestId("copy-thread-link")).toBeNull(); + }); + + it("does render the permalink", async () => { + const { container } = getComponent({ + permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false), + }); + + const btn = getByTestId(container, "threadlist-dropdown-button"); + await userEvent.click(btn); + expect(screen.queryByTestId("copy-thread-link")).not.toBeNull(); + }); +}); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index 1d01c4a5a5..e8234fa951 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { act } from "react-dom/test-utils"; import { sleep } from "matrix-js-sdk/src/utils"; -import { ISendEventResponse, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; +import { ISendEventResponse, MatrixClient, MsgType, RelationType } from "matrix-js-sdk/src/matrix"; // eslint-disable-next-line deprecate/import import { mount } from 'enzyme'; import { mocked } from "jest-mock"; @@ -291,7 +291,7 @@ describe('', () => { it('correctly sets the editorStateKey for threads', () => { const relation = { - rel_type: "m.thread", + rel_type: RelationType.Thread, event_id: "myFakeThreadId", }; const includeReplyLegacyFallback = false; diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx index 12161ae816..583bf1d36d 100644 --- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx @@ -20,13 +20,12 @@ import { act, render, screen, waitFor } from "@testing-library/react"; import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext"; +import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { Layout } from "../../../../../src/settings/enums/Layout"; import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/WysiwygComposer"; -import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; import SettingsStore from "../../../../../src/settings/SettingsStore"; // Work around missing ClipboardEvent type @@ -74,43 +73,7 @@ describe('WysiwygComposer', () => { return eventId === mockEvent.getId() ? mockEvent : null; }); - const defaultRoomContext: IRoomState = { - room: mockRoom, - roomLoading: true, - peekLoading: false, - shouldPeek: true, - membersLoaded: false, - numUnreadMessages: 0, - canPeek: false, - showApps: false, - isPeeking: false, - showRightPanel: true, - joining: false, - atEndOfLiveTimeline: true, - showTopUnreadMessagesBar: false, - statusBarVisible: false, - canReact: false, - canSendMessages: false, - layout: Layout.Group, - lowBandwidth: false, - alwaysShowTimestamps: false, - showTwelveHourTimestamps: false, - readMarkerInViewThresholdMs: 3000, - readMarkerOutOfViewThresholdMs: 30000, - showHiddenEvents: false, - showReadReceipts: true, - showRedactions: true, - showJoinLeaves: true, - showAvatarChanges: true, - showDisplaynameChanges: true, - matrixClientIsReady: false, - timelineRenderingType: TimelineRenderingType.Room, - liveTimeline: undefined, - canSelfRedact: false, - resizing: false, - narrow: false, - activeCall: null, - }; + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); let sendMessage: () => void; const customRender = (onChange = (_content: string) => void 0, disabled = false) => { diff --git a/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap index 0859be41f7..1a017295d4 100644 --- a/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/FontScalingPanel-test.tsx.snap @@ -31,6 +31,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
approves login and waits for new device 1`] = `
@@ -218,6 +219,7 @@ exports[` displays qr code after it is created 1`] = `
@@ -359,6 +361,7 @@ exports[` renders spinner while generating code 1`] = `
diff --git a/test/modules/__snapshots__/ModuleComponents-test.tsx.snap b/test/modules/__snapshots__/ModuleComponents-test.tsx.snap index b8c51afeb3..864c775c4f 100644 --- a/test/modules/__snapshots__/ModuleComponents-test.tsx.snap +++ b/test/modules/__snapshots__/ModuleComponents-test.tsx.snap @@ -12,6 +12,7 @@ exports[`Module Components should override the factory for a ModuleSpinner 1`] =
): IRoomState { + return { + room, + roomLoading: true, + peekLoading: false, + shouldPeek: true, + membersLoaded: false, + numUnreadMessages: 0, + canPeek: false, + showApps: false, + isPeeking: false, + showRightPanel: true, + joining: false, + atEndOfLiveTimeline: true, + showTopUnreadMessagesBar: false, + statusBarVisible: false, + canReact: false, + canSendMessages: false, + layout: Layout.Group, + lowBandwidth: false, + alwaysShowTimestamps: false, + showTwelveHourTimestamps: false, + readMarkerInViewThresholdMs: 3000, + readMarkerOutOfViewThresholdMs: 30000, + showHiddenEvents: false, + showReadReceipts: true, + showRedactions: true, + showJoinLeaves: true, + showAvatarChanges: true, + showDisplaynameChanges: true, + matrixClientIsReady: false, + timelineRenderingType: TimelineRenderingType.Room, + liveTimeline: undefined, + canSelfRedact: false, + resizing: false, + narrow: false, + activeCall: null, + + ...override, + }; +} diff --git a/test/test-utils/threads.ts b/test/test-utils/threads.ts index 3b2c635256..419b09b2b8 100644 --- a/test/test-utils/threads.ts +++ b/test/test-utils/threads.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix"; +import { Thread } from "matrix-js-sdk/src/models/thread"; import { mkMessage, MessageEventProps } from "./test-utils"; @@ -78,7 +79,7 @@ export const makeThreadEvents = ({ rootEvent.setUnsigned({ "m.relations": { - "m.thread": { + [RelationType.Thread]: { latest_event: events[events.length - 1], count: length, current_user_participated: [...participantUserIds, authorId].includes(currentUserId), @@ -88,3 +89,36 @@ export const makeThreadEvents = ({ return { rootEvent, events }; }; + +type MakeThreadProps = { + room: Room; + client: MatrixClient; + authorId: string; + participantUserIds: string[]; + length?: number; + ts?: number; +}; + +export const mkThread = ({ + room, + client, + authorId, + participantUserIds, + length = 2, + ts = 1, +}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent } => { + const { rootEvent, events } = makeThreadEvents({ + roomId: room.roomId, + authorId, + participantUserIds, + length, + ts, + currentUserId: client.getUserId(), + }); + + const thread = room.createThread(rootEvent.getId(), rootEvent, events, true); + // So that we do not have to mock the thread loading + thread.initialEventsFetched = true; + + return { thread, rootEvent }; +}; diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap index 916283e402..4dba41de67 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap @@ -195,6 +195,7 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s