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
|
@ -28,6 +28,7 @@ import {
|
|||
TimelineWindow,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
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 EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
|
@ -40,6 +41,7 @@ import Spinner from "../views/elements/Spinner";
|
|||
import { Layout } from "../../settings/enums/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import Measured from "../views/elements/Measured";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -255,10 +257,11 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
|
||||
const emptyState = (
|
||||
<div className="mx_RightPanel_empty mx_FilePanel_empty">
|
||||
<h2>{_t("file_panel|empty_heading")}</h2>
|
||||
<p>{_t("file_panel|empty_description")}</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
Icon={FilesIcon}
|
||||
title={_t("file_panel|empty_heading")}
|
||||
description={_t("file_panel|empty_description")}
|
||||
/>
|
||||
);
|
||||
|
||||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
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 { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
@ -26,6 +27,7 @@ import { Layout } from "../../settings/enums/Layout";
|
|||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import Measured from "../views/elements/Measured";
|
||||
import Heading from "../views/typography/Heading";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -57,10 +59,11 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
|||
|
||||
public render(): React.ReactNode {
|
||||
const emptyState = (
|
||||
<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t("notif_panel|empty_heading")}</h2>
|
||||
<p>{_t("notif_panel|empty_description")}</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
Icon={NotificationsIcon}
|
||||
title={_t("notif_panel|empty_heading")}
|
||||
description={_t("notif_panel|empty_description")}
|
||||
/>
|
||||
);
|
||||
|
||||
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 { IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
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 BaseCard from "../views/right_panel/BaseCard";
|
||||
|
@ -37,6 +38,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import Heading from "../views/typography/Heading";
|
||||
import { clearRoomNotification } from "../../utils/notifications";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -73,8 +75,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC<
|
|||
export const ThreadPanelHeader: React.FC<{
|
||||
filterOption: ThreadFilterType;
|
||||
setFilterOption: (filterOption: ThreadFilterType) => void;
|
||||
empty: boolean;
|
||||
}> = ({ filterOption, setFilterOption, empty }) => {
|
||||
}> = ({ filterOption, setFilterOption }) => {
|
||||
const mxClient = useMatrixClientContext();
|
||||
const roomContext = useRoomContext();
|
||||
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">
|
||||
{_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}
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
openMenu();
|
||||
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}
|
||||
<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}
|
||||
isExpanded={menuDisplayed}
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
openMenu();
|
||||
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
|
||||
}}
|
||||
>
|
||||
{`${_t("threads|show_thread_filter")} ${value?.label}`}
|
||||
</ContextMenuButton>
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -268,11 +207,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
<BaseCard
|
||||
hideHeaderButtons
|
||||
header={
|
||||
<ThreadPanelHeader
|
||||
filterOption={filterOption}
|
||||
setFilterOption={setFilterOption}
|
||||
empty={!hasThreads}
|
||||
/>
|
||||
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
|
||||
}
|
||||
id="thread-panel"
|
||||
className="mx_ThreadPanel"
|
||||
|
@ -295,10 +230,12 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
timelineSet={timelineSet}
|
||||
showUrlPreview={false} // No URL previews at the threads list level
|
||||
empty={
|
||||
<EmptyThread
|
||||
hasThreads={hasThreads}
|
||||
filterOption={filterOption}
|
||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
||||
<EmptyState
|
||||
Icon={ThreadsIcon}
|
||||
title={_t("threads|empty_title")}
|
||||
description={_t("threads|empty_description", {
|
||||
replyInThread: _t("action|reply_in_thread"),
|
||||
})}
|
||||
/>
|
||||
}
|
||||
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",
|
||||
"other": "%(count)s replies"
|
||||
},
|
||||
"empty_explainer": "Threads help keep your conversations on-topic and easy to track.",
|
||||
"empty_has_threads_tip": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.",
|
||||
"empty_heading": "Keep discussions organised with threads",
|
||||
"empty_tip": "<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.",
|
||||
"empty_description": "Use “%(replyInThread)s” when hovering over a message.",
|
||||
"empty_title": "Threads help keep your conversations on-topic and easy to track.",
|
||||
"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",
|
||||
"show_all_threads": "Show all threads",
|
||||
"show_thread_filter": "Show:"
|
||||
},
|
||||
"threads_activity_centre": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue