From 70418f8f3d743c142d6e0b5dff16ae190c08b5d7 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 4 Oct 2024 09:11:37 +0200 Subject: [PATCH] Add a pinned message badge under a pinned message (#118) * Add pinned message badge for Modern Layout * Add Bubble layout support * Add thread support * Add irc support * Rename event tile badges * Don't render footer when there is no reactions * Add a test for `PinnedMessageBadge.tsx` * Add a test in EventTile-test.tsx * Add e2e test --- playwright/e2e/pinned-messages/index.ts | 8 ++++ .../pinned-messages/pinned-messages.spec.ts | 16 ++++++++ .../pinned-message-Msg1-linux.png | Bin 0 -> 6953 bytes res/css/_components.pcss | 1 + .../views/messages/_PinnedMessageBadge.pcss | 26 +++++++++++++ res/css/views/messages/_ReactionsRow.pcss | 1 - res/css/views/rooms/_EventBubbleTile.pcss | 10 ++++- res/css/views/rooms/_EventTile.pcss | 18 +++++++-- .../views/messages/PinnedMessageBadge.tsx | 24 ++++++++++++ src/components/views/rooms/EventTile.tsx | 35 ++++++++++++++++-- src/i18n/strings/en_EN.json | 1 + .../messages/PinnedMessageBadge-test.tsx | 19 ++++++++++ .../PinnedMessageBadge-test.tsx.snap | 14 +++++++ .../components/views/rooms/EventTile-test.tsx | 27 ++++++++++++++ 14 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png create mode 100644 res/css/views/messages/_PinnedMessageBadge.pcss create mode 100644 src/components/views/messages/PinnedMessageBadge.tsx create mode 100644 test/components/views/messages/PinnedMessageBadge-test.tsx create mode 100644 test/components/views/messages/__snapshots__/PinnedMessageBadge-test.tsx.snap diff --git a/playwright/e2e/pinned-messages/index.ts b/playwright/e2e/pinned-messages/index.ts index 61e09a8111..ac50b62294 100644 --- a/playwright/e2e/pinned-messages/index.ts +++ b/playwright/e2e/pinned-messages/index.ts @@ -91,6 +91,14 @@ export class Helpers { await this.app.viewRoomByName(typeof room === "string" ? room : room.name); } + /** + * Get the timeline tile for the given message + * @param message + */ + getEventTile(message: string) { + return this.page.locator(".mx_EventTile", { hasText: message }); + } + /** * Pin the given message from the quick actions * @param message diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index f9d2abbb08..9f6f38f177 100644 --- a/playwright/e2e/pinned-messages/pinned-messages.spec.ts +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -18,6 +18,22 @@ test.describe("Pinned messages", () => { await util.assertEmptyPinnedMessagesList(); }); + test("should pin one message and to have the pinned message badge in the timeline", async ({ + page, + app, + room1, + util, + }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1"]); + await util.pinMessages(["Msg1"]); + + const tile = util.getEventTile("Msg1"); + await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { + mask: [tile.locator(".mx_MessageTimestamp")], + }); + }); + test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); diff --git a/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png b/playwright/snapshots/pinned-messages/pinned-messages.spec.ts/pinned-message-Msg1-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..c195186db845af7aaeb89da6e8fca33136c6f68c GIT binary patch literal 6953 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 literal 0 HcmV?d00001 diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 78e0524da6..d62c63572a 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -249,6 +249,7 @@ @import "./views/messages/_MessageActionBar.pcss"; @import "./views/messages/_MessageTimestamp.pcss"; @import "./views/messages/_MjolnirBody.pcss"; +@import "./views/messages/_PinnedMessageBadge.pcss"; @import "./views/messages/_ReactionsRow.pcss"; @import "./views/messages/_ReactionsRowButton.pcss"; @import "./views/messages/_RedactedBody.pcss"; diff --git a/res/css/views/messages/_PinnedMessageBadge.pcss b/res/css/views/messages/_PinnedMessageBadge.pcss new file mode 100644 index 0000000000..99770a7037 --- /dev/null +++ b/res/css/views/messages/_PinnedMessageBadge.pcss @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + * + */ + +.mx_PinnedMessageBadge { + position: relative; + display: flex; + align-items: center; + gap: var(--cpd-space-1x); + + padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x); + font: var(--cpd-font-body-xs-medium); + background-color: var(--cpd-color-alpha-gray-200); + color: var(--cpd-color-text-secondary); + + border-radius: 99px; + border: 1px solid var(--cpd-color-alpha-gray-400); + + svg { + fill: var(--cpd-color-icon-secondary); + } +} diff --git a/res/css/views/messages/_ReactionsRow.pcss b/res/css/views/messages/_ReactionsRow.pcss index 3a820cfdc7..e07b529ef4 100644 --- a/res/css/views/messages/_ReactionsRow.pcss +++ b/res/css/views/messages/_ReactionsRow.pcss @@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details. */ .mx_ReactionsRow { - margin: 6px 0; color: var(--cpd-color-text-primary); .mx_ReactionsRow_addReactionButton { diff --git a/res/css/views/rooms/_EventBubbleTile.pcss b/res/css/views/rooms/_EventBubbleTile.pcss index ec443c44de..3a42cde9bb 100644 --- a/res/css/views/rooms/_EventBubbleTile.pcss +++ b/res/css/views/rooms/_EventBubbleTile.pcss @@ -172,7 +172,8 @@ Please see LICENSE files in the repository root for full details. border-color: $quinary-content; } - .mx_ReactionsRow { + .mx_EventTile_footer { + margin: var(--cpd-space-1-5x) 0; margin-inline: var(--EventTile_bubble_line-margin-inline-start) var(--EventTile_bubble_line-margin-inline-end); } @@ -204,7 +205,8 @@ Please see LICENSE files in the repository root for full details. margin-inline-end: auto; } - .mx_ReactionsRow { + .mx_ReactionsRow, + .mx_EventTile_footer { justify-content: flex-start; } @@ -245,6 +247,10 @@ Please see LICENSE files in the repository root for full details. max-width: 100%; } + .mx_EventTile_footer { + justify-content: flex-end; + } + .mx_ReactionsRow { justify-content: flex-end; diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index e1bd304632..92e4cf78ea 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -463,6 +463,10 @@ $left-gutter: 64px; margin-left: calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding)); } } + + .mx_EventTile_footer { + margin: var(--cpd-space-1-5x) 0; + } } &[data-layout="group"] { @@ -509,8 +513,8 @@ $left-gutter: 64px; margin-left: $left-gutter; } - .mx_ReactionsRow { - margin: $spacing-4 64px; + .mx_EventTile_footer { + margin: var(--cpd-space-1x) var(--cpd-space-16x); } > .mx_DisambiguatedProfile { @@ -1248,7 +1252,7 @@ $left-gutter: 64px; padding-block-start: $spacing-16; .mx_EventTile_line, - .mx_ReactionsRow { + .mx_EventTile_footer { margin-inline-end: var(--ThreadView_group_spacing-end); } @@ -1266,7 +1270,7 @@ $left-gutter: 64px; } } - .mx_ReactionsRow { + .mx_EventTile_footer { /* Align with message text and summary text */ margin-inline-start: var(--ThreadView_group_spacing-start); } @@ -1456,6 +1460,12 @@ $left-gutter: 64px; display: flex; } +.mx_EventTile_footer { + display: flex; + gap: var(--cpd-space-2x); + align-items: center; +} + /* Media query for mobile UI */ @media only screen and (max-width: 480px) { .mx_EventTile_content { diff --git a/src/components/views/messages/PinnedMessageBadge.tsx b/src/components/views/messages/PinnedMessageBadge.tsx new file mode 100644 index 0000000000..bfe1919597 --- /dev/null +++ b/src/components/views/messages/PinnedMessageBadge.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + * + */ + +import React, { JSX } from "react"; +import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg"; + +import { _t } from "../../../languageHandler.tsx"; + +/** + * A badge to indicate that a message is pinned. + */ +export function PinnedMessageBadge(): JSX.Element { + return ( +
+ + {_t("room|pinned_message_badge")} +
+ ); +} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 746cceabd8..a41dbc218f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, forwardRef, MouseEvent, ReactNode } from "react"; +import React, { createRef, forwardRef, JSX, MouseEvent, ReactNode } from "react"; import classNames from "classnames"; import { EventStatus, @@ -76,6 +76,8 @@ import { ElementCall } from "../../../models/Call"; import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge"; import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar"; import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper"; +import PinningUtils from "../../../utils/PinningUtils.ts"; +import { PinnedMessageBadge } from "../messages/PinnedMessageBadge.tsx"; export type GetRelationsForEvent = ( eventId: string, @@ -1123,6 +1125,11 @@ export class UnwrappedEventTile extends React.Component const timestamp = showTimestamp && ts ? messageTimestamp : null; + let pinnedMessageBadge: JSX.Element | undefined; + if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) { + pinnedMessageBadge = ; + } + let reactionsRow: JSX.Element | undefined; if (!isRedacted) { reactionsRow = ( @@ -1134,6 +1141,9 @@ export class UnwrappedEventTile extends React.Component ); } + // If we have reactions or a pinned message badge, we need a footer + const hasFooter = Boolean((reactionsRow && this.state.reactions) || pinnedMessageBadge); + const linkedTimestamp = !this.props.hideTimestamp ? ( {msgOption} , - reactionsRow, + hasFooter && ( +
+ {(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge} + {reactionsRow} + {this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge} +
+ ), ], ); } @@ -1428,14 +1444,25 @@ export class UnwrappedEventTile extends React.Component {actionBar} {this.props.layout === Layout.IRC && ( <> - {reactionsRow} + {hasFooter && ( +
+ {pinnedMessageBadge} + {reactionsRow} +
+ )} {this.renderThreadInfo()} )} {this.props.layout !== Layout.IRC && ( <> - {reactionsRow} + {hasFooter && ( +
+ {(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge} + {reactionsRow} + {this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge} +
+ )} {this.renderThreadInfo()} )} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8653b0ec4e..21619b99d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2034,6 +2034,7 @@ "not_found_title": "This room or space does not exist.", "not_found_title_name": "%(roomName)s does not exist.", "peek_join_prompt": "You're previewing %(roomName)s. Want to join it?", + "pinned_message_badge": "Pinned message", "pinned_message_banner": { "button_close_list": "Close list", "button_view_all": "View all", diff --git a/test/components/views/messages/PinnedMessageBadge-test.tsx b/test/components/views/messages/PinnedMessageBadge-test.tsx new file mode 100644 index 0000000000..ba35e4de39 --- /dev/null +++ b/test/components/views/messages/PinnedMessageBadge-test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + * + */ + +import React from "react"; +import { render } from "@testing-library/react"; + +import { PinnedMessageBadge } from "../../../../src/components/views/messages/PinnedMessageBadge.tsx"; + +describe("PinnedMessageBadge", () => { + it("should render", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/messages/__snapshots__/PinnedMessageBadge-test.tsx.snap b/test/components/views/messages/__snapshots__/PinnedMessageBadge-test.tsx.snap new file mode 100644 index 0000000000..6a2beb4864 --- /dev/null +++ b/test/components/views/messages/__snapshots__/PinnedMessageBadge-test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PinnedMessageBadge should render 1`] = ` + +
+
+ Pinned message +
+ +`; diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index ee87e3250a..138cf1cdef 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -32,6 +32,8 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap"; import dis from "../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../src/dispatcher/actions"; import { IRoomState } from "../../../../src/components/structures/RoomView"; +import PinningUtils from "../../../../src/utils/PinningUtils"; +import { Layout } from "../../../../src/settings/enums/Layout"; describe("EventTile", () => { const ROOM_ID = "!roomId:example.org"; @@ -91,6 +93,10 @@ describe("EventTile", () => { }); }); + afterEach(() => { + jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false); + }); + describe("EventTile thread summary", () => { beforeEach(() => { jest.spyOn(client, "supportsThreads").mockReturnValue(true); @@ -154,6 +160,27 @@ describe("EventTile", () => { }); }); + describe("EventTile renderingType: Threads", () => { + it("should display the pinned message badge", async () => { + jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true); + getComponent({}, TimelineRenderingType.Thread); + + expect(screen.getByText("Pinned message")).toBeInTheDocument(); + }); + }); + + describe("EventTile renderingType: default", () => { + it.each([[Layout.Group], [Layout.Bubble], [Layout.IRC]])( + "should display the pinned message badge", + async (layout) => { + jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true); + getComponent({ layout }); + + expect(screen.getByText("Pinned message")).toBeInTheDocument(); + }, + ); + }); + describe("EventTile in the right panel", () => { beforeAll(() => { const dmRoomMap: DMRoomMap = {