diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 1abf50ee41..bbc6a3eadc 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -62,7 +62,6 @@ @import "./structures/_MainSplit.pcss"; @import "./structures/_MatrixChat.pcss"; @import "./structures/_NonUrgentToastContainer.pcss"; -@import "./structures/_NotificationPanel.pcss"; @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; diff --git a/res/css/structures/_NotificationPanel.pcss b/res/css/structures/_NotificationPanel.pcss deleted file mode 100644 index 2ff3335280..0000000000 --- a/res/css/structures/_NotificationPanel.pcss +++ /dev/null @@ -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"); -} diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index d21bbdcdc0..22720a99e0 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -210,7 +210,6 @@ limitations under the License. .mx_FilePanel, .mx_UserInfo, -.mx_NotificationPanel, .mx_MemberList { &.mx_BaseCard { padding: $spacing-32 0 0; diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 9932fcfd9f..1169f51388 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -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; --leftOffset: 48px; $borderRadius: 8px; @@ -916,9 +917,7 @@ $left-gutter: 64px; } .mx_DisambiguatedProfile { - margin-inline: 0 $spacing-12; display: inline-flex; - flex: 1; .mx_DisambiguatedProfile_displayName, .mx_DisambiguatedProfile_mxid { @@ -941,6 +940,7 @@ $left-gutter: 64px; width: 100%; box-sizing: border-box; padding-bottom: 0; + padding-inline-start: var(--leftOffset); .mx_ThreadPanel_replies { margin-top: $spacing-8; @@ -966,11 +966,6 @@ $left-gutter: 64px; } } - .mx_DisambiguatedProfile, - .mx_EventTile_line { - padding-inline-start: var(--leftOffset); - } - .mx_MessageTimestamp { font-size: $font-12px; 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 only screen and (max-width: 480px) { .mx_EventTile_content { diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index cf3f3e0fba..9e4365880e 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -25,6 +25,7 @@ import Spinner from "../views/elements/Spinner"; import { Layout } from "../../settings/enums/Layout"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import Measured from "../views/elements/Measured"; +import Heading from "../views/typography/Heading"; interface IProps { onClose(): void; @@ -90,8 +91,21 @@ export default class NotificationPanel extends React.PureComponent - - + + {_t("Notifications")} + + } + /** + * 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 && } {content} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 711b6e315d..32136018a8 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -29,8 +29,6 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning"; 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 { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -63,8 +61,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; -import Toolbar from "../../../accessibility/Toolbar"; -import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; import { ThreadNotificationState } from "../../../stores/notifications/ThreadNotificationState"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; @@ -85,6 +81,7 @@ import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayloa import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; import { ElementCall } from "../../../models/Call"; import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge"; +import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar"; export type GetRelationsForEvent = ( eventId: string, @@ -972,6 +969,8 @@ export class UnwrappedEventTile extends React.Component isContinuation = false; } + const isRenderingNotification = this.context.timelineRenderingType === TimelineRenderingType.Notification; + const isEditing = !!this.props.editState; const classes = classNames({ mx_EventTile_bubbleContainer: isBubbleMessage, @@ -996,7 +995,8 @@ export class UnwrappedEventTile extends React.Component mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === MsgType.Emote, 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, }); @@ -1012,12 +1012,12 @@ export class UnwrappedEventTile extends React.Component // Local echos have a send "status". const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId(); - let avatar: JSX.Element; - let sender: JSX.Element; + let avatar: JSX.Element | null = null; + let sender: JSX.Element | null = null; let avatarSize: number; let needsSenderProfile: boolean; - if (this.context.timelineRenderingType === TimelineRenderingType.Notification) { + if (isRenderingNotification) { avatarSize = 24; needsSenderProfile = true; } else if (isInfoMessage) { @@ -1061,7 +1061,9 @@ export class UnwrappedEventTile extends React.Component member = this.props.mxEvent.sender; } // 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 = (
const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId(); 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, - }, - [ - , - , -
- {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, - )} -
, - ], - ); - } case TimelineRenderingType.Thread: { return React.createElement( this.props.as || "li", @@ -1289,8 +1240,8 @@ export class UnwrappedEventTile extends React.Component // appease TS highlights: this.props.highlights, highlightLink: this.props.highlightLink, - onHeightChanged: this.props.onHeightChanged, - permalinkCreator: this.props.permalinkCreator, + onHeightChanged: () => this.props.onHeightChanged, + permalinkCreator: this.props.permalinkCreator!, }, this.context.showHiddenEvents, )} @@ -1304,6 +1255,7 @@ export class UnwrappedEventTile extends React.Component ], ); } + case TimelineRenderingType.Notification: case TimelineRenderingType.ThreadsList: { 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 @@ -1326,20 +1278,48 @@ export class UnwrappedEventTile extends React.Component "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), "onClick": (ev: MouseEvent) => { - dis.dispatch({ - action: Action.ShowThread, - rootEvent: this.props.mxEvent, - push: true, - }); const target = ev.currentTarget as HTMLElement; - const index = Array.from(target.parentElement.children).indexOf(target); - PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index); + 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({ + action: Action.ShowThread, + rootEvent: this.props.mxEvent, + push: true, + }); + PosthogTrackers.trackInteraction("WebThreadsPanelThreadItem", ev, index ?? -1); + break; + } }, }, <> - {sender} - {avatar} - {timestamp} +
+ {sender} + {isRenderingNotification && room ? ( + + {" "} + {_t( + " in %(room)s", + { room: room.name }, + { strong: (sub) => {sub} }, + )} + + ) : ( + "" + )} + {timestamp} +
+ {isRenderingNotification && room ? ( +
+ +
+ ) : ( + avatar + )}
{this.props.mxEvent.isRedacted() ? ( @@ -1350,24 +1330,13 @@ export class UnwrappedEventTile extends React.Component
{this.renderThreadPanelSummary()}
- - - - - - - - + {this.context.timelineRenderingType === TimelineRenderingType.ThreadsList && ( + + )} + {msgOption} , diff --git a/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx b/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx new file mode 100644 index 0000000000..0c4dcda8ac --- /dev/null +++ b/src/components/views/rooms/EventTile/EventTileThreadToolbar.tsx @@ -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 ( + + + + + + + + + ); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7cf87238eb..43247b0bd1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1879,9 +1879,7 @@ "Mod": "Mod", "From a thread": "From a thread", "This event could not be displayed": "This event could not be displayed", - "Message Actions": "Message Actions", - "View in room": "View in room", - "Copy link to thread": "Copy link to thread", + " in %(room)s": " in %(room)s", "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", "Encrypted by a deleted session": "Encrypted by a deleted session", @@ -2130,6 +2128,9 @@ "Italic": "Italic", "Underline": "Underline", "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", "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.", diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index 1f347b9e4a..f425bc5aa5 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import * as React from "react"; -import { act, render, screen, waitFor } from "@testing-library/react"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -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 { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; 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 MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; @@ -31,6 +31,9 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { getRoomContext, mkEncryptedEvent, mkEvent, mkMessage, stubClient } from "../../../test-utils"; 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", () => { 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", () => { // data for our stubbed getEventEncryptionInfo: a map from event id to result const eventToEncryptionInfoMap = new Map();