Unify notifications panel event design (#9754)

This commit is contained in:
Germain 2022-12-21 10:25:50 +00:00 committed by GitHub
parent 6585fb1f55
commit bef8e077f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 191 additions and 219 deletions

View file

@ -62,7 +62,6 @@
@import "./structures/_MainSplit.pcss"; @import "./structures/_MainSplit.pcss";
@import "./structures/_MatrixChat.pcss"; @import "./structures/_MatrixChat.pcss";
@import "./structures/_NonUrgentToastContainer.pcss"; @import "./structures/_NonUrgentToastContainer.pcss";
@import "./structures/_NotificationPanel.pcss";
@import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_QuickSettingsButton.pcss";
@import "./structures/_RightPanel.pcss"; @import "./structures/_RightPanel.pcss";
@import "./structures/_RoomSearch.pcss"; @import "./structures/_RoomSearch.pcss";

View file

@ -1,113 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_NotificationPanel {
order: 2;
flex: 1 1 0;
overflow-y: auto;
display: flex;
.mx_RoomView_messageListWrapper {
flex-direction: row;
align-items: center;
justify-content: center;
}
.mx_RoomView_MessageList {
width: 100%;
h2 {
margin-left: 0;
}
}
/* FIXME: rather than having EventTile's default CSS be for MessagePanel,
we should make EventTile a base CSS class and customise it specifically
for usage in {Message,File,Notification}Panel. */
.mx_EventTile_avatar {
display: none;
}
.mx_EventTile {
word-break: break-word;
position: relative;
padding-block: 18px;
.mx_EventTile_senderDetails,
.mx_EventTile_line {
padding-left: 36px; /* align with the room name */
}
.mx_EventTile_senderDetails {
position: relative;
a {
display: flex;
column-gap: 5px; /* TODO: Use a spacing variable */
}
}
.mx_DisambiguatedProfile,
.mx_MessageTimestamp {
color: $primary-content;
font-size: $font-12px;
display: inline;
}
&:hover .mx_EventTile_line {
background-color: $background;
}
&:not(.mx_EventTile_last):not(.mx_EventTile_lastInSection)::after {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: $tertiary-content;
height: 1px;
opacity: 0.4;
content: "";
}
}
.mx_EventTile_roomName {
display: flex;
align-items: center;
column-gap: $spacing-8;
font-weight: bold;
font-size: $font-14px;
a {
color: $primary-content;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.mx_EventTile_selected .mx_EventTile_line {
padding-left: 0;
}
.mx_EventTile_content {
margin-right: 0;
}
}
.mx_NotificationPanel_empty::before {
mask-image: url("$(res)/img/element-icons/notifications.svg");
}

View file

@ -210,7 +210,6 @@ limitations under the License.
.mx_FilePanel, .mx_FilePanel,
.mx_UserInfo, .mx_UserInfo,
.mx_NotificationPanel,
.mx_MemberList { .mx_MemberList {
&.mx_BaseCard { &.mx_BaseCard {
padding: $spacing-32 0 0; padding: $spacing-32 0 0;

View file

@ -836,7 +836,8 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile[data-shape="ThreadsList"] { .mx_EventTile[data-shape="ThreadsList"],
.mx_EventTile[data-shape="Notification"] {
--topOffset: $spacing-12; --topOffset: $spacing-12;
--leftOffset: 48px; --leftOffset: 48px;
$borderRadius: 8px; $borderRadius: 8px;
@ -916,9 +917,7 @@ $left-gutter: 64px;
} }
.mx_DisambiguatedProfile { .mx_DisambiguatedProfile {
margin-inline: 0 $spacing-12;
display: inline-flex; display: inline-flex;
flex: 1;
.mx_DisambiguatedProfile_displayName, .mx_DisambiguatedProfile_displayName,
.mx_DisambiguatedProfile_mxid { .mx_DisambiguatedProfile_mxid {
@ -941,6 +940,7 @@ $left-gutter: 64px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 0; padding-bottom: 0;
padding-inline-start: var(--leftOffset);
.mx_ThreadPanel_replies { .mx_ThreadPanel_replies {
margin-top: $spacing-8; margin-top: $spacing-8;
@ -966,11 +966,6 @@ $left-gutter: 64px;
} }
} }
.mx_DisambiguatedProfile,
.mx_EventTile_line {
padding-inline-start: var(--leftOffset);
}
.mx_MessageTimestamp { .mx_MessageTimestamp {
font-size: $font-12px; font-size: $font-12px;
max-width: var(--MessageTimestamp-max-width); max-width: var(--MessageTimestamp-max-width);
@ -1300,6 +1295,21 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile_details {
display: flex;
width: -webkit-fill-available;
align-items: center;
justify-content: space-between;
gap: $spacing-8;
margin-left: var(--leftOffset);
.mx_EventTile_truncated {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* Media query for mobile UI */ /* Media query for mobile UI */
@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.mx_EventTile_content { .mx_EventTile_content {

View file

@ -25,6 +25,7 @@ import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import Heading from "../views/typography/Heading";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -90,8 +91,21 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
narrow: this.state.narrow, narrow: this.state.narrow,
}} }}
> >
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer> <BaseCard
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} /> header={
<Heading size="h4" className="mx_BaseCard_header_title_heading">
{_t("Notifications")}
</Heading>
}
/**
* Need to rename this CSS class to something more generic
* Will be done once all the panels are using a similar layout
*/
className="mx_ThreadPanel"
onClose={this.props.onClose}
withoutScrollContainer={true}
>
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
{content} {content}
</BaseCard> </BaseCard>
</RoomContext.Provider> </RoomContext.Provider>

View file

@ -29,8 +29,6 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { Icon as LinkIcon } from "../../../../res/img/element-icons/link.svg";
import { Icon as ViewInRoomIcon } from "../../../../res/img/element-icons/view-in-room.svg";
import ReplyChain from "../elements/ReplyChain"; import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@ -63,8 +61,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import Toolbar from "../../../accessibility/Toolbar";
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState"; import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
@ -85,6 +81,7 @@ import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayloa
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
import { ElementCall } from "../../../models/Call"; import { ElementCall } from "../../../models/Call";
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge"; import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
export type GetRelationsForEvent = ( export type GetRelationsForEvent = (
eventId: string, eventId: string,
@ -972,6 +969,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
isContinuation = false; isContinuation = false;
} }
const isRenderingNotification = this.context.timelineRenderingType === TimelineRenderingType.Notification;
const isEditing = !!this.props.editState; const isEditing = !!this.props.editState;
const classes = classNames({ const classes = classNames({
mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile_bubbleContainer: isBubbleMessage,
@ -996,7 +995,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
mx_EventTile_bad: isEncryptionFailure, mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === MsgType.Emote, mx_EventTile_emote: msgtype === MsgType.Emote,
mx_EventTile_noSender: this.props.hideSender, mx_EventTile_noSender: this.props.hideSender,
mx_EventTile_clamp: this.context.timelineRenderingType === TimelineRenderingType.ThreadsList, mx_EventTile_clamp:
this.context.timelineRenderingType === TimelineRenderingType.ThreadsList || isRenderingNotification,
mx_EventTile_noBubble: noBubbleEvent, mx_EventTile_noBubble: noBubbleEvent,
}); });
@ -1012,12 +1012,12 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// Local echos have a send "status". // Local echos have a send "status".
const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId(); const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId();
let avatar: JSX.Element; let avatar: JSX.Element | null = null;
let sender: JSX.Element; let sender: JSX.Element | null = null;
let avatarSize: number; let avatarSize: number;
let needsSenderProfile: boolean; let needsSenderProfile: boolean;
if (this.context.timelineRenderingType === TimelineRenderingType.Notification) { if (isRenderingNotification) {
avatarSize = 24; avatarSize = 24;
needsSenderProfile = true; needsSenderProfile = true;
} else if (isInfoMessage) { } else if (isInfoMessage) {
@ -1061,7 +1061,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
member = this.props.mxEvent.sender; member = this.props.mxEvent.sender;
} }
// In the ThreadsList view we use the entire EventTile as a click target to open the thread instead // In the ThreadsList view we use the entire EventTile as a click target to open the thread instead
const viewUserOnClick = this.context.timelineRenderingType !== TimelineRenderingType.ThreadsList; const viewUserOnClick = ![TimelineRenderingType.ThreadsList, TimelineRenderingType.Notification].includes(
this.context.timelineRenderingType,
);
avatar = ( avatar = (
<div className="mx_EventTile_avatar"> <div className="mx_EventTile_avatar">
<MemberAvatar <MemberAvatar
@ -1202,57 +1204,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId(); const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId();
switch (this.context.timelineRenderingType) { switch (this.context.timelineRenderingType) {
case TimelineRenderingType.Notification: {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
return React.createElement(
this.props.as || "li",
{
"className": classes,
"aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
},
[
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
{room ? room.name : ""}
</a>
</div>,
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
{avatar}
<a
href={permalink}
onClick={this.onPermalinkClicked}
onContextMenu={this.onTimestampContextMenu}
>
{sender}
{timestamp}
</a>
</div>,
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
{this.renderContextMenu()}
{renderTile(
TimelineRenderingType.Notification,
{
...this.props,
// overrides
ref: this.tile,
isSeeingThroughMessageHiddenForModeration,
// appease TS
highlights: this.props.highlights,
highlightLink: this.props.highlightLink,
onHeightChanged: this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator,
},
this.context.showHiddenEvents,
)}
</div>,
],
);
}
case TimelineRenderingType.Thread: { case TimelineRenderingType.Thread: {
return React.createElement( return React.createElement(
this.props.as || "li", this.props.as || "li",
@ -1289,8 +1240,8 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
// appease TS // appease TS
highlights: this.props.highlights, highlights: this.props.highlights,
highlightLink: this.props.highlightLink, highlightLink: this.props.highlightLink,
onHeightChanged: this.props.onHeightChanged, onHeightChanged: () => this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator!,
}, },
this.context.showHiddenEvents, this.context.showHiddenEvents,
)} )}
@ -1304,6 +1255,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
], ],
); );
} }
case TimelineRenderingType.Notification:
case TimelineRenderingType.ThreadsList: { case TimelineRenderingType.ThreadsList: {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
@ -1326,20 +1278,48 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
"onMouseEnter": () => this.setState({ hover: true }), "onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }), "onMouseLeave": () => this.setState({ hover: false }),
"onClick": (ev: MouseEvent) => { "onClick": (ev: MouseEvent) => {
const target = ev.currentTarget as HTMLElement;
let index = -1;
if (target.parentElement) index = Array.from(target.parentElement.children).indexOf(target);
switch (this.context.timelineRenderingType) {
case TimelineRenderingType.Notification:
this.viewInRoom(ev);
break;
case TimelineRenderingType.ThreadsList:
dis.dispatch<ShowThreadPayload>({ dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread, action: Action.ShowThread,
rootEvent: this.props.mxEvent, rootEvent: this.props.mxEvent,
push: true, push: true,
}); });
const target = ev.currentTarget as HTMLElement; PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index ?? -1);
const index = Array.from(target.parentElement.children).indexOf(target); break;
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index); }
}, },
}, },
<> <>
<div className="mx_EventTile_details">
{sender} {sender}
{avatar} {isRenderingNotification && room ? (
<span className="mx_EventTile_truncated">
{" "}
{_t(
" in <strong>%(room)s</strong>",
{ room: room.name },
{ strong: (sub) => <strong>{sub}</strong> },
)}
</span>
) : (
""
)}
{timestamp} {timestamp}
</div>
{isRenderingNotification && room ? (
<div className="mx_EventTile_avatar">
<RoomAvatar room={room} width={28} height={28} />
</div>
) : (
avatar
)}
<div className={lineClasses} key="mx_EventTile_line"> <div className={lineClasses} key="mx_EventTile_line">
<div className="mx_EventTile_body"> <div className="mx_EventTile_body">
{this.props.mxEvent.isRedacted() ? ( {this.props.mxEvent.isRedacted() ? (
@ -1350,24 +1330,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
</div> </div>
{this.renderThreadPanelSummary()} {this.renderThreadPanelSummary()}
</div> </div>
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off"> {this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && (
<RovingAccessibleTooltipButton <EventTileThreadToolbar
className="mx_MessageActionBar_iconButton" viewInRoom={this.viewInRoom}
onClick={this.viewInRoom} copyLinkToThread={this.copyLinkToThread}
title={_t("View in room")} />
key="view_in_room" )}
>
<ViewInRoomIcon />
</RovingAccessibleTooltipButton>
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
onClick={this.copyLinkToThread}
title={_t("Copy link to thread")}
key="copy_link_to_thread"
>
<LinkIcon />
</RovingAccessibleTooltipButton>
</Toolbar>
{msgOption} {msgOption}
<UnreadNotificationBadge room={room} threadId={this.props.mxEvent.getId()} /> <UnreadNotificationBadge room={room} threadId={this.props.mxEvent.getId()} />
</>, </>,

View file

@ -0,0 +1,53 @@
/*
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 React from "react";
import { RovingAccessibleTooltipButton } from "../../../../accessibility/RovingTabIndex";
import Toolbar from "../../../../accessibility/Toolbar";
import { _t } from "../../../../languageHandler";
import { Icon as LinkIcon } from "../../../../../res/img/element-icons/link.svg";
import { Icon as ViewInRoomIcon } from "../../../../../res/img/element-icons/view-in-room.svg";
import { ButtonEvent } from "../../elements/AccessibleButton";
export function EventTileThreadToolbar({
viewInRoom,
copyLinkToThread,
}: {
viewInRoom: (evt: ButtonEvent) => void;
copyLinkToThread: (evt: ButtonEvent) => void;
}) {
return (
<Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
onClick={viewInRoom}
title={_t("View in room")}
key="view_in_room"
>
<ViewInRoomIcon />
</RovingAccessibleTooltipButton>
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_iconButton"
onClick={copyLinkToThread}
title={_t("Copy link to thread")}
key="copy_link_to_thread"
>
<LinkIcon />
</RovingAccessibleTooltipButton>
</Toolbar>
);
}

View file

@ -1879,9 +1879,7 @@
"Mod": "Mod", "Mod": "Mod",
"From a thread": "From a thread", "From a thread": "From a thread",
"This event could not be displayed": "This event could not be displayed", "This event could not be displayed": "This event could not be displayed",
"Message Actions": "Message Actions", " in <strong>%(room)s</strong>": " in <strong>%(room)s</strong>",
"View in room": "View in room",
"Copy link to thread": "Copy link to thread",
"Encrypted by an unverified session": "Encrypted by an unverified session", "Encrypted by an unverified session": "Encrypted by an unverified session",
"Unencrypted": "Unencrypted", "Unencrypted": "Unencrypted",
"Encrypted by a deleted session": "Encrypted by a deleted session", "Encrypted by a deleted session": "Encrypted by a deleted session",
@ -2130,6 +2128,9 @@
"Italic": "Italic", "Italic": "Italic",
"Underline": "Underline", "Underline": "Underline",
"Code": "Code", "Code": "Code",
"Message Actions": "Message Actions",
"View in room": "View in room",
"Copy link to thread": "Copy link to thread",
"Error updating main address": "Error updating main address", "Error updating main address": "Error updating main address",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -23,6 +22,7 @@ import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; import { DeviceTrustLevel, UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api"; import { IEncryptedEventInfo } from "matrix-js-sdk/src/crypto/api";
import { render, waitFor, screen, act, fireEvent } from "@testing-library/react";
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile"; import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
@ -31,6 +31,9 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils";
import { mkThread } from "../../../test-utils/threads"; import { mkThread } from "../../../test-utils/threads";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import dis from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
describe("EventTile", () => { describe("EventTile", () => {
const ROOM_ID = "!roomId:example.org"; const ROOM_ID = "!roomId:example.org";
@ -159,6 +162,43 @@ describe("EventTile", () => {
}); });
}); });
describe("EventTile in the right panel", () => {
beforeAll(() => {
const dmRoomMap: DMRoomMap = {
getUserIdForRoomId: jest.fn(),
} as unknown as DMRoomMap;
DMRoomMap.setShared(dmRoomMap);
});
it("renders the room name for notifications", () => {
const { container } = getComponent({}, TimelineRenderingType.Notification);
expect(container.getElementsByClassName("mx_EventTile_details")[0]).toHaveTextContent(
"@alice:example.org in !roomId:example.org",
);
});
it("renders the sender for the thread list", () => {
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
expect(container.getElementsByClassName("mx_EventTile_details")[0]).toHaveTextContent("@alice:example.org");
});
it.each([
[TimelineRenderingType.Notification, Action.ViewRoom],
[TimelineRenderingType.ThreadsList, Action.ShowThread],
])("type %s dispatches %s", (renderingType, action) => {
jest.spyOn(dis, "dispatch");
const { container } = getComponent({}, renderingType);
fireEvent.click(container.querySelector("li")!);
expect(dis.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action,
}),
);
});
});
describe("Event verification", () => { describe("Event verification", () => {
// data for our stubbed getEventEncryptionInfo: a map from event id to result // data for our stubbed getEventEncryptionInfo: a map from event id to result
const eventToEncryptionInfoMap = new Map<string, IEncryptedEventInfo>(); const eventToEncryptionInfoMap = new Map<string, IEncryptedEventInfo>();