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

Commit 90e8cb2

Browse files
committed
Add reusable empty state for the right panel
Signed-off-by: Michael Telatynski <[email protected]>
1 parent 39d453a commit 90e8cb2

File tree

6 files changed

+126
-163
lines changed

6 files changed

+126
-163
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@
259259
@import "./views/polls/pollHistory/_PollHistory.pcss";
260260
@import "./views/polls/pollHistory/_PollHistoryList.pcss";
261261
@import "./views/right_panel/_BaseCard.pcss";
262+
@import "./views/right_panel/_EmptyState.pcss";
262263
@import "./views/right_panel/_EncryptionInfo.pcss";
263264
@import "./views/right_panel/_PinnedMessagesCard.pcss";
264265
@import "./views/right_panel/_RightPanelTabs.pcss";
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_EmptyState {
18+
height: 100%;
19+
box-sizing: border-box;
20+
padding: var(--cpd-space-4x);
21+
text-align: center;
22+
23+
svg {
24+
width: 56px;
25+
height: 56px;
26+
box-sizing: border-box;
27+
border-radius: 8px;
28+
padding: var(--cpd-space-3x);
29+
background-color: $panel-actions;
30+
}
31+
32+
&::before {
33+
/* Bloom using magic numbers directly out of Figma */
34+
content: "";
35+
position: absolute;
36+
z-index: -1;
37+
width: 642px;
38+
height: 775px;
39+
right: -253.77px;
40+
top: 0;
41+
background: radial-gradient(49.95% 49.95% at 50% 50%, rgba(13, 189, 139, 0.12) 0%, rgba(18, 115, 235, 0) 100%);
42+
transform: rotate(-89.69deg);
43+
overflow: hidden;
44+
}
45+
}

res/css/views/right_panel/_ThreadPanel.pcss

+8-67
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,17 @@ limitations under the License.
106106
}
107107

108108
.mx_RoomView_messagePanel {
109-
/* To avoid the rule from being applied to .mx_ThreadPanel_empty */
109+
&.mx_RoomView_messageListWrapper {
110+
position: initial;
111+
}
112+
110113
.mx_RoomView_messageListWrapper {
111114
width: calc(100% + 6px); /* 8px - 2px */
112115
}
116+
117+
.mx_RoomView_empty {
118+
display: contents;
119+
}
113120
}
114121

115122
.mx_RoomView_MessageList {
@@ -168,72 +175,6 @@ limitations under the License.
168175
mask-image: url("$(res)/img/element-icons/link.svg");
169176
}
170177

