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
This commit is contained in:
Florian Duros 2024-10-04 09:11:37 +02:00 committed by GitHub
parent 2dbaf00e71
commit 70418f8f3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 189 additions and 11 deletions

View file

@ -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

View file

@ -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"]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -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";

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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 (
<div className="mx_PinnedMessageBadge">
<PinIcon width="16" />
{_t("room|pinned_message_badge")}
</div>
);
}

View file

@ -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<EventTileProps, IState>
const timestamp = showTimestamp && ts ? messageTimestamp : null;
let pinnedMessageBadge: JSX.Element | undefined;
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
pinnedMessageBadge = <PinnedMessageBadge />;
}
let reactionsRow: JSX.Element | undefined;
if (!isRedacted) {
reactionsRow = (
@ -1134,6 +1141,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
);
}
// 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 ? (
<a
href={permalink}
@ -1239,7 +1249,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
</a>
{msgOption}
</div>,
reactionsRow,
hasFooter && (
<div className="mx_EventTile_footer" key="mx_EventTile_footer">
{(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge}
{reactionsRow}
{this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge}
</div>
),
],
);
}
@ -1428,14 +1444,25 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{actionBar}
{this.props.layout === Layout.IRC && (
<>
{hasFooter && (
<div className="mx_EventTile_footer">
{pinnedMessageBadge}
{reactionsRow}
</div>
)}
{this.renderThreadInfo()}
</>
)}
</div>
{this.props.layout !== Layout.IRC && (
<>
{hasFooter && (
<div className="mx_EventTile_footer">
{(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge}
{reactionsRow}
{this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge}
</div>
)}
{this.renderThreadInfo()}
</>
)}

View file

@ -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",

View file

@ -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(<PinnedMessageBadge />);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PinnedMessageBadge should render 1`] = `
<DocumentFragment>
<div
class="mx_PinnedMessageBadge"
>
<div
width="16"
/>
Pinned message
</div>
</DocumentFragment>
`;

View file

@ -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 = {