Skip to content

Commit

Permalink
Display pinned messages on a banner at the top of a room (#12917)
Browse files Browse the repository at this point in the history
* Move pinned message hooks to a dedicated file

* Add a banner at the top of a room to display the pinned messages

* Put the pinning banner behind labs pinning labs flag

* Add redacted event support

* Handle UTD in pinning message banner

* Add tests for redaction

* Make all the banner clickable

* Add tests for PinnedMessageBanner.tsx

* Add e2e tests for the pinned message banner

* Review changes
  • Loading branch information
florianduros authored Aug 29, 2024
1 parent 8b2ded8 commit d16ab09
Show file tree
Hide file tree
Showing 29 changed files with 1,130 additions and 180 deletions.
34 changes: 31 additions & 3 deletions playwright/e2e/pinned-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,8 @@ export class Helpers {

/**
* Return the right panel
* @private
*/
private getRightPanel() {
public getRightPanel() {
return this.page.locator("#mx_RightPanel");
}

Expand All @@ -183,7 +182,6 @@ export class Helpers {
await expect(rightPanel.getByRole("heading", { name: "Pinned messages" })).toHaveText(
`${messages.length} Pinned messages`,
);
await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-messages-${messages.length}.png`);

const list = rightPanel.getByRole("list");
await expect(list.getByRole("listitem")).toHaveCount(messages.length);
Expand Down Expand Up @@ -243,6 +241,36 @@ export class Helpers {
await item.getByRole("button").click();
await this.page.getByRole("menu", { name: "Open menu" }).getByRole("menuitem", { name: "Unpin" }).click();
}

/**
* Return the banner
* @private
*/
public getBanner() {
return this.page.getByTestId("pinned-message-banner");
}

/**
* Assert that the banner contains the given message
* @param msg
*/
async assertMessageInBanner(msg: string) {
await expect(this.getBanner().getByText(msg)).toBeVisible();
}

/**
* Return the view all button
*/
public getViewAllButton() {
return this.page.getByRole("button", { name: "View all" });
}

/**
* Return the close list button
*/
public getCloseListButton() {
return this.page.getByRole("button", { name: "Close list" });
}
}

export { expect };
63 changes: 63 additions & 0 deletions playwright/e2e/pinned-messages/pinned-messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test.describe("Pinned messages", () => {
await util.openRoomInfo();
await util.openPinnedMessagesList();
await util.assertPinnedMessagesList(["Msg1", "Msg2", "Msg4"]);
await expect(util.getRightPanel()).toMatchScreenshot(`pinned-messages-list-pin-3.png`);
});

test("should unpin one message", async ({ page, app, room1, util }) => {
Expand All @@ -59,6 +60,7 @@ test.describe("Pinned messages", () => {
await util.openPinnedMessagesList();
await util.unpinMessageFromMessageList("Msg2");
await util.assertPinnedMessagesList(["Msg1", "Msg4"]);
await expect(util.getRightPanel()).toMatchScreenshot(`pinned-messages-list-unpin-2.png`);
await util.backPinnedMessagesList();
await util.assertPinnedCountInRoomInfo(2);
});
Expand Down Expand Up @@ -87,4 +89,65 @@ test.describe("Pinned messages", () => {
await util.pinMessagesFromQuickActions(["Msg1"], true);
await util.assertPinnedCountInRoomInfo(0);
});

test("should display one message in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]);
await util.assertMessageInBanner("Msg1");
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
});

test("should display 2 messages in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["Msg1", "Msg2"]);

await util.assertMessageInBanner("Msg1");
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg1.png");

await util.getBanner().click();
await util.assertMessageInBanner("Msg2");
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");

await util.getBanner().click();
await util.assertMessageInBanner("Msg1");
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg1.png");
});

test("should display 4 messages in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);

for (const msg of ["Msg1", "Msg4", "Msg3", "Msg2"]) {
await util.assertMessageInBanner(msg);
await expect(util.getBanner()).toMatchScreenshot(`pinned-message-banner-4-${msg}.png`);
await util.getBanner().click();
}
});

test("should open the pinned messages list from the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["Msg1", "Msg2"]);

await util.getViewAllButton().click();
await util.assertPinnedMessagesList(["Msg1", "Msg2"]);
await expect(util.getRightPanel()).toMatchScreenshot("pinned-message-banner-2.png");

await expect(util.getCloseListButton()).toBeVisible();
});

test("banner should listen to pinned message list", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["Msg1", "Msg2"]);

await expect(util.getViewAllButton()).toBeVisible();

await util.openRoomInfo();
await util.openPinnedMessagesList();
await expect(util.getCloseListButton()).toBeVisible();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 @@ -298,6 +298,7 @@
@import "./views/rooms/_NewRoomIntro.pcss";
@import "./views/rooms/_NotificationBadge.pcss";
@import "./views/rooms/_PinnedEventTile.pcss";
@import "./views/rooms/_PinnedMessageBanner.pcss";
@import "./views/rooms/_PresenceLabel.pcss";
@import "./views/rooms/_ReadReceiptGroup.pcss";
@import "./views/rooms/_ReplyPreview.pcss";
Expand Down
119 changes: 119 additions & 0 deletions res/css/views/rooms/_PinnedMessageBanner.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.mx_PinnedMessageBanner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--cpd-space-4x);
/* 80px = 79px + 1px from the bottom border */
height: 79px;
padding: 0 var(--cpd-space-4x);

background-color: var(--cpd-color-bg-canvas-default);
border-bottom: 1px solid var(--cpd-color-gray-400);

/* From figma */
box-shadow: 0 var(--cpd-space-2x) var(--cpd-space-6x) calc(var(--cpd-space-2x) * -1) rgba(27, 29, 34, 0.1);

.mx_PinnedMessageBanner_main {
background: transparent;
border: none;
text-align: start;
cursor: pointer;

height: 100%;
flex-grow: 1;
display: flex;
align-items: center;

.mx_PinnedMessageBanner_content {
display: grid;
grid-template:
"indicators pinIcon title" auto
"indicators pinIcon message" auto;
column-gap: var(--cpd-space-2x);
}

.mx_PinnedMessageBanner_Indicators {
grid-area: indicators;
display: flex;
flex-direction: column;
gap: var(--cpd-space-0-5x);
height: 100%;

.mx_PinnedMessageBanner_Indicator {
width: var(--cpd-space-0-5x);
background-color: var(--cpd-color-gray-600);
height: 100%;
}

.mx_PinnedMessageBanner_Indicator--active {
background-color: var(--cpd-color-icon-accent-primary);
}

.mx_PinnedMessageBanner_Indicator--hidden {
background-color: transparent;
}
}

.mx_PinnedMessageBanner_PinIcon {
grid-area: pinIcon;
align-self: center;
fill: var(--cpd-color-icon-secondary-alpha);
}

.mx_PinnedMessageBanner_title {
grid-area: title;
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-action-accent);
height: 20px;

.mx_PinnedMessageBanner_title_counter {
font: var(--cpd-font-body-sm-semibold);
}
}

.mx_PinnedMessageBanner_message {
grid-area: message;
font: var(--cpd-font-body-sm-regular);
height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.mx_PinnedMessageBanner_redactedMessage {
grid-area: message;
height: 20px;
display: flex;
align-items: center;
}
}

.mx_PinnedMessageBanner_actions {
white-space: nowrap;
}
}

.mx_PinnedMessageBanner[data-single-message="true"] {
/* 64px = 63px + 1px from the bottom border */
height: 63px;

.mx_PinnedMessageBanner_content {
grid-template: "pinIcon message" auto;
}
}
10 changes: 10 additions & 0 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoi
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";

const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
Expand Down Expand Up @@ -2409,6 +2410,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</AuxPanel>
);

const isPinningEnabled = SettingsStore.getValue<boolean>("feature_pinning");
let pinnedMessageBanner;
if (isPinningEnabled) {
pinnedMessageBanner = (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);
}

let messageComposer;
const showComposer =
// joined and not showing search results
Expand Down Expand Up @@ -2537,6 +2546,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
<Measured sensor={this.roomViewBody.current} onMeasurement={this.onMeasurement} />
)}
{auxPanel}
{pinnedMessageBanner}
<main className={timelineClasses}>
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
{topUnreadMessagesBar}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/context_menus/RoomContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import { RoomNotifState } from "../../../RoomNotifs";
import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
Expand All @@ -53,6 +52,7 @@ import { UIComponent } from "../../../settings/UIFeature";
import { DeveloperToolsOption } from "./DeveloperToolsOption";
import { tagRoom } from "../../../utils/room/tagRoom";
import { useIsVideoRoom } from "../../../utils/video-rooms";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents";

interface IProps extends IContextMenuProps {
room: Room;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import HeaderButtons, { HeaderKind } from "./HeaderButtons";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { ActionPayload } from "../../../dispatcher/payloads";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { useReadPinnedEvents, usePinnedEvents } from "./PinnedMessagesCard";
import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads";
import SettingsStore from "../../../settings/SettingsStore";
import {
Expand All @@ -40,6 +39,7 @@ import { SummarizedNotificationState } from "../../../stores/notifications/Summa
import PosthogTrackers from "../../../PosthogTrackers";
import { ButtonEvent } from "../elements/AccessibleButton";
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
import { usePinnedEvents, useReadPinnedEvents } from "../../../hooks/usePinnedEvents";

const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
Expand Down
Loading

0 comments on commit d16ab09

Please sign in to comment.