Skip to content
This repository was archived by the owner on Sep 11, 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
15 changes: 13 additions & 2 deletions playwright/e2e/spaces/threads-activity-centre/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,12 @@ export class Helpers {
/**
* Assert that the threads activity centre button has no indicator
*/
assertNoTacIndicator() {
return expect(this.getTacButton()).toMatchScreenshot("tac-no-indicator.png");
async assertNoTacIndicator() {
// Assert by checkng neither of the known indicators are visible first. This will wait
// if it takes a little time to disappear, but the screenshot comparison won't.
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
await expect(this.getTacButton()).toMatchScreenshot("tac-no-indicator.png");
}

/**
Expand Down Expand Up @@ -375,6 +379,13 @@ export class Helpers {
expandSpacePanel() {
return this.page.getByRole("button", { name: "Expand" }).click();
}

/**
* Clicks the button to mark all threads as read in the current room
*/
clickMarkAllThreadsRead() {
return this.page.getByLabel("Mark all as read").click();
}
}

export { expect };
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,17 @@ test.describe("Threads Activity Centre", () => {
await util.hoverTacButton();
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
});

test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => {
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);

await util.assertNotificationTac();

await util.openTac();
await util.clickRoomInTac(room1.name);

util.clickMarkAllThreadsRead();

await util.assertNoTacIndicator();
});
});
13 changes: 12 additions & 1 deletion res/css/views/right_panel/_ThreadPanel.pcss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021,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.
Expand All @@ -20,11 +20,22 @@ limitations under the License.

