Add tabs to the right panel (#12672)
* Create new method for header button behaviour With the introduction of tabs, the behaviour of the header buttons is changed as follows: - Close any right panel if open - Open the correct right panel if no panel was open before The old method (and behaviour) is retained as showOrHidePhase. * Implement tabs in the right panel There are three tabs: Info, People and Threads * Remove unwanted code from RoomSummaryCard - Remove the menu item for opening the memberlist since that is now taken of by the tabs. - Remove the close button * Remove code for focusing close button from tac item See https://github.com/matrix-org/matrix-react-sdk/pull/12410 There's no longer a close button to focus so we instead focus the thread tab. This is done in RightPaneltabs.tsx so we just need to remove this code. * Introduce a room info icon to the header This was previously present in the legacy room header but not in the new header. * BaseCard changes - Adds id, ariaLabelledBy and role props to implement tab accessibility. - Adds hideHeaderButtons prop to hide header buttons (think back and close buttons). - Change confusing header rendering code: header is not rendered ONLY when no header is passed AND hideHeaderButtons is true. * Refactor repeated code into function Created a new function createSpaceScopeHeader which returns the component if the room is a space room. Previously this code was duplicated in every component that uses SpaceScopeHeader component. * Pass BaseCard attributes and use helper function Actually using the code from the last two commits * Add, update and remove tests/screenshots/snapshots * Fix distance between search bar and tabs * Update compound * Update screenshots/snapshots
|
@ -78,7 +78,7 @@
|
||||||
"@sentry/browser": "^8.0.0",
|
"@sentry/browser": "^8.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@vector-im/compound-design-tokens": "^1.2.0",
|
"@vector-im/compound-design-tokens": "^1.2.0",
|
||||||
"@vector-im/compound-web": "^5.1.2",
|
"@vector-im/compound-web": "^5.2.3",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
|
|
|
@ -103,7 +103,7 @@ const verify = async (page: Page, bob: Bot) => {
|
||||||
const bobsVerificationRequestPromise = waitForVerificationRequest(bob);
|
const bobsVerificationRequestPromise = waitForVerificationRequest(bob);
|
||||||
|
|
||||||
const roomInfo = await openRoomInfo(page);
|
const roomInfo = await openRoomInfo(page);
|
||||||
await roomInfo.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await roomInfo.getByText("Bob").click();
|
await roomInfo.getByText("Bob").click();
|
||||||
await roomInfo.getByRole("button", { name: "Verify" }).click();
|
await roomInfo.getByRole("button", { name: "Verify" }).click();
|
||||||
await roomInfo.getByRole("button", { name: "Start Verification" }).click();
|
await roomInfo.getByRole("button", { name: "Start Verification" }).click();
|
||||||
|
@ -279,7 +279,7 @@ test.describe("Cryptography", function () {
|
||||||
|
|
||||||
// Assert that verified icon is rendered
|
// Assert that verified icon is rendered
|
||||||
await page.getByRole("button", { name: "Room members" }).click();
|
await page.getByRole("button", { name: "Room members" }).click();
|
||||||
await page.getByRole("button", { name: "Room information" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||||
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted");
|
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted");
|
||||||
|
|
||||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||||
|
|
|
@ -102,7 +102,7 @@ test.describe("Dehydration", () => {
|
||||||
|
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
await getMemberTileByName(page, NAME).click();
|
||||||
|
|
|
@ -80,7 +80,7 @@ test.describe("Lazy Loading", () => {
|
||||||
|
|
||||||
async function openMemberlist(page: Page): Promise<void> {
|
async function openMemberlist(page: Page): Promise<void> {
|
||||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click();
|
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click();
|
||||||
await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||||
|
|
|
@ -399,11 +399,10 @@ class Helpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the threads panel. (Actually, close any right panel, but for these
|
* Close the threads panel.
|
||||||
* tests we only open the threads panel.)
|
|
||||||
*/
|
*/
|
||||||
async closeThreadsPanel() {
|
async closeThreadsPanel() {
|
||||||
await this.page.locator(".mx_RightPanel").getByLabel("Close").click();
|
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||||
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +410,7 @@ class Helpers {
|
||||||
* Return to the list of threads, given we are viewing a single thread.
|
* Return to the list of threads, given we are viewing a single thread.
|
||||||
*/
|
*/
|
||||||
async backToThreadsList() {
|
async backToThreadsList() {
|
||||||
await this.page.locator(".mx_RightPanel").getByLabel("Threads").click();
|
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -113,7 +113,7 @@ test.describe("RightPanel", () => {
|
||||||
test("should handle viewing room member", async ({ page, app }) => {
|
test("should handle viewing room member", async ({ page, app }) => {
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
await getMemberTileByName(page, NAME).click();
|
||||||
|
@ -123,7 +123,7 @@ test.describe("RightPanel", () => {
|
||||||
await page.getByRole("button", { name: "Room members" }).click();
|
await page.getByRole("button", { name: "Room members" }).click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Room information" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -337,12 +337,10 @@ export class Helpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that the thread panel is focused (actually the 'close' button, specifically)
|
* Assert that the thread tab is focused
|
||||||
*/
|
*/
|
||||||
assertThreadPanelFocused() {
|
assertThreadTabFocused() {
|
||||||
return expect(
|
return expect(this.page.locator("#thread-panel-tab")).toBeFocused();
|
||||||
this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"),
|
|
||||||
).toBeFocused();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -161,17 +161,12 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await util.assertNoTacIndicator();
|
await util.assertNoTacIndicator();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should focus the thread panel close button when clicking an item in the TAC", async ({
|
test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => {
|
||||||
room1,
|
|
||||||
room2,
|
|
||||||
util,
|
|
||||||
msg,
|
|
||||||
}) => {
|
|
||||||
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||||
|
|
||||||
await util.openTac();
|
await util.openTac();
|
||||||
await util.clickRoomInTac(room1.name);
|
await util.clickRoomInTac(room1.name);
|
||||||
|
|
||||||
await util.assertThreadPanelFocused();
|
await util.assertThreadTabFocused();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
|
@ -261,6 +261,7 @@
|
||||||
@import "./views/right_panel/_BaseCard.pcss";
|
@import "./views/right_panel/_BaseCard.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/_RoomSummaryCard.pcss";
|
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
||||||
@import "./views/right_panel/_ThreadPanel.pcss";
|
@import "./views/right_panel/_ThreadPanel.pcss";
|
||||||
@import "./views/right_panel/_TimelineCard.pcss";
|
@import "./views/right_panel/_TimelineCard.pcss";
|
||||||
|
|
25
res/css/views/right_panel/_RightPanelTabs.pcss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
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_RightPanelTabs {
|
||||||
|
margin: 0;
|
||||||
|
height: 64px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -235,7 +235,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_header {
|
.mx_RoomSummaryCard_header {
|
||||||
padding: 15px 12px;
|
padding: 24px 12px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_search {
|
.mx_RoomSummaryCard_search {
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
.mx_Spinner {
|
.mx_Spinner {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
|
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
||||||
|
@ -171,6 +172,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
<MemberList
|
<MemberList
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
key={roomId}
|
key={roomId}
|
||||||
|
hideHeaderButtons
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
searchQuery={this.state.searchQuery}
|
searchQuery={this.state.searchQuery}
|
||||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||||
|
@ -294,7 +296,6 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
card = (
|
card = (
|
||||||
<RoomSummaryCard
|
<RoomSummaryCard
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
onClose={this.onClose}
|
|
||||||
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
||||||
permalinkCreator={this.props.permalinkCreator!}
|
permalinkCreator={this.props.permalinkCreator!}
|
||||||
onSearchChange={this.props.onSearchChange}
|
onSearchChange={this.props.onSearchChange}
|
||||||
|
@ -314,6 +315,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="mx_RightPanel" id="mx_RightPanel">
|
<aside className="mx_RightPanel" id="mx_RightPanel">
|
||||||
|
{phase && <RightPanelTabs phase={phase} />}
|
||||||
{card}
|
{card}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1287,7 +1287,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Action.View3pidInvite:
|
case Action.View3pidInvite:
|
||||||
|
|
|
@ -37,9 +37,6 @@ 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 { useDispatcher } from "../../hooks/useDispatcher";
|
|
||||||
import dis from "../../dispatcher/dispatcher";
|
|
||||||
import { Action } from "../../dispatcher/actions";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -259,14 +256,6 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
}
|
}
|
||||||
}, [timelineSet, timelinePanel]);
|
}, [timelineSet, timelinePanel]);
|
||||||
|
|
||||||
useDispatcher(dis, (payload) => {
|
|
||||||
// This actually foucses the close button on the threads panel, as its the only interactive element,
|
|
||||||
// but at least it puts the user in the right area of the app.
|
|
||||||
if (payload.action === Action.FocusThreadsPanel) {
|
|
||||||
closeButonRef.current?.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider
|
<RoomContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -277,6 +266,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
|
hideHeaderButtons
|
||||||
header={
|
header={
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
|
@ -284,7 +274,10 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
empty={!hasThreads}
|
empty={!hasThreads}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
id="thread-panel"
|
||||||
className="mx_ThreadPanel"
|
className="mx_ThreadPanel"
|
||||||
|
ariaLabelledBy="thread-panel-tab"
|
||||||
|
role="tabpanel"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
ref={card}
|
ref={card}
|
||||||
|
|
|
@ -26,8 +26,12 @@ import { CardContext } from "./context";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
header?: ReactNode | null;
|
header?: ReactNode | null;
|
||||||
|
hideHeaderButtons?: boolean;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
role?: "tabpanel";
|
||||||
|
ariaLabelledBy?: string;
|
||||||
withoutScrollContainer?: boolean;
|
withoutScrollContainer?: boolean;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
onClose?(ev: ButtonEvent): void;
|
onClose?(ev: ButtonEvent): void;
|
||||||
|
@ -62,6 +66,10 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
onClose,
|
onClose,
|
||||||
onBack,
|
onBack,
|
||||||
className,
|
className,
|
||||||
|
id,
|
||||||
|
ariaLabelledBy,
|
||||||
|
role,
|
||||||
|
hideHeaderButtons,
|
||||||
header,
|
header,
|
||||||
footer,
|
footer,
|
||||||
withoutScrollContainer,
|
withoutScrollContainer,
|
||||||
|
@ -100,13 +108,31 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerButtons: React.ReactElement | undefined;
|
||||||
|
if (!hideHeaderButtons) {
|
||||||
|
headerButtons = (
|
||||||
|
<>
|
||||||
|
{backButton}
|
||||||
|
{closeButton}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRenderHeader = header || !hideHeaderButtons;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContext.Provider value={{ isCard: true }}>
|
<CardContext.Provider value={{ isCard: true }}>
|
||||||
<div className={classNames("mx_BaseCard", className)} ref={ref} onKeyDown={onKeyDown}>
|
<div
|
||||||
{header !== null && (
|
id={id}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
role={role}
|
||||||
|
className={classNames("mx_BaseCard", className)}
|
||||||
|
ref={ref}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
{shouldRenderHeader && (
|
||||||
<div className="mx_BaseCard_header">
|
<div className="mx_BaseCard_header">
|
||||||
{backButton}
|
{headerButtons}
|
||||||
{closeButton}
|
|
||||||
<div className="mx_BaseCard_headerProp">{header}</div>
|
<div className="mx_BaseCard_headerProp">{header}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -214,27 +214,27 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
||||||
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
||||||
if (this.state.phase === currentPhase) {
|
if (this.state.phase === currentPhase) {
|
||||||
RightPanelStore.instance.showOrHidePanel(currentPhase);
|
RightPanelStore.instance.showOrHidePhase(currentPhase);
|
||||||
} else {
|
} else {
|
||||||
RightPanelStore.instance.showOrHidePanel(currentPhase, RightPanelStore.instance.currentCard.state);
|
RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNotificationsClicked = (): void => {
|
private onNotificationsClicked = (): void => {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPinnedMessagesClicked = (): void => {
|
private onPinnedMessagesClicked = (): void => {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.PinnedMessages);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages);
|
||||||
};
|
};
|
||||||
private onTimelineCardClicked = (): void => {
|
private onTimelineCardClicked = (): void => {
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Timeline);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||||
|
|
86
src/components/views/right_panel/RightPanelTabs.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
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, { useRef } from "react";
|
||||||
|
import { NavBar, NavItem } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
|
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
|
import dispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
|
function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
|
||||||
|
const tabs = [RightPanelPhases.RoomSummary, RightPanelPhases.RoomMemberList, RightPanelPhases.ThreadPanel];
|
||||||
|
return !!phase && tabs.includes(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
phase: RightPanelPhases;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightPanelTabs: React.FC<Props> = ({ phase }): JSX.Element | null => {
|
||||||
|
const threadsTabRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
useDispatcher(dispatcher, (payload) => {
|
||||||
|
// This actually focuses the threads tab, as its the only interactive element,
|
||||||
|
// but at least it puts the user in the right area of the app.
|
||||||
|
if (payload.action === Action.FocusThreadsPanel) {
|
||||||
|
threadsTabRef.current?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldShowTabsForPhase(phase)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavBar className="mx_RightPanelTabs" aria-label="right panel" role="tablist">
|
||||||
|
<NavItem
|
||||||
|
aria-controls="room-summary-panel"
|
||||||
|
id="room-summary-panel-tab"
|
||||||
|
onClick={() => {
|
||||||
|
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomSummary }, true);
|
||||||
|
}}
|
||||||
|
active={phase === RightPanelPhases.RoomSummary}
|
||||||
|
>
|
||||||
|
{_t("right_panel|info")}
|
||||||
|
</NavItem>
|
||||||
|
<NavItem
|
||||||
|
aria-controls="memberlist-panel"
|
||||||
|
id="memberlist-panel-tab"
|
||||||
|
onClick={(ev: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||||
|
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
|
||||||
|
}}
|
||||||
|
active={phase === RightPanelPhases.RoomMemberList}
|
||||||
|
>
|
||||||
|
{_t("common|people")}
|
||||||
|
</NavItem>
|
||||||
|
<NavItem
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
id="thread-panel-tab"
|
||||||
|
onClick={() => {
|
||||||
|
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true);
|
||||||
|
}}
|
||||||
|
active={phase === RightPanelPhases.ThreadPanel}
|
||||||
|
ref={threadsTabRef}
|
||||||
|
>
|
||||||
|
{_t("common|threads")}
|
||||||
|
</NavItem>
|
||||||
|
</NavBar>
|
||||||
|
);
|
||||||
|
};
|
|
@ -39,7 +39,6 @@ import {
|
||||||
} from "@vector-im/compound-web";
|
} from "@vector-im/compound-web";
|
||||||
import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg";
|
import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite.svg";
|
||||||
import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
|
import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
|
||||||
import { Icon as UserProfileSolidIcon } from "@vector-im/compound-design-tokens/icons/user-profile-solid.svg";
|
|
||||||
import { Icon as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
import { Icon as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
||||||
import { Icon as SettingsIcon } from "@vector-im/compound-design-tokens/icons/settings.svg";
|
import { Icon as SettingsIcon } from "@vector-im/compound-design-tokens/icons/settings.svg";
|
||||||
import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg";
|
import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg";
|
||||||
|
@ -106,7 +105,6 @@ import { useTransition } from "../../../hooks/useTransition";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
onClose(): void;
|
|
||||||
onSearchChange?: (e: ChangeEvent) => void;
|
onSearchChange?: (e: ChangeEvent) => void;
|
||||||
onSearchCancel?: () => void;
|
onSearchCancel?: () => void;
|
||||||
focusRoomSearch?: boolean;
|
focusRoomSearch?: boolean;
|
||||||
|
@ -382,7 +380,6 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
||||||
const RoomSummaryCard: React.FC<IProps> = ({
|
const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
room,
|
room,
|
||||||
permalinkCreator,
|
permalinkCreator,
|
||||||
onClose,
|
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSearchCancel,
|
onSearchCancel,
|
||||||
focusRoomSearch,
|
focusRoomSearch,
|
||||||
|
@ -416,11 +413,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRoomMembersClick = (ev: Event): void => {
|
|
||||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
|
|
||||||
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const e2eStatus = roomContext.e2eStatus;
|
const e2eStatus = roomContext.e2eStatus;
|
||||||
|
@ -532,7 +524,13 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard header={null} className="mx_RoomSummaryCard" onClose={onClose}>
|
<BaseCard
|
||||||
|
hideHeaderButtons
|
||||||
|
id="room-summary-panel"
|
||||||
|
className="mx_RoomSummaryCard"
|
||||||
|
ariaLabelledBy="room-summary-panel-tab"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
<Flex
|
<Flex
|
||||||
as="header"
|
as="header"
|
||||||
className="mx_RoomSummaryCard_header"
|
className="mx_RoomSummaryCard_header"
|
||||||
|
@ -558,12 +556,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
/>
|
/>
|
||||||
</Form.Root>
|
</Form.Root>
|
||||||
)}
|
)}
|
||||||
<AccessibleButton
|
|
||||||
data-testid="base-card-close-button"
|
|
||||||
className="mx_BaseCard_close"
|
|
||||||
onClick={onClose}
|
|
||||||
title={_t("action|close")}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{header}
|
{header}
|
||||||
|
@ -589,13 +581,6 @@ const RoomSummaryCard: React.FC<IProps> = ({
|
||||||
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
|
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
|
||||||
// this icon matches the legacy implementation
|
|
||||||
// and is a short term solution until legacy room header is removed
|
|
||||||
Icon={UserProfileSolidIcon}
|
|
||||||
label={_t("common|people")}
|
|
||||||
onSelect={onRoomMembersClick}
|
|
||||||
/>
|
|
||||||
{!isVideoRoom && (
|
{!isVideoRoom && (
|
||||||
<>
|
<>
|
||||||
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
|
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
|
||||||
|
|
|
@ -80,7 +80,7 @@ import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-me
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
import { asyncSome } from "../../../utils/arrays";
|
import { asyncSome } from "../../../utils/arrays";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
import { SpaceScopeHeader } from "../rooms/SpaceScopeHeader";
|
import { createSpaceScopeHeader } from "../rooms/SpaceScopeHeader";
|
||||||
|
|
||||||
export interface IDevice extends Device {
|
export interface IDevice extends Device {
|
||||||
ambiguous?: boolean;
|
ambiguous?: boolean;
|
||||||
|
@ -1774,10 +1774,11 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} roomId={room?.roomId} />
|
<UserInfoHeader member={member} e2eStatus={e2eStatus} roomId={room?.roomId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard
|
<BaseCard
|
||||||
className={classes.join(" ")}
|
className={classes.join(" ")}
|
||||||
header={room ? <SpaceScopeHeader room={room} /> : undefined}
|
header={createSpaceScopeHeader(room)}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
closeLabel={closeLabel}
|
closeLabel={closeLabel}
|
||||||
cardState={cardState}
|
cardState={cardState}
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { SDKContext } from "../../../contexts/SDKContext";
|
||||||
import { canInviteTo } from "../../../utils/room/canInviteTo";
|
import { canInviteTo } from "../../../utils/room/canInviteTo";
|
||||||
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
|
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { SpaceScopeHeader } from "./SpaceScopeHeader";
|
import { createSpaceScopeHeader } from "./SpaceScopeHeader";
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||||
|
@ -64,6 +64,7 @@ const SHOW_MORE_INCREMENT = 100;
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
hideHeaderButtons?: boolean;
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
onSearchQueryChanged: (query: string) => void;
|
onSearchQueryChanged: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -358,7 +359,14 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return (
|
return (
|
||||||
<BaseCard className="mx_MemberList" onClose={this.props.onClose}>
|
<BaseCard
|
||||||
|
id="memberlist-panel"
|
||||||
|
className="mx_MemberList"
|
||||||
|
ariaLabelledBy="memberlist-panel-tab"
|
||||||
|
role="tabpanel"
|
||||||
|
hideHeaderButtons={this.props.hideHeaderButtons}
|
||||||
|
onClose={this.props.onClose}
|
||||||
|
>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
|
@ -415,12 +423,14 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const scopeHeader = room ? <SpaceScopeHeader room={room} /> : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard
|
<BaseCard
|
||||||
|
id="memberlist-panel"
|
||||||
className="mx_MemberList"
|
className="mx_MemberList"
|
||||||
header={<React.Fragment>{scopeHeader}</React.Fragment>}
|
ariaLabelledBy="memberlist-panel-tab"
|
||||||
|
role="tabpanel"
|
||||||
|
header={createSpaceScopeHeader(room)}
|
||||||
|
hideHeaderButtons={this.props.hideHeaderButtons}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/v
|
||||||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||||
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||||
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
||||||
|
import { Icon as RoomInfoIcon } from "@vector-im/compound-design-tokens/icons/info-solid.svg";
|
||||||
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||||
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
||||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||||
|
@ -336,6 +337,17 @@ export default function RoomHeader({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Tooltip label={_t("right_panel|room_summary_card|title")}>
|
||||||
|
<IconButton
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||||
|
}}
|
||||||
|
aria-label={_t("right_panel|room_summary_card|title")}
|
||||||
|
>
|
||||||
|
<RoomInfoIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip label={_t("common|threads")}>
|
<Tooltip label={_t("common|threads")}>
|
||||||
<IconButton
|
<IconButton
|
||||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||||
|
|
|
@ -21,18 +21,23 @@ import { Text } from "@vector-im/compound-web";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a space scope header if needed
|
||||||
|
* @param room The room object
|
||||||
|
* @returns rendered component if the room is a space room, otherwise returns null
|
||||||
|
*/
|
||||||
|
export function createSpaceScopeHeader(room?: Room | null): React.JSX.Element | null {
|
||||||
|
if (room?.isSpaceRoom()) return <SpaceScopeHeader room={room} />;
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope header used to decorate right panels that are scoped to a space.
|
* Scope header used to decorate right panels that are scoped to a space.
|
||||||
* When room is not a space renders nothing.
|
* It renders room avatar and name.
|
||||||
* Otherwise renders room avatar and name.
|
|
||||||
*/
|
*/
|
||||||
export const SpaceScopeHeader: React.FC<{ room: Room }> = ({ room }) => {
|
export const SpaceScopeHeader: React.FC<{ room: Room }> = ({ room }) => {
|
||||||
const roomName = useRoomName(room);
|
const roomName = useRoomName(room);
|
||||||
|
|
||||||
if (!room.isSpaceRoom()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
as="div"
|
as="div"
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { Action } from "../../../dispatcher/actions";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
import BaseCard from "../right_panel/BaseCard";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import { SpaceScopeHeader } from "./SpaceScopeHeader";
|
import { createSpaceScopeHeader } from "./SpaceScopeHeader";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
event: MatrixEvent;
|
event: MatrixEvent;
|
||||||
|
@ -133,10 +133,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopeHeader: JSX.Element | undefined = this.room ? <SpaceScopeHeader room={this.room} /> : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard header={scopeHeader} onClose={this.props.onClose}>
|
<BaseCard header={createSpaceScopeHeader(this.room)} onClose={this.props.onClose}>
|
||||||
<Flex className="mx_ThirdPartyMemberInfo" direction="column" gap="var(--cpd-space-4x)">
|
<Flex className="mx_ThirdPartyMemberInfo" direction="column" gap="var(--cpd-space-4x)">
|
||||||
<Flex direction="column" as="section" justify="start" gap="var(--cpd-space-2x)">
|
<Flex direction="column" as="section" justify="start" gap="var(--cpd-space-2x)">
|
||||||
{/* same as userinfo name style */}
|
{/* same as userinfo name style */}
|
||||||
|
|
|
@ -1833,6 +1833,7 @@
|
||||||
"edit_integrations": "Edit widgets, bridges & bots",
|
"edit_integrations": "Edit widgets, bridges & bots",
|
||||||
"export_chat_button": "Export chat",
|
"export_chat_button": "Export chat",
|
||||||
"files_button": "Files",
|
"files_button": "Files",
|
||||||
|
"info": "Info",
|
||||||
"pinned_messages": {
|
"pinned_messages": {
|
||||||
"empty": "Nothing pinned, yet",
|
"empty": "Nothing pinned, yet",
|
||||||
"explainer": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
"explainer": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||||
|
|
|
@ -236,6 +236,23 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the right panel is open, it is closed.
|
||||||
|
* If the right panel is closed, it is opened with `phase`.
|
||||||
|
*
|
||||||
|
* This is different from showOrHidePhase which only closes the panel
|
||||||
|
* if the panel was already showing the phase passed as argument.
|
||||||
|
* @see showOrHidePhase
|
||||||
|
* @param phase The right panel phase.
|
||||||
|
*/
|
||||||
|
public showOrHidePanel(phase: RightPanelPhases): void {
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.setCard({ phase });
|
||||||
|
} else {
|
||||||
|
this.togglePanel(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to show a right panel phase.
|
* Helper to show a right panel phase.
|
||||||
* If the UI is already showing that phase, the right panel will be hidden.
|
* If the UI is already showing that phase, the right panel will be hidden.
|
||||||
|
@ -245,7 +262,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||||
* @param phase The right panel phase.
|
* @param phase The right panel phase.
|
||||||
* @param cardState The state within the phase.
|
* @param cardState The state within the phase.
|
||||||
*/
|
*/
|
||||||
public showOrHidePanel(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
public showOrHidePhase(phase: RightPanelPhases, cardState?: Partial<IRightPanelCardState>): void {
|
||||||
if (this.currentCard.phase == phase && !cardState && this.isOpen) {
|
if (this.currentCard.phase == phase && !cardState && this.isOpen) {
|
||||||
this.togglePanel(null);
|
this.togglePanel(null);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,6 +32,6 @@ export const onView3pidInvite = (payload: ActionPayload, rightPanelStore: RightP
|
||||||
state: { memberInfoEvent: payload.event },
|
state: { memberInfoEvent: payload.event },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
rightPanelStore.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
rightPanelStore.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,8 +36,6 @@ import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||||
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils";
|
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../test-utils";
|
||||||
import { mkThread } from "../../test-utils/threads";
|
import { mkThread } from "../../test-utils/threads";
|
||||||
import { IRoomState } from "../../../src/components/structures/RoomView";
|
import { IRoomState } from "../../../src/components/structures/RoomView";
|
||||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
|
||||||
|
|
||||||
jest.mock("../../../src/utils/Feedback");
|
jest.mock("../../../src/utils/Feedback");
|
||||||
|
|
||||||
|
@ -148,43 +146,6 @@ describe("ThreadPanel", () => {
|
||||||
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
|
||||||
await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled());
|
await waitFor(() => expect(mockClient.sendReadReceipt).not.toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("focuses the close button on FocusThreadsPanel dispatch", () => {
|
|
||||||
const ROOM_ID = "!roomId:example.org";
|
|
||||||
|
|
||||||
stubClient();
|
|
||||||
mockPlatformPeg();
|
|
||||||
const mockClient = mocked(MatrixClientPeg.safeGet());
|
|
||||||
|
|
||||||
const room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
|
||||||
<RoomContext.Provider
|
|
||||||
value={getRoomContext(room, {
|
|
||||||
canSendMessages: true,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ThreadPanel
|
|
||||||
roomId={ROOM_ID}
|
|
||||||
onClose={jest.fn()}
|
|
||||||
resizeNotifier={new ResizeNotifier()}
|
|
||||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
|
||||||
/>
|
|
||||||
</RoomContext.Provider>
|
|
||||||
</MatrixClientContext.Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unfocus it first so we know it's not just focused by coincidence
|
|
||||||
screen.getByTestId("base-card-close-button").blur();
|
|
||||||
expect(screen.getByTestId("base-card-close-button")).not.toHaveFocus();
|
|
||||||
|
|
||||||
defaultDispatcher.dispatch({ action: Action.FocusThreadsPanel }, true);
|
|
||||||
|
|
||||||
expect(screen.getByTestId("base-card-close-button")).toHaveFocus();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Filtering", () => {
|
describe("Filtering", () => {
|
||||||
|
|
72
test/components/views/right_panel/RightPanelTabs-test.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
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 { render, fireEvent } from "@testing-library/react";
|
||||||
|
|
||||||
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
|
||||||
|
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||||
|
import { RightPanelTabs } from "../../../../src/components/views/right_panel/RightPanelTabs";
|
||||||
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
|
||||||
|
describe("<RightPanelTabs />", () => {
|
||||||
|
it("Component renders the correct tabs", () => {
|
||||||
|
const { container, getByRole } = render(<RightPanelTabs phase={RightPanelPhases.RoomSummary} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
|
||||||
|
// We expect Info, People and Threads as tabs
|
||||||
|
expect(getByRole("tab", { name: "Info" })).toBeDefined();
|
||||||
|
expect(getByRole("tab", { name: "People" })).toBeDefined();
|
||||||
|
expect(getByRole("tab", { name: "Threads" })).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Correct tab is active", () => {
|
||||||
|
const { container } = render(<RightPanelTabs phase={RightPanelPhases.RoomMemberList} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
// Assert that the active tab is Info
|
||||||
|
expect(container.querySelectorAll("[aria-selected='true'").length).toEqual(1);
|
||||||
|
expect(container.querySelector("[aria-selected='true'")).toHaveAccessibleName("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Renders nothing for some phases, eg: FilePanel", () => {
|
||||||
|
const { container } = render(<RightPanelTabs phase={RightPanelPhases.FilePanel} />);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onClick behaviors work as expected", () => {
|
||||||
|
const spy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||||
|
const { getByRole } = render(<RightPanelTabs phase={RightPanelPhases.RoomSummary} />);
|
||||||
|
|
||||||
|
// Info -> People
|
||||||
|
fireEvent.click(getByRole("tab", { name: "People" }));
|
||||||
|
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||||
|
|
||||||
|
// People -> Threads
|
||||||
|
fireEvent.click(getByRole("tab", { name: "Threads" }));
|
||||||
|
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.ThreadPanel }, true);
|
||||||
|
|
||||||
|
// Threads -> Info
|
||||||
|
fireEvent.click(getByRole("tab", { name: "Info" }));
|
||||||
|
expect(spy).toHaveBeenLastCalledWith({ phase: RightPanelPhases.RoomSummary }, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Threads tab is focused on action", () => {
|
||||||
|
const { getByRole } = render(<RightPanelTabs phase={RightPanelPhases.ThreadPanel} />);
|
||||||
|
dis.dispatch({ action: Action.FocusThreadsPanel }, true);
|
||||||
|
expect(getByRole("tab", { name: "Threads" })).toHaveFocus();
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,7 +35,6 @@ import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } f
|
||||||
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/PollHistoryDialog";
|
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/PollHistoryDialog";
|
||||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||||
import { _t } from "../../../../src/languageHandler";
|
import { _t } from "../../../../src/languageHandler";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
|
||||||
import { tagRoom } from "../../../../src/utils/room/tagRoom";
|
import { tagRoom } from "../../../../src/utils/room/tagRoom";
|
||||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
@ -195,7 +194,6 @@ describe("<RoomSummaryCard />", () => {
|
||||||
<RoomSummaryCard
|
<RoomSummaryCard
|
||||||
room={room}
|
room={room}
|
||||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||||
onClose={jest.fn()}
|
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
focusRoomSearch={true}
|
focusRoomSearch={true}
|
||||||
/>
|
/>
|
||||||
|
@ -212,7 +210,6 @@ describe("<RoomSummaryCard />", () => {
|
||||||
<RoomSummaryCard
|
<RoomSummaryCard
|
||||||
room={room}
|
room={room}
|
||||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||||
onClose={jest.fn()}
|
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
/>
|
/>
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
|
@ -270,18 +267,6 @@ describe("<RoomSummaryCard />", () => {
|
||||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
|
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders room members options when new room UI is not enabled", () => {
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
|
||||||
const { getByText } = getComponent();
|
|
||||||
|
|
||||||
fireEvent.click(getByText(_t("common|people")));
|
|
||||||
|
|
||||||
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
|
|
||||||
{ phase: RightPanelPhases.RoomMemberList },
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("pinning", () => {
|
describe("pinning", () => {
|
||||||
it("renders pins options when pinning feature is enabled", () => {
|
it("renders pins options when pinning feature is enabled", () => {
|
||||||
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning");
|
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning");
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RightPanelTabs /> Component renders the correct tabs 1`] = `
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
class="mx_RightPanelTabs _nav-bar_135dy_16"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
aria-label="right panel"
|
||||||
|
class="_nav-bar-items_135dy_22"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
data-current="true"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="room-summary-panel"
|
||||||
|
aria-selected="true"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="room-summary-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="memberlist-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="memberlist-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
People
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="thread-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Threads
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RightPanelTabs /> Correct tab is active 1`] = `
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
class="mx_RightPanelTabs _nav-bar_135dy_16"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
aria-label="right panel"
|
||||||
|
class="_nav-bar-items_135dy_22"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="room-summary-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="room-summary-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
data-current="true"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="memberlist-panel"
|
||||||
|
aria-selected="true"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="memberlist-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
People
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="_nav-tab_135dy_33"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-controls="thread-panel"
|
||||||
|
aria-selected="false"
|
||||||
|
class="_nav-item_135dy_55"
|
||||||
|
id="thread-panel-tab"
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
Threads
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -3,7 +3,10 @@
|
||||||
exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
aria-labelledby="room-summary-panel-tab"
|
||||||
class="mx_BaseCard mx_RoomSummaryCard"
|
class="mx_BaseCard mx_RoomSummaryCard"
|
||||||
|
id="room-summary-panel"
|
||||||
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_AutoHideScrollbar"
|
class="mx_AutoHideScrollbar"
|
||||||
|
@ -12,15 +15,7 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
||||||
<header
|
<header
|
||||||
class="mx_Flex mx_RoomSummaryCard_header"
|
class="mx_Flex mx_RoomSummaryCard_header"
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
aria-label="Close"
|
|
||||||
class="mx_AccessibleButton mx_BaseCard_close"
|
|
||||||
data-testid="base-card-close-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<header
|
<header
|
||||||
class="mx_RoomSummaryCard_container"
|
class="mx_RoomSummaryCard_container"
|
||||||
>
|
>
|
||||||
|
@ -244,36 +239,6 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
|
||||||
data-kind="primary"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_icon_1gwvj_44"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
|
||||||
>
|
|
||||||
People
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_nav-hint_1gwvj_60"
|
|
||||||
fill="currentColor"
|
|
||||||
height="24"
|
|
||||||
viewBox="8 0 8 24"
|
|
||||||
width="8"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
@ -423,7 +388,10 @@ exports[`<RoomSummaryCard /> has button to edit topic when expanded 1`] = `
|
||||||
exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
aria-labelledby="room-summary-panel-tab"
|
||||||
class="mx_BaseCard mx_RoomSummaryCard"
|
class="mx_BaseCard mx_RoomSummaryCard"
|
||||||
|
id="room-summary-panel"
|
||||||
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_AutoHideScrollbar"
|
class="mx_AutoHideScrollbar"
|
||||||
|
@ -432,15 +400,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
<header
|
<header
|
||||||
class="mx_Flex mx_RoomSummaryCard_header"
|
class="mx_Flex mx_RoomSummaryCard_header"
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
aria-label="Close"
|
|
||||||
class="mx_AccessibleButton mx_BaseCard_close"
|
|
||||||
data-testid="base-card-close-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<header
|
<header
|
||||||
class="mx_RoomSummaryCard_container"
|
class="mx_RoomSummaryCard_container"
|
||||||
>
|
>
|
||||||
|
@ -637,36 +597,6 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
|
||||||
data-kind="primary"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_icon_1gwvj_44"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
|
||||||
>
|
|
||||||
People
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_nav-hint_1gwvj_60"
|
|
||||||
fill="currentColor"
|
|
||||||
height="24"
|
|
||||||
viewBox="8 0 8 24"
|
|
||||||
width="8"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
@ -816,7 +746,10 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
aria-labelledby="room-summary-panel-tab"
|
||||||
class="mx_BaseCard mx_RoomSummaryCard"
|
class="mx_BaseCard mx_RoomSummaryCard"
|
||||||
|
id="room-summary-panel"
|
||||||
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_AutoHideScrollbar"
|
class="mx_AutoHideScrollbar"
|
||||||
|
@ -825,15 +758,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||||
<header
|
<header
|
||||||
class="mx_Flex mx_RoomSummaryCard_header"
|
class="mx_Flex mx_RoomSummaryCard_header"
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-3x);"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
aria-label="Close"
|
|
||||||
class="mx_AccessibleButton mx_BaseCard_close"
|
|
||||||
data-testid="base-card-close-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<header
|
<header
|
||||||
class="mx_RoomSummaryCard_container"
|
class="mx_RoomSummaryCard_container"
|
||||||
>
|
>
|
||||||
|
@ -1041,36 +966,6 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||||
data-orientation="horizontal"
|
data-orientation="horizontal"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
|
||||||
data-kind="primary"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_icon_1gwvj_44"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
|
|
||||||
>
|
|
||||||
People
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="_nav-hint_1gwvj_60"
|
|
||||||
fill="currentColor"
|
|
||||||
height="24"
|
|
||||||
viewBox="8 0 8 24"
|
|
||||||
width="8"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
class="_item_1gwvj_17 _interactive_1gwvj_36"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
|
|
@ -216,6 +216,13 @@ describe("RoomHeader", () => {
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("has room info icon that opens the room info panel", async () => {
|
||||||
|
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
||||||
|
fireEvent.click(infoButton);
|
||||||
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
|
});
|
||||||
|
|
||||||
it("opens the thread panel", async () => {
|
it("opens the thread panel", async () => {
|
||||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,20 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="Room info"
|
||||||
|
class="_icon-button_rijzz_17"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_133tf_26"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Threads"
|
aria-label="Threads"
|
||||||
class="_icon-button_rijzz_17"
|
class="_icon-button_rijzz_17"
|
||||||
|
|
|
@ -225,4 +225,22 @@ describe("RightPanelStore", () => {
|
||||||
await viewRoom("!1:example.org");
|
await viewRoom("!1:example.org");
|
||||||
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
|
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("showOrHidePhase works as expected", async () => {
|
||||||
|
await viewRoom("!1:example.org");
|
||||||
|
|
||||||
|
// Open the memberlist panel
|
||||||
|
store.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||||
|
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
|
||||||
|
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
|
||||||
|
|
||||||
|
// showOrHide with RoomSummary should now close the panel
|
||||||
|
store.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||||
|
expect(store.isOpenForRoom("!1:example.org")).toEqual(false);
|
||||||
|
|
||||||
|
// showOrHide with RoomSummary should now open the panel
|
||||||
|
store.showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||||
|
expect(store.isOpenForRoom("!1:example.org")).toEqual(true);
|
||||||
|
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomSummary);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe("onView3pidInvite()", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rightPanelStore = {
|
rightPanelStore = {
|
||||||
pushCard: jest.fn(),
|
pushCard: jest.fn(),
|
||||||
showOrHidePanel: jest.fn(),
|
showOrHidePhase: jest.fn(),
|
||||||
} as unknown as MockedObject<RightPanelStore>;
|
} as unknown as MockedObject<RightPanelStore>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ describe("onView3pidInvite()", () => {
|
||||||
};
|
};
|
||||||
onView3pidInvite(payload, rightPanelStore);
|
onView3pidInvite(payload, rightPanelStore);
|
||||||
|
|
||||||
expect(rightPanelStore.showOrHidePanel).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList);
|
expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList);
|
||||||
expect(rightPanelStore.pushCard).not.toHaveBeenCalled();
|
expect(rightPanelStore.pushCard).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ describe("onView3pidInvite()", () => {
|
||||||
};
|
};
|
||||||
onView3pidInvite(payload, rightPanelStore);
|
onView3pidInvite(payload, rightPanelStore);
|
||||||
|
|
||||||
expect(rightPanelStore.showOrHidePanel).not.toHaveBeenCalled();
|
expect(rightPanelStore.showOrHidePhase).not.toHaveBeenCalled();
|
||||||
expect(rightPanelStore.pushCard).toHaveBeenCalledWith({
|
expect(rightPanelStore.pushCard).toHaveBeenCalledWith({
|
||||||
phase: RightPanelPhases.Room3pidMemberInfo,
|
phase: RightPanelPhases.Room3pidMemberInfo,
|
||||||
state: { memberInfoEvent: payload.event },
|
state: { memberInfoEvent: payload.event },
|
||||||
|
|
45
yarn.lock
|
@ -3040,10 +3040,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
svg2vectordrawable "^2.9.1"
|
svg2vectordrawable "^2.9.1"
|
||||||
|
|
||||||
"@vector-im/compound-web@^5.1.2":
|
"@vector-im/compound-web@^5.2.3":
|
||||||
version "5.1.2"
|
version "5.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.1.2.tgz#2ee3d859819e153c898770c058a4277c0b8ef3b8"
|
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-5.2.3.tgz#feab8ae7623cfaa243b9be69325e1696bfa1a09c"
|
||||||
integrity sha512-p7ide2JRblCkcSMPNakkWjK9GxA8boMQCEgXCT7Dp+owhONf2QsYpyRzlW+tPZ3DULd+h4nqWRova4uSeZtBbA==
|
integrity sha512-KU5vAgNIFBzRHfCRK5dGAhxjrfkrUXeOYzDUNc2QjEnqGaUR3RM4c53sw0Ga1oHbOeAWoUGId+ptH3ewPdUTAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/react" "^0.26.9"
|
"@floating-ui/react" "^0.26.9"
|
||||||
"@floating-ui/react-dom" "^2.0.8"
|
"@floating-ui/react-dom" "^2.0.8"
|
||||||
|
@ -3054,6 +3054,7 @@
|
||||||
"@radix-ui/react-slot" "^1.0.2"
|
"@radix-ui/react-slot" "^1.0.2"
|
||||||
"@radix-ui/react-tooltip" "^1.0.6"
|
"@radix-ui/react-tooltip" "^1.0.6"
|
||||||
classnames "^2.3.2"
|
classnames "^2.3.2"
|
||||||
|
ts-xor "^1.3.0"
|
||||||
vaul "^0.7.0"
|
vaul "^0.7.0"
|
||||||
|
|
||||||
"@zxcvbn-ts/core@^3.0.4":
|
"@zxcvbn-ts/core@^3.0.4":
|
||||||
|
@ -8475,16 +8476,7 @@ string-length@^4.0.1:
|
||||||
char-regex "^1.0.2"
|
char-regex "^1.0.2"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -8571,14 +8563,7 @@ string_decoder@~1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -8921,6 +8906,11 @@ ts-node@^10.9.1:
|
||||||
v8-compile-cache-lib "^3.0.1"
|
v8-compile-cache-lib "^3.0.1"
|
||||||
yn "3.1.1"
|
yn "3.1.1"
|
||||||
|
|
||||||
|
ts-xor@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-xor/-/ts-xor-1.3.0.tgz#3e59f24f0321f9f10f350e0cee3b534b89a2c70b"
|
||||||
|
integrity sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==
|
||||||
|
|
||||||
tsconfig-paths@^3.15.0:
|
tsconfig-paths@^3.15.0:
|
||||||
version "3.15.0"
|
version "3.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
||||||
|
@ -9372,7 +9362,7 @@ which@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
@ -9390,15 +9380,6 @@ wrap-ansi@^6.2.0:
|
||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|