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);
|
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
|
* Pin the given message from the quick actions
|
||||||
* @param message
|
* @param message
|
||||||
|
|
|
@ -18,6 +18,22 @@ test.describe("Pinned messages", () => {
|
||||||
await util.assertEmptyPinnedMessagesList();
|
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 }) => {
|
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
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/_MessageActionBar.pcss";
|
||||||
@import "./views/messages/_MessageTimestamp.pcss";
|
@import "./views/messages/_MessageTimestamp.pcss";
|
||||||
@import "./views/messages/_MjolnirBody.pcss";
|
@import "./views/messages/_MjolnirBody.pcss";
|
||||||
|
@import "./views/messages/_PinnedMessageBadge.pcss";
|
||||||
@import "./views/messages/_ReactionsRow.pcss";
|
@import "./views/messages/_ReactionsRow.pcss";
|
||||||
@import "./views/messages/_ReactionsRowButton.pcss";
|
@import "./views/messages/_ReactionsRowButton.pcss";
|
||||||
@import "./views/messages/_RedactedBody.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 {
|
.mx_ReactionsRow {
|
||||||
margin: 6px 0;
|
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
|
|
||||||
.mx_ReactionsRow_addReactionButton {
|
.mx_ReactionsRow_addReactionButton {
|
||||||
|
|
|
@ -172,7 +172,8 @@ Please see LICENSE files in the repository root for full details.
|
||||||
border-color: $quinary-content;
|
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);
|
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;
|
margin-inline-end: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow,
|
||||||
|
.mx_EventTile_footer {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,6 +247,10 @@ Please see LICENSE files in the repository root for full details.
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_footer {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
|
|
@ -463,6 +463,10 @@ $left-gutter: 64px;
|
||||||
margin-left: calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding));
|
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"] {
|
&[data-layout="group"] {
|
||||||
|
@ -509,8 +513,8 @@ $left-gutter: 64px;
|
||||||
margin-left: $left-gutter;
|
margin-left: $left-gutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_EventTile_footer {
|
||||||
margin: $spacing-4 64px;
|
margin: var(--cpd-space-1x) var(--cpd-space-16x);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_DisambiguatedProfile {
|
> .mx_DisambiguatedProfile {
|
||||||
|
@ -1248,7 +1252,7 @@ $left-gutter: 64px;
|
||||||
padding-block-start: $spacing-16;
|
padding-block-start: $spacing-16;
|
||||||
|
|
||||||
.mx_EventTile_line,
|
.mx_EventTile_line,
|
||||||
.mx_ReactionsRow {
|
.mx_EventTile_footer {
|
||||||
margin-inline-end: var(--ThreadView_group_spacing-end);
|
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 */
|
/* Align with message text and summary text */
|
||||||
margin-inline-start: var(--ThreadView_group_spacing-start);
|
margin-inline-start: var(--ThreadView_group_spacing-start);
|
||||||
}
|
}
|
||||||
|
@ -1456,6 +1460,12 @@ $left-gutter: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_footer {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Media query for mobile UI */
|
/* Media query for mobile UI */
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_EventTile_content {
|
.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.
|
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 classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
EventStatus,
|
EventStatus,
|
||||||
|
@ -76,6 +76,8 @@ import { ElementCall } from "../../../models/Call";
|
||||||
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
|
||||||
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
|
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
|
||||||
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils.ts";
|
||||||
|
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge.tsx";
|
||||||
|
|
||||||
export type GetRelationsForEvent = (
|
export type GetRelationsForEvent = (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
|
@ -1123,6 +1125,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
|
|
||||||
const timestamp = showTimestamp && ts ? messageTimestamp : null;
|
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;
|
let reactionsRow: JSX.Element | undefined;
|
||||||
if (!isRedacted) {
|
if (!isRedacted) {
|
||||||
reactionsRow = (
|
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 ? (
|
const linkedTimestamp = !this.props.hideTimestamp ? (
|
||||||
<a
|
<a
|
||||||
href={permalink}
|
href={permalink}
|
||||||
|
@ -1239,7 +1249,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
</a>
|
</a>
|
||||||
{msgOption}
|
{msgOption}
|
||||||
</div>,
|
</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}
|
{actionBar}
|
||||||
{this.props.layout === Layout.IRC && (
|
{this.props.layout === Layout.IRC && (
|
||||||
<>
|
<>
|
||||||
{reactionsRow}
|
{hasFooter && (
|
||||||
|
<div className="mx_EventTile_footer">
|
||||||
|
{pinnedMessageBadge}
|
||||||
|
{reactionsRow}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{this.renderThreadInfo()}
|
{this.renderThreadInfo()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{this.props.layout !== Layout.IRC && (
|
{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()}
|
{this.renderThreadInfo()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2034,6 +2034,7 @@
|
||||||
"not_found_title": "This room or space does not exist.",
|
"not_found_title": "This room or space does not exist.",
|
||||||
"not_found_title_name": "%(roomName)s does not exist.",
|
"not_found_title_name": "%(roomName)s does not exist.",
|
||||||
"peek_join_prompt": "You're previewing %(roomName)s. Want to join it?",
|
"peek_join_prompt": "You're previewing %(roomName)s. Want to join it?",
|
||||||
|
"pinned_message_badge": "Pinned message",
|
||||||
"pinned_message_banner": {
|
"pinned_message_banner": {
|
||||||
"button_close_list": "Close list",
|
"button_close_list": "Close list",
|
||||||
"button_view_all": "View all",
|
"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 dis from "../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||||
|
import PinningUtils from "../../../../src/utils/PinningUtils";
|
||||||
|
import { Layout } from "../../../../src/settings/enums/Layout";
|
||||||
|
|
||||||
describe("EventTile", () => {
|
describe("EventTile", () => {
|
||||||
const ROOM_ID = "!roomId:example.org";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
|
@ -91,6 +93,10 @@ describe("EventTile", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe("EventTile thread summary", () => {
|
describe("EventTile thread summary", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
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", () => {
|
describe("EventTile in the right panel", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const dmRoomMap: DMRoomMap = {
|
const dmRoomMap: DMRoomMap = {
|
||||||
|
|
Loading…
Reference in a new issue