mirror of https://github.com/vector-im/riot-web
Include thread replies in message previews (#10631)
* Include thread replies to message previews * Extend tests * Fix type issue * Use currentColor for thread icon * Fix long room name overflow * Update snapshots * Fix preview * Fix typing issue * Fix type issues * Tweak thread reply detection * Extend tests * Fix type issue * Fix testpull/28788/head^2
parent
6be09eec09
commit
b5727cb463
|
@ -55,13 +55,18 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.mx_RoomTile_title,
|
||||
.mx_RoomTile_subtitle {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
color: $secondary-content;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
/* Ellipsize any text overflow */
|
||||
text-overflow: ellipsis;
|
||||
.mx_RoomTile_title,
|
||||
.mx_RoomTile_subtitle_text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -74,11 +79,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomTile_subtitle {
|
||||
line-height: $font-18px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_RoomTile_titleWithSubtitle {
|
||||
margin-top: -3px; /* shift the title up a bit more */
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.88867 0.138672C1.36989 0.138672 0.138672 1.36989 0.138672 2.88867V15.1109C0.138672 15.395 0.299174 15.6547 0.553262 15.7817C0.80735 15.9088 1.11141 15.8813 1.33867 15.7109L4.36089 13.4442C4.57726 13.2819 4.84043 13.1942 5.11089 13.1942H13.1109C14.6297 13.1942 15.8609 11.963 15.8609 10.4442V2.88822C15.8609 1.36922 14.6295 0.138672 13.1109 0.138672H2.88867ZM3.69421 5.33301C3.69421 4.91879 4.03 4.58301 4.44421 4.58301H11.5553C11.9695 4.58301 12.3053 4.91879 12.3053 5.33301C12.3053 5.74722 11.9695 6.08301 11.5553 6.08301H4.44421C4.03 6.08301 3.69421 5.74722 3.69421 5.33301ZM4.44421 7.24976C4.03 7.24976 3.69421 7.58554 3.69421 7.99976C3.69421 8.41397 4.03 8.74976 4.44421 8.74976H7.99977C8.41398 8.74976 8.74977 8.41397 8.74977 7.99976C8.74977 7.58554 8.41398 7.24976 7.99977 7.24976H4.44421Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 974 B |
|
@ -27,7 +27,7 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton, MenuProps } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
@ -44,11 +44,11 @@ import PosthogTrackers from "../../../PosthogTrackers";
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useHasRoomLiveVoiceBroadcast, VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
||||
import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast";
|
||||
import { RoomTileSubtitle } from "./RoomTileSubtitle";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
|
@ -68,7 +68,7 @@ interface State {
|
|||
notificationsMenuPosition: PartialDOMRect | null;
|
||||
generalMenuPosition: PartialDOMRect | null;
|
||||
call: Call | null;
|
||||
messagePreview?: string;
|
||||
messagePreview: MessagePreview | null;
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`;
|
||||
|
@ -96,7 +96,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
generalMenuPosition: null,
|
||||
call: CallStore.instance.getCall(this.props.room.roomId),
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
messagePreview: null,
|
||||
};
|
||||
this.generatePreview();
|
||||
|
||||
|
@ -208,7 +208,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
}
|
||||
|
||||
const messagePreview =
|
||||
(await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag)) ?? undefined;
|
||||
(await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag)) ?? null;
|
||||
this.setState({ messagePreview });
|
||||
}
|
||||
|
||||
|
@ -359,6 +359,20 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RoomTile has a subtile if one of the following applies:
|
||||
* - there is a call
|
||||
* - there is a live voice broadcast
|
||||
* - message previews are enabled and there is a previewable message
|
||||
*/
|
||||
private get shouldRenderSubtitle(): boolean {
|
||||
return (
|
||||
!!this.state.call ||
|
||||
this.props.hasLiveVoiceBroadcast ||
|
||||
(this.props.showMessagePreview && !!this.state.messagePreview)
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const classes = classNames({
|
||||
mx_RoomTile: true,
|
||||
|
@ -385,26 +399,15 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
);
|
||||
}
|
||||
|
||||
let subtitle;
|
||||
if (this.state.call) {
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle">
|
||||
<RoomTileCallSummary call={this.state.call} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.hasLiveVoiceBroadcast) {
|
||||
subtitle = <VoiceBroadcastRoomSubtitle />;
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<div
|
||||
className="mx_RoomTile_subtitle"
|
||||
id={messagePreviewId(this.props.room.roomId)}
|
||||
title={this.state.messagePreview}
|
||||
>
|
||||
{this.state.messagePreview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const subtitle = this.shouldRenderSubtitle ? (
|
||||
<RoomTileSubtitle
|
||||
call={this.state.call}
|
||||
hasLiveVoiceBroadcast={this.props.hasLiveVoiceBroadcast}
|
||||
messagePreview={this.state.messagePreview}
|
||||
roomId={this.props.room.roomId}
|
||||
showMessagePreview={this.props.showMessagePreview}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const titleClasses = classNames({
|
||||
mx_RoomTile_title: true,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { MessagePreview } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { Call } from "../../../models/Call";
|
||||
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||
import { VoiceBroadcastRoomSubtitle } from "../../../voice-broadcast";
|
||||
import { Icon as ThreadIcon } from "../../../../res/img/compound/thread-16px.svg";
|
||||
|
||||
interface Props {
|
||||
call: Call | null;
|
||||
hasLiveVoiceBroadcast: boolean;
|
||||
messagePreview: MessagePreview | null;
|
||||
roomId: string;
|
||||
showMessagePreview: boolean;
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string): string => `mx_RoomTile_messagePreview_${roomId}`;
|
||||
|
||||
export const RoomTileSubtitle: React.FC<Props> = ({
|
||||
call,
|
||||
hasLiveVoiceBroadcast,
|
||||
messagePreview,
|
||||
roomId,
|
||||
showMessagePreview,
|
||||
}) => {
|
||||
if (call) {
|
||||
return (
|
||||
<div className="mx_RoomTile_subtitle">
|
||||
<RoomTileCallSummary call={call} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasLiveVoiceBroadcast) {
|
||||
return <VoiceBroadcastRoomSubtitle />;
|
||||
}
|
||||
|
||||
if (showMessagePreview && messagePreview) {
|
||||
const className = classNames("mx_RoomTile_subtitle", {
|
||||
"mx_RoomTile_subtitle--thread-reply": messagePreview.isThreadReply,
|
||||
});
|
||||
|
||||
const icon = messagePreview.isThreadReply ? <ThreadIcon className="mx_Icon mx_Icon_16" /> : null;
|
||||
|
||||
return (
|
||||
<div className={className} id={messagePreviewId(roomId)} title={messagePreview.text}>
|
||||
{icon}
|
||||
<span className="mx_RoomTile_subtitle_text">{messagePreview.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
import { RelationType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
@ -96,6 +97,43 @@ interface IState {
|
|||
// Empty because we don't actually use the state
|
||||
}
|
||||
|
||||
export interface MessagePreview {
|
||||
event: MatrixEvent;
|
||||
isThreadReply: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const isThreadReply = (event: MatrixEvent): boolean => {
|
||||
// a thread root event cannot be a thread reply
|
||||
if (event.isThreadRoot) return false;
|
||||
|
||||
const thread = event.getThread();
|
||||
|
||||
// it cannot be a thread reply if there is no thread
|
||||
if (!thread) return false;
|
||||
|
||||
const relation = event.getRelation();
|
||||
|
||||
if (
|
||||
!!relation &&
|
||||
relation.rel_type === RelationType.Annotation &&
|
||||
relation.event_id === thread.rootEvent?.getId()
|
||||
) {
|
||||
// annotations on the thread root are not a thread reply
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const mkMessagePreview = (text: string, event: MatrixEvent): MessagePreview => {
|
||||
return {
|
||||
event,
|
||||
text,
|
||||
isThreadReply: isThreadReply(event),
|
||||
};
|
||||
};
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new MessagePreviewStore();
|
||||
|
@ -111,7 +149,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
// null indicates the preview is empty / irrelevant
|
||||
private previews = new Map<string, Map<TagID | TAG_ANY, [MatrixEvent, string] | null>>();
|
||||
private previews = new Map<string, Map<TagID | TAG_ANY, MessagePreview | null>>();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
|
@ -131,7 +169,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
* @param inTagId The tag ID in which the room resides
|
||||
* @returns The preview, or null if none present.
|
||||
*/
|
||||
public async getPreviewForRoom(room: Room, inTagId: TagID): Promise<string | null> {
|
||||
public async getPreviewForRoom(room: Room, inTagId: TagID): Promise<MessagePreview | null> {
|
||||
if (!room) return null; // invalid room, just return nothing
|
||||
|
||||
if (!this.previews.has(room.roomId)) await this.generatePreview(room, inTagId);
|
||||
|
@ -140,9 +178,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
if (!previews) return null;
|
||||
|
||||
if (previews.has(inTagId)) {
|
||||
return previews.get(inTagId)![1];
|
||||
return previews.get(inTagId)!;
|
||||
}
|
||||
return previews.get(TAG_ANY)?.[1] ?? null;
|
||||
return previews.get(TAG_ANY) ?? null;
|
||||
}
|
||||
|
||||
public generatePreviewForEvent(event: MatrixEvent): string {
|
||||
|
@ -166,16 +204,28 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
private async generatePreview(room: Room, tagId?: TagID): Promise<void> {
|
||||
const events = room.timeline;
|
||||
const events = [...room.getLiveTimeline().getEvents()];
|
||||
|
||||
// add last reply from each thread
|
||||
room.getThreads().forEach((thread: Thread): void => {
|
||||
const lastReply = thread.lastReply();
|
||||
if (lastReply) events.push(lastReply);
|
||||
});
|
||||
|
||||
// sort events from oldest to newest
|
||||
events.sort((a: MatrixEvent, b: MatrixEvent) => {
|
||||
return a.getTs() - b.getTs();
|
||||
});
|
||||
|
||||
if (!events) return; // should only happen in tests
|
||||
|
||||
let map = this.previews.get(room.roomId);
|
||||
if (!map) {
|
||||
map = new Map<TagID | TAG_ANY, [MatrixEvent, string] | null>();
|
||||
map = new Map<TagID | TAG_ANY, MessagePreview | null>();
|
||||
this.previews.set(room.roomId, map);
|
||||
}
|
||||
|
||||
const previousEventInAny = map.get(TAG_ANY)?.[0];
|
||||
const previousEventInAny = map.get(TAG_ANY)?.event;
|
||||
|
||||
// Set the tags so we know what to generate
|
||||
if (!map.has(TAG_ANY)) map.set(TAG_ANY, null);
|
||||
|
@ -196,27 +246,28 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
if (!previewDef) continue;
|
||||
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
|
||||
|
||||
const anyPreview = previewDef.previewer.getTextFor(event);
|
||||
if (!anyPreview) continue; // not previewable for some reason
|
||||
const anyPreviewText = previewDef.previewer.getTextFor(event);
|
||||
if (!anyPreviewText) continue; // not previewable for some reason
|
||||
|
||||
if (!this.shouldSkipPreview(event, previousEventInAny)) {
|
||||
changed = changed || anyPreview !== map.get(TAG_ANY)?.[1];
|
||||
map.set(TAG_ANY, [event, anyPreview]);
|
||||
changed = changed || anyPreviewText !== map.get(TAG_ANY)?.text;
|
||||
map.set(TAG_ANY, mkMessagePreview(anyPreviewText, event));
|
||||
}
|
||||
|
||||
const tagsToGenerate = Array.from(map.keys()).filter((t) => t !== TAG_ANY); // we did the any tag above
|
||||
for (const genTagId of tagsToGenerate) {
|
||||
const previousEventInTag = map.get(genTagId)?.[0];
|
||||
const previousEventInTag = map.get(genTagId)?.event;
|
||||
if (this.shouldSkipPreview(event, previousEventInTag)) continue;
|
||||
|
||||
const realTagId = genTagId === TAG_ANY ? undefined : genTagId;
|
||||
const preview = previewDef.previewer.getTextFor(event, realTagId);
|
||||
if (preview === anyPreview) {
|
||||
changed = changed || anyPreview !== map.get(genTagId)?.[1];
|
||||
|
||||
if (preview === anyPreviewText) {
|
||||
changed = changed || anyPreviewText !== map.get(genTagId)?.text;
|
||||
map.delete(genTagId);
|
||||
} else {
|
||||
changed = changed || preview !== map.get(genTagId)?.[1];
|
||||
map.set(genTagId, preview ? [event, preview] : null);
|
||||
changed = changed || preview !== map.get(genTagId)?.text;
|
||||
map.set(genTagId, preview ? mkMessagePreview(anyPreviewText, event) : null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,7 +281,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
// At this point, we didn't generate a preview so clear it
|
||||
this.previews.set(room.roomId, new Map<TagID | TAG_ANY, [MatrixEvent, string] | null>());
|
||||
this.previews.set(room.roomId, new Map<TagID | TAG_ANY, MessagePreview | null>());
|
||||
this.emit(UPDATE_EVENT, this);
|
||||
this.emit(MessagePreviewStore.getPreviewChangedEventName(room), room);
|
||||
}
|
||||
|
|
|
@ -131,16 +131,6 @@ describe("EventTile", () => {
|
|||
});
|
||||
|
||||
describe("EventTile renderingType: ThreadsList", () => {
|
||||
beforeEach(() => {
|
||||
const { rootEvent } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
});
|
||||
mxEvent = rootEvent;
|
||||
});
|
||||
|
||||
it("shows an unread notification badge", () => {
|
||||
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
setupAsyncStoreWithClient,
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
mkMessage,
|
||||
} from "../../../test-utils";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||
|
@ -45,6 +47,7 @@ import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
|||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { TestSdkContext } from "../../../TestSdkContext";
|
||||
import { SDKContext } from "../../../../src/contexts/SDKContext";
|
||||
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
|
||||
|
||||
describe("RoomTile", () => {
|
||||
jest.spyOn(PlatformPeg, "get").mockReturnValue({
|
||||
|
@ -69,7 +72,12 @@ describe("RoomTile", () => {
|
|||
const renderRoomTile = (): void => {
|
||||
renderResult = render(
|
||||
<SDKContext.Provider value={sdkContext}>
|
||||
<RoomTile room={room} showMessagePreview={false} isMinimized={false} tag={DefaultTagID.Untagged} />
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={showMessagePreview}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>
|
||||
</SDKContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
@ -79,12 +87,44 @@ describe("RoomTile", () => {
|
|||
let room: Room;
|
||||
let renderResult: RenderResult;
|
||||
let sdkContext: TestSdkContext;
|
||||
let showMessagePreview = false;
|
||||
|
||||
filterConsole(
|
||||
// irrelevant for this test
|
||||
"Room !1:example.org does not have an m.room.create event",
|
||||
);
|
||||
|
||||
const addMessageToRoom = (ts: number) => {
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "test message",
|
||||
user: client.getSafeUserId(),
|
||||
ts,
|
||||
});
|
||||
|
||||
room.timeline.push(message);
|
||||
};
|
||||
|
||||
const addThreadMessageToRoom = (ts: number) => {
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "test thread reply",
|
||||
user: client.getSafeUserId(),
|
||||
ts,
|
||||
});
|
||||
|
||||
// Mock thread reply for tests.
|
||||
jest.spyOn(room, "getThreads").mockReturnValue([
|
||||
// @ts-ignore
|
||||
{
|
||||
lastReply: () => message,
|
||||
timeline: [],
|
||||
} as Thread,
|
||||
]);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sdkContext = new TestSdkContext();
|
||||
|
||||
|
@ -99,130 +139,199 @@ describe("RoomTile", () => {
|
|||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
renderRoomTile();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// @ts-ignore
|
||||
MessagePreviewStore.instance.previews = new Map<string, Map<TagID | TAG_ANY, MessagePreview | null>>();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render the room", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when a call starts", () => {
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
describe("when message previews are not enabled", () => {
|
||||
beforeEach(() => {
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
renderRoomTile();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
renderResult.unmount();
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
it("should render the room", () => {
|
||||
expect(renderResult.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("tracks connection state", async () => {
|
||||
screen.getByText("Video");
|
||||
describe("when a call starts", () => {
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
// Insert an await point in the connection method so we can inspect
|
||||
// the intermediate connecting state
|
||||
let completeConnection: () => void = () => {};
|
||||
const connectionCompleted = new Promise<void>((resolve) => (completeConnection = resolve));
|
||||
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
|
||||
beforeEach(() => {
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await screen.findByText("Joining…");
|
||||
const joinedFound = screen.findByText("Joined");
|
||||
completeConnection();
|
||||
await joinedFound;
|
||||
})(),
|
||||
call.connect(),
|
||||
]);
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
await Promise.all([screen.findByText("Video"), call.disconnect()]);
|
||||
});
|
||||
|
||||
it("tracks participants", () => {
|
||||
const alice: [RoomMember, Set<string>] = [mkRoomMember(room.roomId, "@alice:example.org"), new Set(["a"])];
|
||||
const bob: [RoomMember, Set<string>] = [
|
||||
mkRoomMember(room.roomId, "@bob:example.org"),
|
||||
new Set(["b1", "b2"]),
|
||||
];
|
||||
const carol: [RoomMember, Set<string>] = [mkRoomMember(room.roomId, "@carol:example.org"), new Set(["c"])];
|
||||
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map([alice]);
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map([alice, bob, carol]);
|
||||
afterEach(() => {
|
||||
renderResult.unmount();
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
expect(screen.getByLabelText("4 participants").textContent).toBe("4");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map();
|
||||
it("tracks connection state", async () => {
|
||||
screen.getByText("Video");
|
||||
|
||||
// Insert an await point in the connection method so we can inspect
|
||||
// the intermediate connecting state
|
||||
let completeConnection: () => void = () => {};
|
||||
const connectionCompleted = new Promise<void>((resolve) => (completeConnection = resolve));
|
||||
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await screen.findByText("Joining…");
|
||||
const joinedFound = screen.findByText("Joined");
|
||||
completeConnection();
|
||||
await joinedFound;
|
||||
})(),
|
||||
call.connect(),
|
||||
]);
|
||||
|
||||
await Promise.all([screen.findByText("Video"), call.disconnect()]);
|
||||
});
|
||||
|
||||
it("tracks participants", () => {
|
||||
const alice: [RoomMember, Set<string>] = [
|
||||
mkRoomMember(room.roomId, "@alice:example.org"),
|
||||
new Set(["a"]),
|
||||
];
|
||||
const bob: [RoomMember, Set<string>] = [
|
||||
mkRoomMember(room.roomId, "@bob:example.org"),
|
||||
new Set(["b1", "b2"]),
|
||||
];
|
||||
const carol: [RoomMember, Set<string>] = [
|
||||
mkRoomMember(room.roomId, "@carol:example.org"),
|
||||
new Set(["c"]),
|
||||
];
|
||||
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map([alice]);
|
||||
});
|
||||
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map([alice, bob, carol]);
|
||||
});
|
||||
expect(screen.getByLabelText("4 participants").textContent).toBe("4");
|
||||
|
||||
act(() => {
|
||||
call.participants = new Map();
|
||||
});
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
});
|
||||
|
||||
describe("and a live broadcast starts", () => {
|
||||
beforeEach(async () => {
|
||||
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
|
||||
});
|
||||
|
||||
it("should still render the call subtitle", () => {
|
||||
expect(screen.queryByText("Video")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
});
|
||||
|
||||
describe("and a live broadcast starts", () => {
|
||||
describe("when a live voice broadcast starts", () => {
|
||||
beforeEach(async () => {
|
||||
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
|
||||
});
|
||||
|
||||
it("should still render the call subtitle", () => {
|
||||
expect(screen.queryByText("Video")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||
it("should render the »Live« subtitle", () => {
|
||||
expect(screen.queryByText("Live")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("and the broadcast stops", () => {
|
||||
beforeEach(async () => {
|
||||
const stopEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
voiceBroadcastInfoEvent,
|
||||
);
|
||||
await act(async () => {
|
||||
room.currentState.setStateEvents([stopEvent]);
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render the »Live« subtitle", () => {
|
||||
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a live voice broadcast starts", () => {
|
||||
beforeEach(async () => {
|
||||
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
|
||||
describe("when message previews are enabled", () => {
|
||||
beforeEach(() => {
|
||||
showMessagePreview = true;
|
||||
});
|
||||
|
||||
it("should render the »Live« subtitle", () => {
|
||||
expect(screen.queryByText("Live")).toBeInTheDocument();
|
||||
it("should render a room without a message as expected", async () => {
|
||||
renderRoomTile();
|
||||
// flush promises here because the preview is created asynchronously
|
||||
await flushPromises();
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("and the broadcast stops", () => {
|
||||
beforeEach(async () => {
|
||||
const stopEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
room.roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getSafeUserId(),
|
||||
client.getDeviceId()!,
|
||||
voiceBroadcastInfoEvent,
|
||||
);
|
||||
await act(async () => {
|
||||
room.currentState.setStateEvents([stopEvent]);
|
||||
await flushPromises();
|
||||
});
|
||||
describe("and there is a message in the room", () => {
|
||||
beforeEach(() => {
|
||||
addMessageToRoom(23);
|
||||
});
|
||||
|
||||
it("should not render the »Live« subtitle", () => {
|
||||
expect(screen.queryByText("Live")).not.toBeInTheDocument();
|
||||
it("should render as expected", async () => {
|
||||
renderRoomTile();
|
||||
expect(await screen.findByText("test message")).toBeInTheDocument();
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is a message in a thread", () => {
|
||||
beforeEach(() => {
|
||||
addThreadMessageToRoom(23);
|
||||
});
|
||||
|
||||
it("should render as expected", async () => {
|
||||
renderRoomTile();
|
||||
expect(await screen.findByText("test thread reply")).toBeInTheDocument();
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is a message and a thread without a reply", () => {
|
||||
beforeEach(() => {
|
||||
addMessageToRoom(23);
|
||||
|
||||
// Mock thread reply for tests.
|
||||
jest.spyOn(room, "getThreads").mockReturnValue([
|
||||
// @ts-ignore
|
||||
{
|
||||
lastReply: () => null,
|
||||
timeline: [],
|
||||
} as Thread,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should render the message preview", async () => {
|
||||
renderRoomTile();
|
||||
expect(await screen.findByText("test message")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,250 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomTile should render the room 1`] = `
|
||||
exports[`RoomTile when message previews are enabled and there is a message in a thread should render as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-describedby="mx_RoomTile_messagePreview_!1:example.org"
|
||||
aria-label="!1:example.org"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_RoomTile"
|
||||
role="treeitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
>
|
||||
<span
|
||||
class="mx_BaseAvatar"
|
||||
role="presentation"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_initial"
|
||||
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_image"
|
||||
data-testid="avatar-img"
|
||||
loading="lazy"
|
||||
src="data:image/png;base64,00"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomTile_titleContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomTile_title mx_RoomTile_titleWithSubtitle"
|
||||
tabindex="-1"
|
||||
title="!1:example.org"
|
||||
>
|
||||
<span
|
||||
dir="auto"
|
||||
>
|
||||
!1:example.org
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomTile_subtitle"
|
||||
id="mx_RoomTile_messagePreview_!1:example.org"
|
||||
title="test thread reply"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomTile_subtitle_text"
|
||||
>
|
||||
test thread reply
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mx_RoomTile_badgeContainer"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Room options"
|
||||
class="mx_AccessibleButton mx_RoomTile_menuButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Notification options"
|
||||
class="mx_AccessibleButton mx_RoomTile_notificationsButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RoomTile when message previews are enabled and there is a message in the room should render as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-describedby="mx_RoomTile_messagePreview_!1:example.org"
|
||||
aria-label="!1:example.org"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_RoomTile"
|
||||
role="treeitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
>
|
||||
<span
|
||||
class="mx_BaseAvatar"
|
||||
role="presentation"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_initial"
|
||||
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_image"
|
||||
data-testid="avatar-img"
|
||||
loading="lazy"
|
||||
src="data:image/png;base64,00"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomTile_titleContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomTile_title mx_RoomTile_titleWithSubtitle"
|
||||
tabindex="-1"
|
||||
title="!1:example.org"
|
||||
>
|
||||
<span
|
||||
dir="auto"
|
||||
>
|
||||
!1:example.org
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomTile_subtitle"
|
||||
id="mx_RoomTile_messagePreview_!1:example.org"
|
||||
title="test message"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomTile_subtitle_text"
|
||||
>
|
||||
test message
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mx_RoomTile_badgeContainer"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Room options"
|
||||
class="mx_AccessibleButton mx_RoomTile_menuButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Notification options"
|
||||
class="mx_AccessibleButton mx_RoomTile_notificationsButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RoomTile when message previews are enabled should render a room without a message as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-describedby="mx_RoomTile_messagePreview_!1:example.org"
|
||||
aria-label="!1:example.org"
|
||||
aria-selected="false"
|
||||
class="mx_AccessibleButton mx_RoomTile"
|
||||
role="treeitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_DecoratedRoomAvatar"
|
||||
>
|
||||
<span
|
||||
class="mx_BaseAvatar"
|
||||
role="presentation"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_initial"
|
||||
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="mx_BaseAvatar_image"
|
||||
data-testid="avatar-img"
|
||||
loading="lazy"
|
||||
src="data:image/png;base64,00"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomTile_titleContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomTile_title"
|
||||
tabindex="-1"
|
||||
title="!1:example.org"
|
||||
>
|
||||
<span
|
||||
dir="auto"
|
||||
>
|
||||
!1:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mx_RoomTile_badgeContainer"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Room options"
|
||||
class="mx_AccessibleButton mx_RoomTile_menuButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Notification options"
|
||||
class="mx_AccessibleButton mx_RoomTile_notificationsButton"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RoomTile when message previews are not enabled should render the room 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="!1:example.org"
|
||||
|
|
|
@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { EventTimeline, EventType, MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MessagePreviewStore } from "../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { mkEvent, mkMessage, mkStubRoom, setupAsyncStoreWithClient, stubClient } from "../../test-utils";
|
||||
import { mkEvent, mkMessage, mkReaction, setupAsyncStoreWithClient, stubClient } from "../../test-utils";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list/models";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
|
||||
describe("MessagePreviewStore", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let store: MessagePreviewStore;
|
||||
|
||||
async function addEvent(
|
||||
store: MessagePreviewStore,
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
fireAction = true,
|
||||
): Promise<void> {
|
||||
room.timeline.push(event);
|
||||
mocked(room.findEventById).mockImplementation((eventId) => room.timeline.find((e) => e.getId() === eventId));
|
||||
room.addLiveEvents([event]);
|
||||
if (fireAction) {
|
||||
// @ts-ignore private access
|
||||
await store.onAction({
|
||||
|
@ -42,15 +46,17 @@ describe("MessagePreviewStore", () => {
|
|||
}
|
||||
}
|
||||
|
||||
it("should ignore edits for events other than the latest one", async () => {
|
||||
const client = stubClient();
|
||||
const room = mkStubRoom("!roomId:server", "Room", client);
|
||||
beforeEach(async () => {
|
||||
client = mocked(stubClient());
|
||||
room = new Room("!roomId:server", client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const store = MessagePreviewStore.testInstance();
|
||||
store = MessagePreviewStore.testInstance();
|
||||
await store.start();
|
||||
await setupAsyncStoreWithClient(store, client);
|
||||
});
|
||||
|
||||
it("should ignore edits for events other than the latest one", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
|
@ -59,7 +65,7 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, firstMessage, false);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.Untagged)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
|
@ -71,7 +77,7 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.Untagged)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
|
@ -93,7 +99,7 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, firstMessageEdit);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.Untagged)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
|
||||
|
@ -115,21 +121,13 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, secondMessageEdit);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.Untagged)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second Message Edit"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore edits to unknown events", async () => {
|
||||
const client = stubClient();
|
||||
const room = mkStubRoom("!roomId:server", "Room", client);
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const store = MessagePreviewStore.testInstance();
|
||||
await store.start();
|
||||
await setupAsyncStoreWithClient(store, client);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toMatchInlineSnapshot(`null`);
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toBeNull();
|
||||
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
|
@ -139,7 +137,7 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, firstMessage, true);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
|
@ -161,22 +159,15 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, randomEdit);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.Untagged)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.Untagged))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate correct preview for message events in DMs", async () => {
|
||||
const client = stubClient();
|
||||
const room = mkStubRoom("!roomId:server", "Room", client);
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
room.currentState.getJoinedMemberCount = jest.fn().mockReturnValue(2);
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getJoinedMemberCount = jest.fn().mockReturnValue(2);
|
||||
|
||||
const store = MessagePreviewStore.testInstance();
|
||||
await store.start();
|
||||
await setupAsyncStoreWithClient(store, client);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toMatchInlineSnapshot(`null`);
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toBeNull();
|
||||
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
|
@ -186,7 +177,7 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: First message"`,
|
||||
);
|
||||
|
||||
|
@ -198,8 +189,45 @@ describe("MessagePreviewStore", () => {
|
|||
});
|
||||
await addEvent(store, room, secondMessage);
|
||||
|
||||
await expect(store.getPreviewForRoom(room, DefaultTagID.DM)).resolves.toMatchInlineSnapshot(
|
||||
expect((await store.getPreviewForRoom(room, DefaultTagID.DM))?.text).toMatchInlineSnapshot(
|
||||
`"@sender:server: Second message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate the correct preview for a reaction", async () => {
|
||||
const firstMessage = mkMessage({
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
room: room.roomId,
|
||||
msg: "First message",
|
||||
});
|
||||
await addEvent(store, room, firstMessage);
|
||||
|
||||
const reaction = mkReaction(firstMessage);
|
||||
await addEvent(store, room, reaction);
|
||||
|
||||
const preview = await store.getPreviewForRoom(room, DefaultTagID.Untagged);
|
||||
expect(preview).toBeDefined();
|
||||
expect(preview.isThreadReply).toBe(false);
|
||||
expect(preview.text).toMatchInlineSnapshot(`"@sender:server reacted 🙃 to First message"`);
|
||||
});
|
||||
|
||||
it("should generate the correct preview for a reaction on a thread root", async () => {
|
||||
const { rootEvent, thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: client.getSafeUserId(),
|
||||
participantUserIds: [client.getSafeUserId()],
|
||||
});
|
||||
await addEvent(store, room, rootEvent);
|
||||
|
||||
const reaction = mkReaction(rootEvent, { ts: 42 });
|
||||
reaction.setThread(thread);
|
||||
await addEvent(store, room, reaction);
|
||||
|
||||
const preview = await store.getPreviewForRoom(room, DefaultTagID.Untagged);
|
||||
expect(preview).toBeDefined();
|
||||
expect(preview.isThreadReply).toBe(false);
|
||||
expect(preview.text).toContain("You reacted 🙃 to root event message");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
ConditionKind,
|
||||
PushRuleActionName,
|
||||
IPushRules,
|
||||
RelationType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||
|
@ -471,6 +472,29 @@ export type MessageEventProps = MakeEventPassThruProps & {
|
|||
msg?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a "🙃" reaction for the given event.
|
||||
* Uses the same room and user as for the event.
|
||||
*
|
||||
* @returns The reaction event
|
||||
*/
|
||||
export const mkReaction = (event: MatrixEvent, opts: Partial<MakeEventProps> = {}): MatrixEvent => {
|
||||
return mkEvent({
|
||||
event: true,
|
||||
room: event.getRoomId(),
|
||||
type: EventType.Reaction,
|
||||
user: event.getSender()!,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: event.getId(),
|
||||
key: "🙃",
|
||||
},
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an m.room.message event.
|
||||
* @param {Object} opts Values for the message
|
||||
|
|
|
@ -135,6 +135,11 @@ export const mkThread = ({
|
|||
}
|
||||
|
||||
const thread = room.createThread(rootEvent.getId()!, rootEvent, events, true);
|
||||
|
||||
events.forEach((event) => {
|
||||
thread.timeline.push(event);
|
||||
});
|
||||
|
||||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
|
||||
|
|
Loading…
Reference in New Issue