Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.

Commit 70418f8

Browse files
authored
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
1 parent 2dbaf00 commit 70418f8

File tree

14 files changed

+189
-11
lines changed

14 files changed

+189
-11
lines changed

playwright/e2e/pinned-messages/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export class Helpers {
9191
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
9292
}
9393

94+
/**
95+
* Get the timeline tile for the given message
96+
* @param message
97+
*/
98+
getEventTile(message: string) {
99+
return this.page.locator(".mx_EventTile", { hasText: message });
100+
}
101+
94102
/**
95103
* Pin the given message from the quick actions
96104
* @param message

playwright/e2e/pinned-messages/pinned-messages.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ test.describe("Pinned messages", () => {
1818
await util.assertEmptyPinnedMessagesList();
1919
});
2020

21+
test("should pin one message and to have the pinned message badge in the timeline", async ({
22+
page,
23+
app,
24+
room1,
25+
util,
26+
}) => {
27+
await util.goTo(room1);
28+
await util.receiveMessages(room1, ["Msg1"]);
29+
await util.pinMessages(["Msg1"]);
30+
31+
const tile = util.getEventTile("Msg1");
32+
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
33+
mask: [tile.locator(".mx_MessageTimestamp")],
34+
});
35+
});
36+
2137
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
2238
await util.goTo(room1);
2339
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
Loading

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@
249249
@import "./views/messages/_MessageActionBar.pcss";
250250
@import "./views/messages/_MessageTimestamp.pcss";
251251
@import "./views/messages/_MjolnirBody.pcss";
252+
@import "./views/messages/_PinnedMessageBadge.pcss";
252253
@import "./views/messages/_ReactionsRow.pcss";
253254
@import "./views/messages/_ReactionsRowButton.pcss";
254255
@import "./views/messages/_RedactedBody.pcss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
* Please see LICENSE files in the repository root for full details.
6+
*
7+
*/
8+
9+
.mx_PinnedMessageBadge {
10+
position: relative;
11+
display: flex;
12+
align-items: center;
13+
gap: var(--cpd-space-1x);
14+
15+
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
16+
font: var(--cpd-font-body-xs-medium);
17+
background-color: var(--cpd-color-alpha-gray-200);
18+
color: var(--cpd-color-text-secondary);
19+
20+
border-radius: 99px;
21+
border: 1px solid var(--cpd-color-alpha-gray-400);
22+
23+
svg {
24+
fill: var(--cpd-color-icon-secondary);
25+
}
26+
}

res/css/views/messages/_ReactionsRow.pcss

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
.mx_ReactionsRow {
9-
margin: 6px 0;
109
color: var(--cpd-color-text-primary);
1110

1211
.mx_ReactionsRow_addReactionButton {

res/css/views/rooms/_EventBubbleTile.pcss

+8-2
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ Please see LICENSE files in the repository root for full details.
172172
border-color: $quinary-content;
173173
}
174174

175-
.mx_ReactionsRow {
175+
.mx_EventTile_footer {
176+
margin: var(--cpd-space-1-5x) 0;
176177
margin-inline: var(--EventTile_bubble_line-margin-inline-start) var(--EventTile_bubble_line-margin-inline-end);
177178
}
178179

@@ -204,7 +205,8 @@ Please see LICENSE files in the repository root for full details.
204205
margin-inline-end: auto;
205206
}
206207

207-
.mx_ReactionsRow {
208+
.mx_ReactionsRow,
209+
.mx_EventTile_footer {
208210
justify-content: flex-start;
209211
}
210212

@@ -245,6 +247,10 @@ Please see LICENSE files in the repository root for full details.
245247
max-width: 100%;
246248
}
247249

250+
.mx_EventTile_footer {
251+
justify-content: flex-end;
252+
}
253+
248254
.mx_ReactionsRow {
249255
justify-content: flex-end;
250256

res/css/views/rooms/_EventTile.pcss

+14-4
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ $left-gutter: 64px;
463463
margin-left: calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding));
464464
}
465465
}
466+
467+
.mx_EventTile_footer {
468+
margin: var(--cpd-space-1-5x) 0;
469+
}
466470
}
467471

468472
&[data-layout="group"] {
@@ -509,8 +513,8 @@ $left-gutter: 64px;
509513
margin-left: $left-gutter;
510514
}
511515

512-
.mx_ReactionsRow {
513-
margin: $spacing-4 64px;
516+
.mx_EventTile_footer {
517+
margin: var(--cpd-space-1x) var(--cpd-space-16x);
514518
}
515519

516520
> .mx_DisambiguatedProfile {
@@ -1248,7 +1252,7 @@ $left-gutter: 64px;
12481252
padding-block-start: $spacing-16;
12491253

12501254
.mx_EventTile_line,
1251-
.mx_ReactionsRow {
1255+
.mx_EventTile_footer {
12521256
margin-inline-end: var(--ThreadView_group_spacing-end);
12531257
}
12541258

@@ -1266,7 +1270,7 @@ $left-gutter: 64px;
12661270
}
12671271
}
12681272

1269-
.mx_ReactionsRow {
1273+
.mx_EventTile_footer {
12701274
/* Align with message text and summary text */
12711275
margin-inline-start: var(--ThreadView_group_spacing-start);
12721276
}
@@ -1456,6 +1460,12 @@ $left-gutter: 64px;
14561460
display: flex;
14571461
}
14581462

1463+
.mx_EventTile_footer {
1464+
display: flex;
1465+
gap: var(--cpd-space-2x);
1466+
align-items: center;
1467+
}
1468+
14591469
/* Media query for mobile UI */
14601470
@media only screen and (max-width: 480px) {
14611471
.mx_EventTile_content {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
* Please see LICENSE files in the repository root for full details.
6+
*
7+
*/
8+
9+
import React, { JSX } from "react";
10+
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg";
11+
12+
import { _t } from "../../../languageHandler.tsx";
13+
14+
/**
15+
* A badge to indicate that a message is pinned.
16+
*/
17+
export function PinnedMessageBadge(): JSX.Element {
18+
return (
19+
<div className="mx_PinnedMessageBadge">
20+
<PinIcon width="16" />
21+
{_t("room|pinned_message_badge")}
22+
</div>
23+
);
24+
}

src/components/views/rooms/EventTile.tsx

+31-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
77
Please see LICENSE files in the repository root for full details.
88
*/
99

10-
import React, { createRef, forwardRef, MouseEvent, ReactNode } from "react";
10+
import React, { createRef, forwardRef, JSX, MouseEvent, ReactNode } from "react";
1111
import classNames from "classnames";
1212
import {
1313
EventStatus,
@@ -76,6 +76,8 @@ import { ElementCall } from "../../../models/Call";
7676
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
7777
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
7878
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
79+
import PinningUtils from "../../../utils/PinningUtils.ts";
80+
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge.tsx";
7981

8082
export type GetRelationsForEvent = (
8183
eventId: string,
@@ -1123,6 +1125,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
11231125

11241126
const timestamp = showTimestamp && ts ? messageTimestamp : null;
11251127

1128+
let pinnedMessageBadge: JSX.Element | undefined;
1129+
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
1130+
pinnedMessageBadge = <PinnedMessageBadge />;
1131+
}
1132+
11261133
let reactionsRow: JSX.Element | undefined;
11271134
if (!isRedacted) {
11281135
reactionsRow = (
@@ -1134,6 +1141,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
11341141
);
11351142
}
11361143

1144+
// If we have reactions or a pinned message badge, we need a footer
1145+
const hasFooter = Boolean((reactionsRow && this.state.reactions) || pinnedMessageBadge);
1146+
11371147
const linkedTimestamp = !this.props.hideTimestamp ? (
11381148
<a
11391149
href={permalink}
@@ -1239,7 +1249,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
12391249
</a>
12401250
{msgOption}
12411251
</div>,
1242-
reactionsRow,
1252+
hasFooter && (
1253+
<div className="mx_EventTile_footer" key="mx_EventTile_footer">
1254+
{(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge}
1255+
{reactionsRow}
1256+
{this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge}
1257+
</div>
1258+
),
12431259
],
12441260
);
12451261
}
@@ -1428,14 +1444,25 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
14281444
{actionBar}
14291445
{this.props.layout === Layout.IRC && (
14301446
<>
1431-
{reactionsRow}
1447+
{hasFooter && (
1448+
<div className="mx_EventTile_footer">
1449+
{pinnedMessageBadge}
1450+
{reactionsRow}
1451+
</div>
1452+
)}
14321453
{this.renderThreadInfo()}
14331454
</>
14341455
)}
14351456
</div>
14361457
{this.props.layout !== Layout.IRC && (
14371458
<>
1438-
{reactionsRow}
1459+
{hasFooter && (
1460+
<div className="mx_EventTile_footer">
1461+
{(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge}
1462+
{reactionsRow}
1463+
{this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge}
1464+
</div>
1465+
)}
14391466
{this.renderThreadInfo()}
14401467
</>
14411468
)}

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,7 @@
20342034
"not_found_title": "This room or space does not exist.",
20352035
"not_found_title_name": "%(roomName)s does not exist.",
20362036
"peek_join_prompt": "You're previewing %(roomName)s. Want to join it?",
2037+
"pinned_message_badge": "Pinned message",
20372038
"pinned_message_banner": {
20382039
"button_close_list": "Close list",
20392040
"button_view_all": "View all",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
* Please see LICENSE files in the repository root for full details.
6+
*
7+
*/
8+
9+
import React from "react";
10+
import { render } from "@testing-library/react";
11+
12+
import { PinnedMessageBadge } from "../../../../src/components/views/messages/PinnedMessageBadge.tsx";
13+
14+
describe("PinnedMessageBadge", () => {
15+
it("should render", () => {
16+
const { asFragment } = render(<PinnedMessageBadge />);
17+
expect(asFragment()).toMatchSnapshot();
18+
});
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`PinnedMessageBadge should render 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="mx_PinnedMessageBadge"
7+
>
8+
<div
9+
width="16"
10+
/>
11+
Pinned message
12+
</div>
13+
</DocumentFragment>
14+
`;

test/components/views/rooms/EventTile-test.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap";
3232
import dis from "../../../../src/dispatcher/dispatcher";
3333
import { Action } from "../../../../src/dispatcher/actions";
3434
import { IRoomState } from "../../../../src/components/structures/RoomView";
35+
import PinningUtils from "../../../../src/utils/PinningUtils";
36+
import { Layout } from "../../../../src/settings/enums/Layout";
3537

3638
describe("EventTile", () => {
3739
const ROOM_ID = "!roomId:example.org";
@@ -91,6 +93,10 @@ describe("EventTile", () => {
9193
});
9294
});
9395

96+
afterEach(() => {
97+
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
98+
});
99+
94100
describe("EventTile thread summary", () => {
95101
beforeEach(() => {
96102
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
@@ -154,6 +160,27 @@ describe("EventTile", () => {
154160
});
155161
});
156162

163+
describe("EventTile renderingType: Threads", () => {
164+
it("should display the pinned message badge", async () => {
165+
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true);
166+
getComponent({}, TimelineRenderingType.Thread);
167+
168+
expect(screen.getByText("Pinned message")).toBeInTheDocument();
169+
});
170+
});
171+
172+
describe("EventTile renderingType: default", () => {
173+
it.each([[Layout.Group], [Layout.Bubble], [Layout.IRC]])(
174+
"should display the pinned message badge",
175+
async (layout) => {
176+
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true);
177+
getComponent({ layout });
178+
179+
expect(screen.getByText("Pinned message")).toBeInTheDocument();
180+
},
181+
);
182+
});
183+
157184
describe("EventTile in the right panel", () => {
158185
beforeAll(() => {
159186
const dmRoomMap: DMRoomMap = {

0 commit comments

Comments
 (0)