mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 10:45:51 +03:00
Iterate design of right panel empty state (#12796)
* Add reusable empty state for the right panel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
d202295015
commit
0fc1c53a8e
21 changed files with 266 additions and 280 deletions
|
@ -50,7 +50,7 @@ test.describe("FilePanel", () => {
|
||||||
test.describe("render", () => {
|
test.describe("render", () => {
|
||||||
test("should render empty state", async ({ page }) => {
|
test("should render empty state", async ({ page }) => {
|
||||||
// Wait until the information about the empty state is rendered
|
// Wait until the information about the empty state is rendered
|
||||||
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible();
|
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
|
// Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
||||||
|
|
|
@ -35,7 +35,7 @@ test.describe("NotificationPanel", () => {
|
||||||
await page.getByRole("button", { name: "Notifications" }).click();
|
await page.getByRole("button", { name: "Notifications" }).click();
|
||||||
|
|
||||||
// Wait until the information about the empty state is rendered
|
// Wait until the information about the empty state is rendered
|
||||||
await expect(page.locator(".mx_NotificationPanel_empty")).toBeVisible();
|
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of RightPanel
|
// Take a snapshot of RightPanel
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
||||||
|
|
|
@ -104,7 +104,7 @@ test.describe("RightPanel", () => {
|
||||||
|
|
||||||
await page.getByRole("menuitem", { name: "Files" }).click();
|
await page.getByRole("menuitem", { name: "Files" }).click();
|
||||||
await expect(page.locator(".mx_FilePanel")).toBeVisible();
|
await expect(page.locator(".mx_FilePanel")).toBeVisible();
|
||||||
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible();
|
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId("base-card-back-button").click();
|
await page.getByTestId("base-card-back-button").click();
|
||||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 71 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 70 KiB |
|
@ -73,7 +73,6 @@
|
||||||
@import "./structures/_MatrixChat.pcss";
|
@import "./structures/_MatrixChat.pcss";
|
||||||
@import "./structures/_MessagePanel.pcss";
|
@import "./structures/_MessagePanel.pcss";
|
||||||
@import "./structures/_NonUrgentToastContainer.pcss";
|
@import "./structures/_NonUrgentToastContainer.pcss";
|
||||||
@import "./structures/_NotificationPanel.pcss";
|
|
||||||
@import "./structures/_QuickSettingsButton.pcss";
|
@import "./structures/_QuickSettingsButton.pcss";
|
||||||
@import "./structures/_RightPanel.pcss";
|
@import "./structures/_RightPanel.pcss";
|
||||||
@import "./structures/_RoomSearch.pcss";
|
@import "./structures/_RoomSearch.pcss";
|
||||||
|
@ -259,6 +258,7 @@
|
||||||
@import "./views/polls/pollHistory/_PollHistory.pcss";
|
@import "./views/polls/pollHistory/_PollHistory.pcss";
|
||||||
@import "./views/polls/pollHistory/_PollHistoryList.pcss";
|
@import "./views/polls/pollHistory/_PollHistoryList.pcss";
|
||||||
@import "./views/right_panel/_BaseCard.pcss";
|
@import "./views/right_panel/_BaseCard.pcss";
|
||||||
|
@import "./views/right_panel/_EmptyState.pcss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.pcss";
|
@import "./views/right_panel/_EncryptionInfo.pcss";
|
||||||
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
||||||
@import "./views/right_panel/_RightPanelTabs.pcss";
|
@import "./views/right_panel/_RightPanelTabs.pcss";
|
||||||
|
|
|
@ -102,7 +102,3 @@ limitations under the License.
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel_empty::before {
|
|
||||||
--maskImage: url("$(res)/img/element-icons/room/files.svg"); /* See: _RightPanel.pcss */
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
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_NotificationPanel_empty::before {
|
|
||||||
--maskImage: url("$(res)/img/element-icons/notifications.svg"); /* See: _RightPanel.pcss */
|
|
||||||
}
|
|
|
@ -72,30 +72,3 @@ limitations under the License.
|
||||||
order: 2;
|
order: 2;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RightPanel_empty {
|
|
||||||
margin-right: -28px;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
p {
|
|
||||||
font: var(--cpd-font-body-md-regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
margin: 11px auto 29px auto;
|
|
||||||
height: 42px;
|
|
||||||
width: 42px;
|
|
||||||
background-color: $header-panel-text-primary-color;
|
|
||||||
mask-image: var(--maskImage);
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
45
res/css/views/right_panel/_EmptyState.pcss
Normal file
45
res/css/views/right_panel/_EmptyState.pcss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
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_EmptyState {
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--cpd-space-4x);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--cpd-space-3x);
|
||||||
|
background-color: $panel-actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
/* Bloom using magic numbers directly out of Figma */
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 642px;
|
||||||
|
height: 775px;
|
||||||
|
right: -253.77px;
|
||||||
|
top: 0;
|
||||||
|
background: radial-gradient(49.95% 49.95% at 50% 50%, rgba(13, 189, 139, 0.12) 0%, rgba(18, 115, 235, 0) 100%);
|
||||||
|
transform: rotate(-89.69deg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
|
@ -106,10 +106,17 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_messagePanel {
|
.mx_RoomView_messagePanel {
|
||||||
/* To avoid the rule from being applied to .mx_ThreadPanel_empty */
|
&.mx_RoomView_messageListWrapper {
|
||||||
|
position: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomView_messageListWrapper {
|
.mx_RoomView_messageListWrapper {
|
||||||
width: calc(100% + 6px); /* 8px - 2px */
|
width: calc(100% + 6px); /* 8px - 2px */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomView_empty {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
.mx_RoomView_MessageList {
|
||||||
|
@ -168,72 +175,6 @@ limitations under the License.
|
||||||
mask-image: url("$(res)/img/element-icons/link.svg");
|
mask-image: url("$(res)/img/element-icons/link.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ThreadPanel_empty {
|
|
||||||
border-radius: 8px;
|
|
||||||
background: $background;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box; /* Include padding and border */
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: $primary-content;
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
font-size: $font-18px;
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: $font-15px;
|
|
||||||
color: $secondary-content;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: $accent;
|
|
||||||
font-size: $font-15px;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ThreadPanel_empty_tip {
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
|
|
||||||
> b {
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ThreadPanel_largeIcon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 18px;
|
|
||||||
background: $system;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
@mixin ThreadSummaryIcon;
|
|
||||||
width: inherit;
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ContextualMenu_wrapper {
|
.mx_ContextualMenu_wrapper {
|
||||||
.mx_ThreadPanel_Header_FilterOptionItem {
|
.mx_ThreadPanel_Header_FilterOptionItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
TimelineWindow,
|
TimelineWindow,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||||
|
@ -40,6 +41,7 @@ import Spinner from "../views/elements/Spinner";
|
||||||
import { Layout } from "../../settings/enums/Layout";
|
import { Layout } from "../../settings/enums/Layout";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import Measured from "../views/elements/Measured";
|
import Measured from "../views/elements/Measured";
|
||||||
|
import EmptyState from "../views/right_panel/EmptyState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -255,10 +257,11 @@ class FilePanel extends React.Component<IProps, IState> {
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
|
|
||||||
const emptyState = (
|
const emptyState = (
|
||||||
<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
<EmptyState
|
||||||
<h2>{_t("file_panel|empty_heading")}</h2>
|
Icon={FilesIcon}
|
||||||
<p>{_t("file_panel|empty_description")}</p>
|
title={_t("file_panel|empty_heading")}
|
||||||
</div>
|
description={_t("file_panel|empty_description")}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);
|
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications.svg";
|
||||||
|
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
@ -26,6 +27,7 @@ import { Layout } from "../../settings/enums/Layout";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import Measured from "../views/elements/Measured";
|
import Measured from "../views/elements/Measured";
|
||||||
import Heading from "../views/typography/Heading";
|
import Heading from "../views/typography/Heading";
|
||||||
|
import EmptyState from "../views/right_panel/EmptyState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
@ -57,10 +59,11 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const emptyState = (
|
const emptyState = (
|
||||||
<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
<EmptyState
|
||||||
<h2>{_t("notif_panel|empty_heading")}</h2>
|
Icon={NotificationsIcon}
|
||||||
<p>{_t("notif_panel|empty_description")}</p>
|
title={_t("notif_panel|empty_heading")}
|
||||||
</div>
|
description={_t("notif_panel|empty_description")}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
|
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
|
||||||
import { IconButton, Tooltip } from "@vector-im/compound-web";
|
import { IconButton, Tooltip } from "@vector-im/compound-web";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads.svg";
|
||||||
|
|
||||||
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
|
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
|
@ -37,6 +38,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import Heading from "../views/typography/Heading";
|
import Heading from "../views/typography/Heading";
|
||||||
import { clearRoomNotification } from "../../utils/notifications";
|
import { clearRoomNotification } from "../../utils/notifications";
|
||||||
|
import EmptyState from "../views/right_panel/EmptyState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -73,8 +75,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC<
|
||||||
export const ThreadPanelHeader: React.FC<{
|
export const ThreadPanelHeader: React.FC<{
|
||||||
filterOption: ThreadFilterType;
|
filterOption: ThreadFilterType;
|
||||||
setFilterOption: (filterOption: ThreadFilterType) => void;
|
setFilterOption: (filterOption: ThreadFilterType) => void;
|
||||||
empty: boolean;
|
}> = ({ filterOption, setFilterOption }) => {
|
||||||
}> = ({ filterOption, setFilterOption, empty }) => {
|
|
||||||
const mxClient = useMatrixClientContext();
|
const mxClient = useMatrixClientContext();
|
||||||
const roomContext = useRoomContext();
|
const roomContext = useRoomContext();
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||||
|
@ -140,86 +141,24 @@ export const ThreadPanelHeader: React.FC<{
|
||||||
<Heading size="4" className="mx_BaseCard_header_title_heading">
|
<Heading size="4" className="mx_BaseCard_header_title_heading">
|
||||||
{_t("common|threads")}
|
{_t("common|threads")}
|
||||||
</Heading>
|
</Heading>
|
||||||
{!empty && (
|
<Tooltip label={_t("threads|mark_all_read")}>
|
||||||
<>
|
<IconButton onClick={onMarkAllThreadsReadClick} aria-label={_t("threads|mark_all_read")} size="24px">
|
||||||
<Tooltip label={_t("threads|mark_all_read")}>
|
<MarkAllThreadsReadIcon />
|
||||||
<IconButton
|
</IconButton>
|
||||||
onClick={onMarkAllThreadsReadClick}
|
</Tooltip>
|
||||||
aria-label={_t("threads|mark_all_read")}
|
<div className="mx_ThreadPanel_vertical_separator" />
|
||||||
size="24px"
|
<ContextMenuButton
|
||||||
>
|
className="mx_ThreadPanel_dropdown"
|
||||||
<MarkAllThreadsReadIcon />
|
ref={button}
|
||||||
</IconButton>
|
isExpanded={menuDisplayed}
|
||||||
</Tooltip>
|
onClick={(ev: ButtonEvent) => {
|
||||||
<div className="mx_ThreadPanel_vertical_separator" />
|
openMenu();
|
||||||
<ContextMenuButton
|
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
|
||||||
className="mx_ThreadPanel_dropdown"
|
}}
|
||||||
ref={button}
|
>
|
||||||
isExpanded={menuDisplayed}
|
{`${_t("threads|show_thread_filter")} ${value?.label}`}
|
||||||
onClick={(ev: ButtonEvent) => {
|
</ContextMenuButton>
|
||||||
openMenu();
|
{contextMenu}
|
||||||
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${_t("threads|show_thread_filter")} ${value?.label}`}
|
|
||||||
</ContextMenuButton>
|
|
||||||
{contextMenu}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EmptyThreadIProps {
|
|
||||||
hasThreads: boolean;
|
|
||||||
filterOption: ThreadFilterType;
|
|
||||||
showAllThreadsCallback: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, showAllThreadsCallback }) => {
|
|
||||||
let body: JSX.Element;
|
|
||||||
if (hasThreads) {
|
|
||||||
body = (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
{_t("threads|empty_has_threads_tip", {
|
|
||||||
replyInThread: _t("action|reply_in_thread"),
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{/* Always display that paragraph to prevent layout shift when hiding the button */}
|
|
||||||
{filterOption === ThreadFilterType.My ? (
|
|
||||||
<button onClick={showAllThreadsCallback}>{_t("threads|show_all_threads")}</button>
|
|
||||||
) : (
|
|
||||||
<> </>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
body = (
|
|
||||||
<>
|
|
||||||
<p>{_t("threads|empty_explainer")}</p>
|
|
||||||
<p className="mx_ThreadPanel_empty_tip">
|
|
||||||
{_t(
|
|
||||||
"threads|empty_tip",
|
|
||||||
{
|
|
||||||
replyInThread: _t("action|reply_in_thread"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
b: (sub) => <b>{sub}</b>,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_ThreadPanel_empty">
|
|
||||||
<div className="mx_ThreadPanel_largeIcon" />
|
|
||||||
<h2>{_t("threads|empty_heading")}</h2>
|
|
||||||
{body}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -268,11 +207,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
hideHeaderButtons
|
hideHeaderButtons
|
||||||
header={
|
header={
|
||||||
<ThreadPanelHeader
|
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
|
||||||
filterOption={filterOption}
|
|
||||||
setFilterOption={setFilterOption}
|
|
||||||
empty={!hasThreads}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
id="thread-panel"
|
id="thread-panel"
|
||||||
className="mx_ThreadPanel"
|
className="mx_ThreadPanel"
|
||||||
|
@ -295,10 +230,12 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
timelineSet={timelineSet}
|
timelineSet={timelineSet}
|
||||||
showUrlPreview={false} // No URL previews at the threads list level
|
showUrlPreview={false} // No URL previews at the threads list level
|
||||||
empty={
|
empty={
|
||||||
<EmptyThread
|
<EmptyState
|
||||||
hasThreads={hasThreads}
|
Icon={ThreadsIcon}
|
||||||
filterOption={filterOption}
|
title={_t("threads|empty_title")}
|
||||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
description={_t("threads|empty_description", {
|
||||||
|
replyInThread: _t("action|reply_in_thread"),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
alwaysShowTimestamps={true}
|
alwaysShowTimestamps={true}
|
||||||
|
|
42
src/components/views/right_panel/EmptyState.tsx
Normal file
42
src/components/views/right_panel/EmptyState.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentType } from "react";
|
||||||
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { Flex } from "../../utils/Flex";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyState: React.FC<Props> = ({ Icon, title, description }) => {
|
||||||
|
return (
|
||||||
|
<Flex className="mx_EmptyState" direction="column" gap="var(--cpd-space-4x)" align="center" justify="center">
|
||||||
|
<Icon width="32px" height="32px" />
|
||||||
|
<Text size="lg" weight="semibold">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="md" weight="regular">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyState;
|
|
@ -3193,16 +3193,13 @@
|
||||||
"one": "%(count)s reply",
|
"one": "%(count)s reply",
|
||||||
"other": "%(count)s replies"
|
"other": "%(count)s replies"
|
||||||
},
|
},
|
||||||
"empty_explainer": "Threads help keep your conversations on-topic and easy to track.",
|
"empty_description": "Use “%(replyInThread)s” when hovering over a message.",
|
||||||
"empty_has_threads_tip": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.",
|
"empty_title": "Threads help keep your conversations on-topic and easy to track.",
|
||||||
"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",
|
"error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation",
|
||||||
"mark_all_read": "Mark all as read",
|
"mark_all_read": "Mark all as read",
|
||||||
"my_threads": "My threads",
|
"my_threads": "My threads",
|
||||||
"my_threads_description": "Shows all threads you've participated in",
|
"my_threads_description": "Shows all threads you've participated in",
|
||||||
"open_thread": "Open thread",
|
"open_thread": "Open thread",
|
||||||
"show_all_threads": "Show all threads",
|
|
||||||
"show_thread_filter": "Show:"
|
"show_thread_filter": "Show:"
|
||||||
},
|
},
|
||||||
"threads_activity_centre": {
|
"threads_activity_centre": {
|
||||||
|
|
58
test/components/structures/FilePanel-test.tsx
Normal file
58
test/components/structures/FilePanel-test.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { screen, render, waitFor } from "@testing-library/react";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import FilePanel from "../../../src/components/structures/FilePanel";
|
||||||
|
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||||
|
import { stubClient } from "../../test-utils";
|
||||||
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
|
jest.mock("matrix-js-sdk/src/matrix", () => ({
|
||||||
|
...jest.requireActual("matrix-js-sdk/src/matrix"),
|
||||||
|
TimelineWindow: jest.fn().mockReturnValue({
|
||||||
|
load: jest.fn().mockResolvedValue(null),
|
||||||
|
getEvents: jest.fn().mockReturnValue([]),
|
||||||
|
canPaginate: jest.fn().mockReturnValue(false),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("FilePanel", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders empty state", async () => {
|
||||||
|
const cli = MatrixClientPeg.safeGet();
|
||||||
|
const room = new Room("!room:server", cli, cli.getSafeUserId(), {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
const timelineSet = new EventTimelineSet(room);
|
||||||
|
room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
|
||||||
|
mocked(cli.getRoom).mockReturnValue(room);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<FilePanel roomId={room.roomId} onClose={jest.fn()} resizeNotifier={new ResizeNotifier()} />,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("No files visible in this room")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -43,44 +43,21 @@ describe("ThreadPanel", () => {
|
||||||
describe("Header", () => {
|
describe("Header", () => {
|
||||||
it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => {
|
it("expect that All filter for ThreadPanelHeader properly renders Show: All threads", () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => {
|
it("expect that My filter for ThreadPanelHeader properly renders Show: My threads", () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.My} setFilterOption={() => undefined} />,
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.My}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches snapshot when no threads", () => {
|
|
||||||
const { asFragment } = render(
|
|
||||||
<ThreadPanelHeader
|
|
||||||
empty={true}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => {
|
it("expect that ThreadPanelHeader properly opens a context menu when clicked on the button", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
const found = container.querySelector(".mx_ThreadPanel_dropdown");
|
const found = container.querySelector(".mx_ThreadPanel_dropdown");
|
||||||
expect(found).toBeTruthy();
|
expect(found).toBeTruthy();
|
||||||
|
@ -91,11 +68,7 @@ describe("ThreadPanel", () => {
|
||||||
|
|
||||||
it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => {
|
it("expect that ThreadPanelHeader has the correct option selected in the context menu", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />,
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
|
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
|
||||||
const found = screen.queryAllByRole("menuitemradio");
|
const found = screen.queryAllByRole("menuitemradio");
|
||||||
|
@ -118,11 +91,7 @@ describe("ThreadPanel", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<RoomContext.Provider value={roomContextObject}>
|
<RoomContext.Provider value={roomContextObject}>
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>
|
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
</RoomContext.Provider>,
|
</RoomContext.Provider>,
|
||||||
);
|
);
|
||||||
|
@ -136,11 +105,7 @@ describe("ThreadPanel", () => {
|
||||||
const mockClient = createTestClient();
|
const mockClient = createTestClient();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
|
||||||
empty={false}
|
|
||||||
filterOption={ThreadFilterType.All}
|
|
||||||
setFilterOption={() => undefined}
|
|
||||||
/>
|
|
||||||
</MatrixClientContext.Provider>,
|
</MatrixClientContext.Provider>,
|
||||||
);
|
);
|
||||||
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`FilePanel renders empty state 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard mx_FilePanel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_header_spacer"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||||
|
data-testid="base-card-close-button"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 28px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_messagePanel mx_RoomView_messageListWrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_RoomView_empty"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Flex mx_EmptyState"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
height="32px"
|
||||||
|
width="32px"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83"
|
||||||
|
>
|
||||||
|
No files visible in this room
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59"
|
||||||
|
>
|
||||||
|
Attach files from chat or just drag and drop them anywhere in a room.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
|
@ -95,17 +95,3 @@ exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
|
||||||
`;
|
|
||||||
|
|
|
@ -274,6 +274,7 @@ export function createTestClient(): MatrixClient {
|
||||||
matrixRTC: createStubMatrixRTC(),
|
matrixRTC: createStubMatrixRTC(),
|
||||||
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
|
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
|
||||||
getAuthIssuer: jest.fn(),
|
getAuthIssuer: jest.fn(),
|
||||||
|
getOrCreateFilter: jest.fn(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
|
Loading…
Reference in a new issue