.mx_BaseCard_header {
.mx_BaseCard_header_title {
.mx_BaseCard_header_title_heading {
margin-right: auto;
}

.mx_AccessibleButton {
font-size: 12px;
color: $secondary-content;
}

.mx_ThreadPanel_vertical_separator {
height: 16px;
margin-left: var(--cpd-space-3x);
margin-right: var(--cpd-space-1x);
border-left: 1px solid var(--cpd-color-gray-400);
}

.mx_ThreadPanel_dropdown {
padding: 3px $spacing-4 3px $spacing-8;
border-radius: 4px;
Expand Down
6 changes: 6 additions & 0 deletions res/img/element-icons/check-all.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 34 additions & 2 deletions src/components/structures/ThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ limitations under the License.
import { Optional } from "matrix-events-sdk";
import React, { useContext, useEffect, useRef, useState } from "react";
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
import { IconButton, Tooltip } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";

import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
import BaseCard from "../views/right_panel/BaseCard";
import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext";
import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext";
import TimelinePanel from "./TimelinePanel";
import { Layout } from "../../settings/enums/Layout";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
Expand All @@ -33,6 +36,7 @@ import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner";
import Heading from "../views/typography/Heading";
import { clearRoomNotification } from "../../utils/notifications";

interface IProps {
roomId: string;
Expand Down Expand Up @@ -71,6 +75,8 @@ export const ThreadPanelHeader: React.FC<{
setFilterOption: (filterOption: ThreadFilterType) => void;
empty: boolean;
}> = ({ filterOption, setFilterOption, empty }) => {
const mxClient = useMatrixClientContext();
const roomContext = useRoomContext();
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [
{
Expand Down Expand Up @@ -109,13 +115,39 @@ export const ThreadPanelHeader: React.FC<{
{contextMenuOptions}
</ContextMenu>
) : null;

const onMarkAllThreadsReadClick = React.useCallback(() => {
if (!roomContext.room) {
logger.error("No room in context to mark all threads read");
return;
}
// This actually clears all room notifications by sending an unthreaded read receipt.
// We'd have to loop over all unread threads (pagninating back to find any we don't
// know about yet) and send threaded receipts for all of them... or implement a
// specific API for it. In practice, the user will have to be viewing the room to
// see this button, so will have marked the room itself read anyway.
clearRoomNotification(roomContext.room, mxClient).catch((e) => {
logger.error("Failed to mark all threads read", e);
});
}, [roomContext.room, mxClient]);

return (
<div className="mx_BaseCard_header_title">
<Heading size="4" className="mx_BaseCard_header_title_heading">
{_t("common|threads")}
</Heading>
{!empty && (
<>
<Tooltip label={_t("threads|mark_all_read")}>
<IconButton
onClick={onMarkAllThreadsReadClick}
aria-label={_t("threads|mark_all_read")}
size="24px"
>
<MarkAllThreadsReadIcon />
</IconButton>
</Tooltip>
<div className="mx_ThreadPanel_vertical_separator" />
<ContextMenuButton
className="mx_ThreadPanel_dropdown"
ref={button}
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 @@ -3151,6 +3151,7 @@
"empty_heading": "Keep discussions organised with threads",
"empty_tip": "<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.",
"error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation",
"mark_all_read": "Mark all as read",
"my_threads": "My threads",
"my_threads_description": "Shows all threads you've participated in",
"open_thread": "Open thread",
Expand Down
62 changes: 60 additions & 2 deletions test/components/structures/ThreadPanel-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, waitFor, getByRole } from "@testing-library/react";
import { mocked } from "jest-mock";
import {
MatrixClient,
Expand All @@ -34,8 +34,9 @@ import { _t } from "../../../src/languageHandler";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { getRoomContext, mockPlatformPeg, stubClient } from "../../test-utils";
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils";
import { mkThread } from "../../test-utils/threads";
import { IRoomState } from "../../../src/components/structures/RoomView";

jest.mock("../../../src/utils/Feedback");

Expand All @@ -48,6 +49,7 @@ describe("ThreadPanel", () => {
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
{ wrapper: TooltipProvider },
);
expect(asFragment()).toMatchSnapshot();
});
Expand All @@ -64,6 +66,18 @@ describe("ThreadPanel", () => {
expect(asFragment()).toMatchSnapshot();
});

it("matches snapshot when no threads", () => {
const { asFragment } = render(
<ThreadPanelHeader
empty={true}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>,
{ wrapper: TooltipProvider },
);
expect(asFragment()).toMatchSnapshot();
});

it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => {
const { container } = render(
<ThreadPanelHeader
Expand Down Expand Up @@ -98,6 +112,50 @@ describe("ThreadPanel", () => {
);
expect(foundButton).toMatchSnapshot();
});

it("sends an unthreaded read receipt when the Mark All Threads Read button is clicked", async () => {
const mockClient = createTestClient();
const mockEvent = {} as MatrixEvent;
const mockRoom = mkRoom(mockClient, "!roomId:example.org");
mockRoom.getLastLiveEvent.mockReturnValue(mockEvent);
const roomContextObject = {
room: mockRoom,
} as unknown as IRoomState;
const { container } = render(
<RoomContext.Provider value={roomContextObject}>
<MatrixClientContext.Provider value={mockClient}>
<TooltipProvider>
<ThreadPanelHeader
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>
</TooltipProvider>
</MatrixClientContext.Provider>
</RoomContext.Provider>,
);
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
await waitFor(() =>
expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(mockEvent, expect.anything(), true),
);
});

it("doesn't send a receipt if no room is in context", async () => {
const mockClient = createTestClient();
const { container } = render(
<MatrixClientContext.Provider value={mockClient}>
<TooltipProvider>
<ThreadPanelHeader
empty={false}
filterOption={ThreadFilterType.All}
setFilterOption={() => undefined}
/>
</TooltipProvider>
</MatrixClientContext.Provider>,
);
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled());
});
});

describe("Filtering", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ exports[`ThreadPanel Header expect that All filter for ThreadPanelHeader properl
>
Threads
</h4>
<button
aria-label="Mark all as read"
class="_icon-button_16nk7_17"
data-state="closed"
role="button"
style="--cpd-icon-button-size: 24px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<div />
</div>
</button>
<div
class="mx_ThreadPanel_vertical_separator"
/>
<div
aria-expanded="false"
aria-haspopup="true"
Expand All @@ -33,6 +51,24 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
>
Threads
</h4>
<button
aria-label="Mark all as read"
class="_icon-button_16nk7_17"
data-state="closed"
role="button"
style="--cpd-icon-button-size: 24px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<div />
</div>
</button>
<div
class="mx_ThreadPanel_vertical_separator"
/>
<div
aria-expanded="false"
aria-haspopup="true"
Expand Down Expand Up @@ -61,3 +97,17 @@ exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option
</span>
</div>
`;

exports[`ThreadPanel Header matches snapshot when no threads 1`] = `
<DocumentFragment>
<div
class="mx_BaseCard_header_title"
>
<h4
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Threads
</h4>
</div>
</DocumentFragment>
`;
1 change: 1 addition & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ export function mkStubRoom(
getJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinedMembers: jest.fn().mockReturnValue([]),
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
getLastLiveEvent: jest.fn().mockReturnValue(undefined),
getMember: jest.fn().mockReturnValue({
userId: "@member:domain.bla",
name: "Member",
Expand Down