Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Display pinned messages on a banner at the top of a room #12917

Merged
merged 13 commits into from
Aug 29, 2024
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
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
Loading