Fix threads fallback incorrectly targets root event (#9229)
* Use RelationType enum instead of hardcoded value * Fix threads replies fallback to target last reply * Only unsubscribe from threads events if needed * fix strict null check * fix strict null checks * strict null checks * fix typing * Unsubscribe listeners if new thread is `null` Co-authored-by: Faye Duxovni <fayed@element.io> * Update strict null checks * Type HTMLElement as nullable * Add thread fallback integration test * lint fix * Update snapshots * Add test after changing thread * Remove test comment * update snapshot * fix room context test utility * Add ThreadListContextMenu test * lint fix * fix thread rendering Co-authored-by: Faye Duxovni <fayed@element.io> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
d898af820b
commit
be281fd735
18 changed files with 453 additions and 103 deletions
|
@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
parent: HTMLElement;
|
parent: HTMLElement | null;
|
||||||
onFileDrop(dataTransfer: DataTransfer): void;
|
onFileDrop(dataTransfer: DataTransfer): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,20 +90,20 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
parent.addEventListener("drop", onDrop);
|
parent?.addEventListener("drop", onDrop);
|
||||||
parent.addEventListener("dragover", onDragOver);
|
parent?.addEventListener("dragover", onDragOver);
|
||||||
parent.addEventListener("dragenter", onDragEnter);
|
parent?.addEventListener("dragenter", onDragEnter);
|
||||||
parent.addEventListener("dragleave", onDragLeave);
|
parent?.addEventListener("dragleave", onDragLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// disconnect the D&D event listeners from the room view. This
|
// disconnect the D&D event listeners from the room view. This
|
||||||
// is really just for hygiene - we're going to be
|
// is really just for hygiene - we're going to be
|
||||||
// deleted anyway, so it doesn't matter if the event listeners
|
// deleted anyway, so it doesn't matter if the event listeners
|
||||||
// don't get cleaned up.
|
// don't get cleaned up.
|
||||||
parent.removeEventListener("drop", onDrop);
|
parent?.removeEventListener("drop", onDrop);
|
||||||
parent.removeEventListener("dragover", onDragOver);
|
parent?.removeEventListener("dragover", onDragOver);
|
||||||
parent.removeEventListener("dragenter", onDragEnter);
|
parent?.removeEventListener("dragenter", onDragEnter);
|
||||||
parent.removeEventListener("dragleave", onDragLeave);
|
parent?.removeEventListener("dragleave", onDragLeave);
|
||||||
};
|
};
|
||||||
}, [parent, onFileDrop]);
|
}, [parent, onFileDrop]);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { createRef, KeyboardEvent } from 'react';
|
import React, { createRef, KeyboardEvent } from 'react';
|
||||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
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 { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||||
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
|
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
|
@ -70,6 +70,7 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
thread?: Thread;
|
thread?: Thread;
|
||||||
|
lastReply?: MatrixEvent | null;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
|
@ -88,9 +89,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const thread = this.props.room.getThread(this.props.mxEvent.getId());
|
||||||
|
|
||||||
|
this.setupThreadListeners(thread);
|
||||||
this.state = {
|
this.state = {
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
narrow: false,
|
narrow: false,
|
||||||
|
thread,
|
||||||
|
lastReply: thread?.lastReply((ev: MatrixEvent) => {
|
||||||
|
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||||
|
@ -99,6 +107,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
|
if (this.state.thread) {
|
||||||
|
this.postThreadUpdate(this.state.thread);
|
||||||
|
}
|
||||||
this.setupThread(this.props.mxEvent);
|
this.setupThread(this.props.mxEvent);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
|
@ -189,19 +200,49 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) => {
|
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({
|
this.setState({
|
||||||
thread,
|
thread,
|
||||||
}, async () => {
|
lastReply: this.threadLastReply,
|
||||||
thread.emit(ThreadEvent.ViewThread);
|
}, async () => this.postThreadUpdate(thread));
|
||||||
await thread.fetchInitialEvents();
|
|
||||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
|
||||||
this.timelinePanel.current?.refreshTimeline();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private async postThreadUpdate(thread: Thread): Promise<void> {
|
||||||
|
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 => {
|
private resetJumpToEvent = (event?: string): void => {
|
||||||
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
||||||
event === this.props.initialEvent?.getId()) {
|
event === this.props.initialEvent?.getId()) {
|
||||||
|
@ -242,14 +283,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private nextBatch: string;
|
private nextBatch: string | undefined | null = null;
|
||||||
|
|
||||||
private onPaginationRequest = async (
|
private onPaginationRequest = async (
|
||||||
timelineWindow: TimelineWindow | null,
|
timelineWindow: TimelineWindow | null,
|
||||||
direction = Direction.Backward,
|
direction = Direction.Backward,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (!Thread.hasServerSideSupport) {
|
if (!Thread.hasServerSideSupport && timelineWindow) {
|
||||||
timelineWindow.extend(direction, limit);
|
timelineWindow.extend(direction, limit);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -262,40 +303,50 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
opts.from = this.nextBatch;
|
opts.from = this.nextBatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nextBatch } = await this.state.thread.fetchEvents(opts);
|
let nextBatch: string | null | undefined = null;
|
||||||
|
if (this.state.thread) {
|
||||||
this.nextBatch = nextBatch;
|
const response = await this.state.thread.fetchEvents(opts);
|
||||||
|
nextBatch = response.nextBatch;
|
||||||
|
this.nextBatch = nextBatch;
|
||||||
|
}
|
||||||
|
|
||||||
// Advances the marker on the TimelineWindow to define the correct
|
// Advances the marker on the TimelineWindow to define the correct
|
||||||
// window of events to display on screen
|
// window of events to display on screen
|
||||||
timelineWindow.extend(direction, limit);
|
timelineWindow?.extend(direction, limit);
|
||||||
|
|
||||||
return !!nextBatch;
|
return !!nextBatch;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFileDrop = (dataTransfer: DataTransfer) => {
|
private onFileDrop = (dataTransfer: DataTransfer) => {
|
||||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
const roomId = this.props.mxEvent.getRoomId();
|
||||||
Array.from(dataTransfer.files),
|
if (roomId) {
|
||||||
this.props.mxEvent.getRoomId(),
|
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||||
this.threadRelation,
|
Array.from(dataTransfer.files),
|
||||||
MatrixClientPeg.get(),
|
roomId,
|
||||||
TimelineRenderingType.Thread,
|
this.threadRelation,
|
||||||
);
|
MatrixClientPeg.get(),
|
||||||
|
TimelineRenderingType.Thread,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn("Unknwon roomId for event", this.props.mxEvent);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private get threadRelation(): IEventRelation {
|
private get threadRelation(): IEventRelation {
|
||||||
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
|
const relation = {
|
||||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
"rel_type": THREAD_RELATION_TYPE.name,
|
"rel_type": THREAD_RELATION_TYPE.name,
|
||||||
"event_id": this.state.thread?.id,
|
"event_id": this.state.thread?.id,
|
||||||
"is_falling_back": true,
|
"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 => {
|
private renderThreadViewHeader = (): JSX.Element => {
|
||||||
|
@ -314,7 +365,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const threadRelation = this.threadRelation;
|
const threadRelation = this.threadRelation;
|
||||||
|
|
||||||
let timeline: JSX.Element;
|
let timeline: JSX.Element | null;
|
||||||
if (this.state.thread) {
|
if (this.state.thread) {
|
||||||
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
|
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
|
||||||
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",
|
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",
|
||||||
|
|
|
@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
|
||||||
interface IProps {
|
export interface ThreadListContextMenuProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
onMenuToggle?: (open: boolean) => void;
|
onMenuToggle?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => {
|
||||||
return { left, top, chevronFace };
|
return { left, top, chevronFace };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadListContextMenu: React.FC<IProps> = ({
|
const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
|
||||||
mxEvent,
|
mxEvent,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
onMenuToggle,
|
onMenuToggle,
|
||||||
|
@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
||||||
closeThreadOptions();
|
closeThreadOptions();
|
||||||
}, [mxEvent, closeThreadOptions]);
|
}, [mxEvent, closeThreadOptions]);
|
||||||
|
|
||||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
|
const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => {
|
||||||
evt.preventDefault();
|
if (permalinkCreator) {
|
||||||
evt.stopPropagation();
|
evt?.preventDefault();
|
||||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
evt?.stopPropagation();
|
||||||
await copyPlaintext(matrixToUrl);
|
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||||
closeThreadOptions();
|
await copyPlaintext(matrixToUrl);
|
||||||
|
closeThreadOptions();
|
||||||
|
}
|
||||||
}, [mxEvent, closeThreadOptions, permalinkCreator]);
|
}, [mxEvent, closeThreadOptions, permalinkCreator]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
||||||
title={_t("Thread options")}
|
title={_t("Thread options")}
|
||||||
isExpanded={menuDisplayed}
|
isExpanded={menuDisplayed}
|
||||||
inputRef={button}
|
inputRef={button}
|
||||||
|
data-testid="threadlist-dropdown-button"
|
||||||
/>
|
/>
|
||||||
{ menuDisplayed && (<IconizedContextMenu
|
{ menuDisplayed && (<IconizedContextMenu
|
||||||
onFinished={closeThreadOptions}
|
onFinished={closeThreadOptions}
|
||||||
|
@ -102,11 +105,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
||||||
label={_t("View in room")}
|
label={_t("View in room")}
|
||||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||||
/> }
|
/> }
|
||||||
<IconizedContextMenuOption
|
{ permalinkCreator &&
|
||||||
onClick={(e) => copyLinkToThread(e)}
|
<IconizedContextMenuOption
|
||||||
label={_t("Copy link to thread")}
|
data-testid="copy-thread-link"
|
||||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
onClick={(e) => copyLinkToThread(e)}
|
||||||
/>
|
label={_t("Copy link to thread")}
|
||||||
|
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||||
|
/>
|
||||||
|
}
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
</IconizedContextMenu>) }
|
</IconizedContextMenu>) }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent<IProps> {
|
||||||
style={{ width: w, height: h }}
|
style={{ width: w, height: h }}
|
||||||
aria-label={_t("Loading...")}
|
aria-label={_t("Loading...")}
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
|
data-testid="spinner"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,7 +57,7 @@ type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transfor
|
||||||
|
|
||||||
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||||
private static container: HTMLElement;
|
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,
|
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
|
||||||
// so we expose the Alignment options off of us statically.
|
// so we expose the Alignment options off of us statically.
|
||||||
|
@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
|
||||||
|
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||||
// positioned, also taking into account any window zoom
|
// positioned, also taking into account any window zoom
|
||||||
private updatePosition = (): void => {
|
private updatePosition = (): void => {
|
||||||
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
|
// 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 parentBox = this.parent.getBoundingClientRect();
|
||||||
const width = UIStore.instance.windowWidth;
|
const width = UIStore.instance.windowWidth;
|
||||||
|
|
|
@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
aria-activedescendant={activeDescendant}
|
aria-activedescendant={activeDescendant}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
aria-disabled={this.props.disabled}
|
aria-disabled={this.props.disabled}
|
||||||
|
data-testid="basicmessagecomposer"
|
||||||
/>
|
/>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ function SendButton(props: ISendButtonProps) {
|
||||||
className="mx_MessageComposer_sendMessage"
|
className="mx_MessageComposer_sendMessage"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
title={props.title ?? _t('Send message')}
|
title={props.title ?? _t('Send message')}
|
||||||
|
data-testid="sendmessagebtn"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
158
test/components/structures/ThreadView-test.tsx
Normal file
158
test/components/structures/ThreadView-test.tsx
Normal file
|
@ -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 <MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={getRoomContext(room, {
|
||||||
|
canSendMessages: true,
|
||||||
|
})}>
|
||||||
|
<ThreadView
|
||||||
|
room={room}
|
||||||
|
onClose={jest.fn()}
|
||||||
|
mxEvent={event}
|
||||||
|
resizeNotifier={new ResizeNotifier()}
|
||||||
|
/>
|
||||||
|
</RoomContext.Provider>,
|
||||||
|
</MatrixClientContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getComponent(): Promise<RenderResult> {
|
||||||
|
const renderResult = render(
|
||||||
|
<TestThreadView />,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(() => getByTestId(renderResult.container, 'spinner')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage(container, text): Promise<void> {
|
||||||
|
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"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
|
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\" data-testid=\\"spinner\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> 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. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> 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. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> 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. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> 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. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" data-testid=\\"basicmessagecomposer\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
|
||||||
|
|
||||||
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
|
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" data-testid=\\"basicmessagecomposer\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
|
||||||
|
|
|
@ -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<ThreadListContextMenuProps>) {
|
||||||
|
return render(<ThreadListContextMenu
|
||||||
|
mxEvent={event}
|
||||||
|
{...props}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { act } from "react-dom/test-utils";
|
import { act } from "react-dom/test-utils";
|
||||||
import { sleep } from "matrix-js-sdk/src/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
|
// eslint-disable-next-line deprecate/import
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
@ -291,7 +291,7 @@ describe('<SendMessageComposer/>', () => {
|
||||||
|
|
||||||
it('correctly sets the editorStateKey for threads', () => {
|
it('correctly sets the editorStateKey for threads', () => {
|
||||||
const relation = {
|
const relation = {
|
||||||
rel_type: "m.thread",
|
rel_type: RelationType.Thread,
|
||||||
event_id: "myFakeThreadId",
|
event_id: "myFakeThreadId",
|
||||||
};
|
};
|
||||||
const includeReplyLegacyFallback = false;
|
const includeReplyLegacyFallback = false;
|
||||||
|
|
|
@ -20,13 +20,12 @@ import { act, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
|
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
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 defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../../src/dispatcher/actions";
|
import { Action } from "../../../../../src/dispatcher/actions";
|
||||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
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 { 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";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
|
||||||
// Work around missing ClipboardEvent type
|
// Work around missing ClipboardEvent type
|
||||||
|
@ -74,43 +73,7 @@ describe('WysiwygComposer', () => {
|
||||||
return eventId === mockEvent.getId() ? mockEvent : null;
|
return eventId === mockEvent.getId() ? mockEvent : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultRoomContext: IRoomState = {
|
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
let sendMessage: () => void;
|
let sendMessage: () => void;
|
||||||
const customRender = (onChange = (_content: string) => void 0, disabled = false) => {
|
const customRender = (onChange = (_content: string) => void 0, disabled = false) => {
|
||||||
|
|
|
@ -31,6 +31,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
className="mx_Spinner_icon"
|
className="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
|
|
@ -33,6 +33,7 @@ exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
class="mx_Spinner_icon"
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 32px; height: 32px;"
|
style="width: 32px; height: 32px;"
|
||||||
/>
|
/>
|
||||||
|
@ -218,6 +219,7 @@ exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
class="mx_Spinner_icon"
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 32px; height: 32px;"
|
style="width: 32px; height: 32px;"
|
||||||
/>
|
/>
|
||||||
|
@ -359,6 +361,7 @@ exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
class="mx_Spinner_icon"
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 32px; height: 32px;"
|
style="width: 32px; height: 32px;"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -12,6 +12,7 @@ exports[`Module Components should override the factory for a ModuleSpinner 1`] =
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
className="mx_Spinner_icon"
|
className="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
|
|
@ -22,6 +22,9 @@ import {
|
||||||
Room,
|
Room,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { IRoomState } from "../../src/components/structures/RoomView";
|
||||||
|
import { TimelineRenderingType } from "../../src/contexts/RoomContext";
|
||||||
|
import { Layout } from "../../src/settings/enums/Layout";
|
||||||
import { mkEvent } from "./test-utils";
|
import { mkEvent } from "./test-utils";
|
||||||
|
|
||||||
export const makeMembershipEvent = (
|
export const makeMembershipEvent = (
|
||||||
|
@ -50,3 +53,45 @@ export const makeRoomWithStateEvents = (
|
||||||
mockClient.getRoom.mockReturnValue(room1);
|
mockClient.getRoom.mockReturnValue(room1);
|
||||||
return room1;
|
return room1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getRoomContext(room: Room, override: Partial<IRoomState>): 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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";
|
import { mkMessage, MessageEventProps } from "./test-utils";
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ export const makeThreadEvents = ({
|
||||||
|
|
||||||
rootEvent.setUnsigned({
|
rootEvent.setUnsigned({
|
||||||
"m.relations": {
|
"m.relations": {
|
||||||
"m.thread": {
|
[RelationType.Thread]: {
|
||||||
latest_event: events[events.length - 1],
|
latest_event: events[events.length - 1],
|
||||||
count: length,
|
count: length,
|
||||||
current_user_participated: [...participantUserIds, authorId].includes(currentUserId),
|
current_user_participated: [...participantUserIds, authorId].includes(currentUserId),
|
||||||
|
@ -88,3 +89,36 @@ export const makeThreadEvents = ({
|
||||||
|
|
||||||
return { rootEvent, events };
|
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 };
|
||||||
|
};
|
||||||
|
|
|
@ -195,6 +195,7 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
|
||||||
<div
|
<div
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
class="mx_Spinner_icon"
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 32px; height: 32px;"
|
style="width: 32px; height: 32px;"
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue