Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions playwright/e2e/pinned-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions playwright/e2e/pinned-messages/pinned-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
26 changes: 26 additions & 0 deletions res/css/views/messages/_PinnedMessageBadge.pcss
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 0 additions & 1 deletion res/css/views/messages/_ReactionsRow.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions res/css/views/rooms/_EventBubbleTile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

Expand Down
18 changes: 14 additions & 4 deletions res/css/views/rooms/_EventTile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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"] {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions src/components/views/messages/PinnedMessageBadge.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
35 changes: 31 additions & 4 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = (
Expand All @@ -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}
Expand Down Expand Up @@ -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>
),
],
);
}
Expand Down Expand Up @@ -1428,14 +1444,25 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{actionBar}
{this.props.layout === Layout.IRC && (
<>
{reactionsRow}
{hasFooter && (
<div className="mx_EventTile_footer">
{pinnedMessageBadge}
{reactionsRow}
</div>
)}
{this.renderThreadInfo()}
</>
)}
</div>
{this.props.layout !== Layout.IRC && (
<>
{reactionsRow}
{hasFooter && (
<div className="mx_EventTile_footer">
{(this.props.layout === Layout.Group || !isOwnEvent) && pinnedMessageBadge}
{reactionsRow}
{this.props.layout === Layout.Bubble && isOwnEvent && pinnedMessageBadge}
</div>
)}
{this.renderThreadInfo()}
</>
)}
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions test/components/views/messages/PinnedMessageBadge-test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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>
`;
27 changes: 27 additions & 0 deletions test/components/views/rooms/EventTile-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -91,6 +93,10 @@ describe("EventTile", () => {
});
});

afterEach(() => {
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
});

describe("EventTile thread summary", () => {
beforeEach(() => {
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
Expand Down Expand Up @@ -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 = {
Expand Down
Loading