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

Commit

Permalink
Mark all threads as read button (#12378)
Browse files Browse the repository at this point in the history
* Mark all threads as read button

* Wrap in TooltipProvider and update snapshots

* Remove TooltipProvider wrapper: just add it to the test

* Add some more tests

* Add test for no-room-context handler because sonarcloud

* Add playwright test

* Make assertNoTacIndicator wait

* Use dedicated useMatrixClientContext function

Co-authored-by: Florian Duros <[email protected]>

* Use dedicated useRoomContext function

Co-authored-by: Florian Duros <[email protected]>

* Compound spacing variables

Co-authored-by: Florian Duros <[email protected]>

* Compound spacing variables

Co-authored-by: Florian Duros <[email protected]>

* Imports

* Use createTestClient()

* Add function to utils

* Use mkRoom

---------

Co-authored-by: Florian Duros <[email protected]>
  • Loading branch information
dbkr and florianduros authored Mar 28, 2024
1 parent f8e210f commit 4ae94ae
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 7 deletions.
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
50 changes: 50 additions & 0 deletions test/components/structures/__snapshots__/ThreadPanel-test.tsx.snap
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

0 comments on commit 4ae94ae

Please sign in to comment.