171-
.mx_ThreadPanel_empty {
172-
border-radius: 8px;
173-
background: $background;
174-
display: flex;
175-
flex-direction: column;
176-
align-items: center;
177-
justify-content: center;
178-
position: absolute;
179-
top: 0;
180-
bottom: 0;
181-
left: 0;
182-
padding: 20px;
183-
box-sizing: border-box; /* Include padding and border */
184-
width: 100%;
185-
186-
h2 {
187-
color: $primary-content;
188-
font-weight: var(--cpd-font-weight-semibold);
189-
font-size: $font-18px;
190-
margin-top: 24px;
191-
margin-bottom: 10px;
192-
}
193-
194-
p {
195-
font-size: $font-15px;
196-
color: $secondary-content;
197-
margin: 10px 0;
198-
}
199-
200-
button {
201-
border: none;
202-
background: none;
203-
color: $accent;
204-
font-size: $font-15px;
205-
206-
&:hover,
207-
&:active {
208-
text-decoration: underline;
209-
cursor: pointer;
210-
}
211-
}
212-
213-
.mx_ThreadPanel_empty_tip {
214-
font-size: $font-12px;
215-
line-height: $font-15px;
216-
217-
> b {
218-
font-weight: var(--cpd-font-weight-semibold);
219-
}
220-
}
221-
}
222-
223-
.mx_ThreadPanel_largeIcon {
224-
width: 28px;
225-
height: 28px;
226-
padding: 18px;
227-
background: $system;
228-
border-radius: 50%;
229-
230-
&::after {
231-
@mixin ThreadSummaryIcon;
232-
width: inherit;
233-
height: inherit;
234-
}
235-
}
236-
237178
.mx_ContextualMenu_wrapper {
238179
.mx_ThreadPanel_Header_FilterOptionItem {
239180
display: flex;

src/components/structures/ThreadPanel.tsx

+28-91
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
1919
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
2020
import { IconButton, Tooltip } from "@vector-im/compound-web";
2121
import { logger } from "matrix-js-sdk/src/logger";
22+
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads.svg";
2223

2324
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
2425
import BaseCard from "../views/right_panel/BaseCard";
@@ -37,6 +38,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
3738
import Spinner from "../views/elements/Spinner";
3839
import Heading from "../views/typography/Heading";
3940
import { clearRoomNotification } from "../../utils/notifications";
41+
import EmptyState from "../views/right_panel/EmptyState";
4042

4143
interface IProps {
4244
roomId: string;
@@ -73,8 +75,7 @@ export const ThreadPanelHeaderFilterOptionItem: React.FC<
7375
export const ThreadPanelHeader: React.FC<{
7476
filterOption: ThreadFilterType;
7577
setFilterOption: (filterOption: ThreadFilterType) => void;
76-
empty: boolean;
77-
}> = ({ filterOption, setFilterOption, empty }) => {
78+
}> = ({ filterOption, setFilterOption }) => {
7879
const mxClient = useMatrixClientContext();
7980
const roomContext = useRoomContext();
8081
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
@@ -140,86 +141,24 @@ export const ThreadPanelHeader: React.FC<{
140141
<Heading size="4" className="mx_BaseCard_header_title_heading">
141142
{_t("common|threads")}
142143
</Heading>
143-
{!empty && (
144-
<>
145-
<Tooltip label={_t("threads|mark_all_read")}>
146-
<IconButton
147-
onClick={onMarkAllThreadsReadClick}
148-
aria-label={_t("threads|mark_all_read")}
149-
size="24px"
150-
>
151-
<MarkAllThreadsReadIcon />
152-
</IconButton>
153-
</Tooltip>
154-
<div className="mx_ThreadPanel_vertical_separator" />
155-
<ContextMenuButton
156-
className="mx_ThreadPanel_dropdown"
157-
ref={button}
158-
isExpanded={menuDisplayed}
159-
onClick={(ev: ButtonEvent) => {
160-
openMenu();
161-
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
162-
}}
163-
>
164-
{`${_t("threads|show_thread_filter")} ${value?.label}`}
165-
</ContextMenuButton>
166-
{contextMenu}
167-
</>
168-
)}
169-
</div>
170-
);
171-
};
172-
173-
interface EmptyThreadIProps {
174-
hasThreads: boolean;
175-
filterOption: ThreadFilterType;
176-
showAllThreadsCallback: () => void;
177-
}
178-
179-
const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, showAllThreadsCallback }) => {
180-
let body: JSX.Element;
181-
if (hasThreads) {
182-
body = (
183-
<>
184-
<p>
185-
{_t("threads|empty_has_threads_tip", {
186-
replyInThread: _t("action|reply_in_thread"),
187-
})}
188-
</p>
189-
<p>
190-
{/* Always display that paragraph to prevent layout shift when hiding the button */}
191-
{filterOption === ThreadFilterType.My ? (
192-
<button onClick={showAllThreadsCallback}>{_t("threads|show_all_threads")}</button>
193-
) : (
194-
<>&nbsp;</>
195-
)}
196-
</p>
197-
</>
198-
);
199-
} else {
200-
body = (
201-
<>
202-
<p>{_t("threads|empty_explainer")}</p>
203-
<p className="mx_ThreadPanel_empty_tip">
204-
{_t(
205-
"threads|empty_tip",
206-
{
207-
replyInThread: _t("action|reply_in_thread"),
208-
},
209-
{
210-
b: (sub) => <b>{sub}</b>,
211-
},
212-
)}
213-
</p>
214-
</>
215-
);
216-
}
217-
218-
return (
219-
<div className="mx_ThreadPanel_empty">
220-
<div className="mx_ThreadPanel_largeIcon" />
221-
<h2>{_t("threads|empty_heading")}</h2>
222-
{body}
144+
<Tooltip label={_t("threads|mark_all_read")}>
145+
<IconButton onClick={onMarkAllThreadsReadClick} aria-label={_t("threads|mark_all_read")} size="24px">
146+
<MarkAllThreadsReadIcon />
147+
</IconButton>
148+
</Tooltip>
149+
<div className="mx_ThreadPanel_vertical_separator" />
150+
<ContextMenuButton
151+
className="mx_ThreadPanel_dropdown"
152+
ref={button}
153+
isExpanded={menuDisplayed}
154+
onClick={(ev: ButtonEvent) => {
155+
openMenu();
156+
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
157+
}}
158+
>
159+
{`${_t("threads|show_thread_filter")} ${value?.label}`}
160+
</ContextMenuButton>
161+
{contextMenu}
223162
</div>
224163
);
225164
};
@@ -268,11 +207,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
268207
<BaseCard
269208
hideHeaderButtons
270209
header={
271-
<ThreadPanelHeader
272-
filterOption={filterOption}
273-
setFilterOption={setFilterOption}
274-
empty={!hasThreads}
275-
/>
210+
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
276211
}
277212
id="thread-panel"
278213
className="mx_ThreadPanel"
@@ -295,10 +230,12 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
295230
timelineSet={timelineSet}
296231
showUrlPreview={false} // No URL previews at the threads list level
297232
empty={
298-
<EmptyThread
299-
hasThreads={hasThreads}
300-
filterOption={filterOption}
301-
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
233+
<EmptyState
234+
Icon={ThreadsIcon}
235+
title={_t("threads|empty_title")}
236+
description={_t("threads|empty_description", {
237+
replyInThread: _t("action|reply_in_thread"),
238+
})}
302239
/>
303240
}
304241
alwaysShowTimestamps={true}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { ComponentType } from "react";
18+
import { Text } from "@vector-im/compound-web";
19+
20+
import { Flex } from "../../utils/Flex";
21+
22+
interface Props {
23+
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
24+
title: string;
25+
description: string;
26+
}
27+
28+
const EmptyState: React.FC<Props> = ({ Icon, title, description }) => {
29+
return (
30+
<Flex className="mx_EmptyState" direction="column" gap="var(--cpd-space-4x)" align="center" justify="center">
31+
<Icon width="32px" height="32px" />
32+
<Text size="lg" weight="semibold">
33+
{title}
34+
</Text>
35+
<Text size="md" weight="regular">
36+
{description}
37+
</Text>
38+
</Flex>
39+
);
40+
};
41+
42+
export default EmptyState;

src/i18n/strings/en_EN.json

+2-5
Original file line numberDiff line numberDiff line change
@@ -3193,16 +3193,13 @@
31933193
"one": "%(count)s reply",
31943194
"other": "%(count)s replies"
31953195
},
3196-
"empty_explainer": "Threads help keep your conversations on-topic and easy to track.",
3197-
"empty_has_threads_tip": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.",
3198-
"empty_heading": "Keep discussions organised with threads",
3199-
"empty_tip": "<b>Tip:</b> Use “%(replyInThread)s” when hovering over a message.",
3196+
"empty_description": "Use “%(replyInThread)s” when hovering over a message.",
3197+
"empty_title": "Threads help keep your conversations on-topic and easy to track.",
32003198
"error_start_thread_existing_relation": "Can't create a thread from an event with an existing relation",
32013199
"mark_all_read": "Mark all as read",
32023200
"my_threads": "My threads",
32033201
"my_threads_description": "Shows all threads you've participated in",
32043202
"open_thread": "Open thread",
3205-
"show_all_threads": "Show all threads",
32063203
"show_thread_filter": "Show:"
32073204
},
32083205
"threads_activity_centre": {

0 commit comments

Comments
 (0)