mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 01:05:42 +03:00
Mark all threads as read button (#12378)
* 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 <florianduros@element.io> * Use dedicated useRoomContext function Co-authored-by: Florian Duros <florianduros@element.io> * Compound spacing variables Co-authored-by: Florian Duros <florianduros@element.io> * Compound spacing variables Co-authored-by: Florian Duros <florianduros@element.io> * Imports * Use createTestClient() * Add function to utils * Use mkRoom --------- Co-authored-by: Florian Duros <florianduros@element.io>
This commit is contained in:
parent
f8e210f1a0
commit
4ae94ae247
9 changed files with 190 additions and 7 deletions
|
@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
@ -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;
|
||||
|
|
6
res/img/element-icons/check-all.svg
Normal file
6
res/img/element-icons/check-all.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.75 2H14.25" stroke="#656D77" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M1.75 6H14.25" stroke="#656D77" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M1.75 10H8.25" stroke="#656D77" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M6.17188 13L8.2932 15.1213L13.95 9.46447" stroke="#656D77" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 494 B |
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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[] = [
|
||||
{
|
||||
|
@ -109,6 +115,22 @@ 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">
|
||||
|
@ -116,6 +138,16 @@ export const ThreadPanelHeader: React.FC<{
|
|||
</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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
@ -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");
|
||||
|
||||
|
@ -48,6 +49,7 @@ describe("ThreadPanel", () => {
|
|||
filterOption={ThreadFilterType.All}
|
||||
setFilterOption={() => undefined}
|
||||
/>,
|
||||
{ wrapper: TooltipProvider },
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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", () => {
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue