Unify notifications panel event design (#9754)
This commit is contained in:
parent
6585fb1f55
commit
bef8e077f6
9 changed files with 191 additions and 219 deletions
|
@ -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";
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) => {
|
||||||
dis.dispatch<ShowThreadPayload>({
|
|
||||||
action: Action.ShowThread,
|
|
||||||
rootEvent: this.props.mxEvent,
|
|
||||||
push: true,
|
|
||||||
});
|
|
||||||
const target = ev.currentTarget as HTMLElement;
|
const target = ev.currentTarget as HTMLElement;
|
||||||
const index = Array.from(target.parentElement.children).indexOf(target);
|
let index = -1;
|
||||||
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index);
|
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>({
|
||||||
|
action: Action.ShowThread,
|
||||||
|
rootEvent: this.props.mxEvent,
|
||||||
|
push: true,
|
||||||
|
});
|
||||||
|
PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index ?? -1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
<>
|
<>
|
||||||
{sender}
|
<div className="mx_EventTile_details">
|
||||||
{avatar}
|
{sender}
|
||||||
{timestamp}
|
{isRenderingNotification && room ? (
|
||||||
|
<span className="mx_EventTile_truncated">
|
||||||
|
{" "}
|
||||||
|
{_t(
|
||||||
|
" in <strong>%(room)s</strong>",
|
||||||
|
{ room: room.name },
|
||||||
|
{ strong: (sub) => <strong>{sub}</strong> },
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
{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()} />
|
||||||
</>,
|
</>,
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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.",
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
Loading…
Reference in a new issue