mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 09:15:41 +03:00
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:
parent
2dbaf00e71
commit
70418f8f3d
14 changed files with 189 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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";
|
||||
|
|
26
res/css/views/messages/_PinnedMessageBadge.pcss
Normal file
26
res/css/views/messages/_PinnedMessageBadge.pcss
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
24
src/components/views/messages/PinnedMessageBadge.tsx
Normal file
24
src/components/views/messages/PinnedMessageBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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()}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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",
|
||||
|
|
19
test/components/views/messages/PinnedMessageBadge-test.tsx
Normal file
19
test/components/views/messages/PinnedMessageBadge-test.tsx
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in a new issue