Ditch right panel tabs and re-add close button (#99)

* Add extra buttons to room summary card

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove right panel tabs in favour of X button on each panel

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update room summary card header to align close button correctly

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix typo in pinned messages heading

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix base card title colours

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-03 09:59:41 +01:00 committed by GitHub
parent 67cb8b7590
commit 2dbaf00e71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 865 additions and 500 deletions

View file

@ -223,7 +223,11 @@ class FilePanel extends React.Component<IProps, IState> {
public render(): React.ReactNode {
if (MatrixClientPeg.safeGet().isGuest()) {
return (
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
<BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
<div className="mx_RoomView_empty">
{_t(
"file_panel|guest_note",
@ -241,7 +245,11 @@ class FilePanel extends React.Component<IProps, IState> {
);
} else if (this.noRoom) {
return (
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
<BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
<div className="mx_RoomView_empty">{_t("file_panel|peek_note")}</div>
</BaseCard>
);
@ -273,6 +281,7 @@ class FilePanel extends React.Component<IProps, IState> {
onClose={this.props.onClose}
withoutScrollContainer
ref={this.card}
header={_t("right_panel|files_button")}
>
{this.card.current && (
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
@ -299,7 +308,11 @@ class FilePanel extends React.Component<IProps, IState> {
timelineRenderingType: TimelineRenderingType.File,
}}
>
<BaseCard className="mx_FilePanel" onClose={this.props.onClose}>
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
<Spinner />
</BaseCard>
</RoomContext.Provider>

View file

@ -18,7 +18,6 @@ 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 Heading from "../views/typography/Heading";
import EmptyState from "../views/right_panel/EmptyState";
interface IProps {
@ -88,13 +87,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
}}
>
<BaseCard
header={
<div className="mx_BaseCard_header_title">
<Heading size="4" className="mx_BaseCard_header_title_heading">
{_t("notifications|enable_prompt_toast_title")}
</Heading>
</div>
}
header={_t("notifications|enable_prompt_toast_title")}
/**
* Need to rename this CSS class to something more generic
* Will be done once all the panels are using a similar layout

View file

@ -33,7 +33,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
import { Action } from "../../dispatcher/actions";
import { XOR } from "../../@types/common";
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
import ExtensionsCard from "../views/right_panel/ExtensionsCard";
interface BaseProps {
@ -164,7 +163,6 @@ export default class RightPanel extends React.Component<Props, IState> {
<MemberList
roomId={roomId}
key={roomId}
hideHeaderButtons
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
@ -313,7 +311,6 @@ export default class RightPanel extends React.Component<Props, IState> {
return (
<aside className="mx_RightPanel" id="mx_RightPanel">
{phase && <RightPanelTabs room={this.props.room} phase={phase} />}
{card}
</aside>
);

View file

@ -193,7 +193,6 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}}
>
<BaseCard
hideHeaderButtons
header={
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
}

View file

@ -38,6 +38,12 @@ interface IProps {
children: ReactNode;
}
function closeRightPanel(ev: MouseEvent<HTMLButtonElement>): void {
ev.preventDefault();
ev.stopPropagation();
RightPanelStore.instance.popCard();
}
const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
(
{
@ -81,12 +87,12 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
}
let closeButton;
if (onClose && !hideHeaderButtons) {
if (!hideHeaderButtons) {
closeButton = (
<IconButton
size="28px"
data-testid="base-card-close-button"
onClick={onClose}
onClick={onClose ?? closeRightPanel}
ref={closeButtonRef}
tooltip={closeLabel ?? _t("action|close")}
subtleBackground
@ -116,9 +122,16 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
<div className="mx_BaseCard_header">
{backButton}
{typeof header === "string" ? (
<Text size="md" weight="medium" className="mx_BaseCard_header_title">
{header}
</Text>
<div className="mx_BaseCard_header_title">
<Text
size="md"
weight="medium"
className="mx_BaseCard_header_title_heading"
role="heading"
>
{header}
</Text>
</div>
) : (
(header ?? <div className="mx_BaseCard_header_spacer" />)
)}

View file

@ -157,13 +157,6 @@ const ExtensionsCard: React.FC<Props> = ({ room, onClose }) => {
}
};
// The button is in the header to keep it outside the scrollable region
const header = (
<Button size="sm" onClick={onManageIntegrations} kind="secondary" Icon={PlusIcon}>
{_t("right_panel|add_integrations")}
</Button>
);
let body: JSX.Element;
if (realApps.length < 1) {
body = (
@ -197,7 +190,10 @@ const ExtensionsCard: React.FC<Props> = ({ room, onClose }) => {
}
return (
<BaseCard header={header} className="mx_ExtensionsCard" onClose={onClose} hideHeaderButtons>
<BaseCard header={_t("right_panel|extensions_button")} className="mx_ExtensionsCard" onClose={onClose}>
<Button size="sm" onClick={onManageIntegrations} kind="secondary" Icon={PlusIcon}>
{_t("right_panel|add_integrations")}
</Button>
{body}
</BaseCard>
);

View file

@ -20,7 +20,6 @@ import { PinnedEventTile } from "../rooms/PinnedEventTile";
import { useRoomState } from "../../../hooks/useRoomState";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext";
import { ReadPinsEventId } from "./types";
import Heading from "../typography/Heading";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { filterBoolean } from "../../../utils/arrays";
import Modal from "../../../Modal";
@ -86,13 +85,7 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe
return (
<BaseCard
header={
<div className="mx_BaseCard_header_title">
<Heading size="4" className="mx_BaseCard_header_title_heading">
{_t("right_panel|pinned_messages|header", { count: pinnedEventIds.length })}
</Heading>
</div>
}
header={_t("right_panel|pinned_messages|header", { count: pinnedEventIds.length })}
className="mx_PinnedMessagesCard"
onClose={onClose}
>

View file

@ -1,105 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { useRef } from "react";
import { NavBar, NavItem } from "@vector-im/compound-web";
import { Room } from "matrix-js-sdk/src/matrix";
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";
import SettingsStore from "../../../settings/SettingsStore";
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
function shouldShowTabsForPhase(phase?: RightPanelPhases): boolean {
const tabs = [
RightPanelPhases.RoomSummary,
RightPanelPhases.RoomMemberList,
RightPanelPhases.ThreadPanel,
RightPanelPhases.Extensions,
];
return !!phase && tabs.includes(phase);
}
type Props = {
room?: Room;
phase: RightPanelPhases;
};
export const RightPanelTabs: React.FC<Props> = ({ phase, room }): 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();
}
});
const isVideoRoom = room !== undefined && calcIsVideoRoom(room);
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>
{SettingsStore.getValue(UIFeature.Widgets) &&
!isVideoRoom &&
shouldShowComponent(UIComponent.AddIntegrations) && (
<NavItem
aria-controls="thread-panel"
id="extensions-panel-tab"
onClick={() => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
}}
active={phase === RightPanelPhases.Extensions}
>
{_t("common|extensions")}
</NavItem>
)}
</NavBar>
);
};

View file

@ -27,6 +27,9 @@ import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/set
import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg";
import LeaveIcon from "@vector-im/compound-design-tokens/assets/web/icons/leave";
import FilesIcon from "@vector-im/compound-design-tokens/assets/web/icons/files";
import ExtensionsIcon from "@vector-im/compound-design-tokens/assets/web/icons/extensions";
import UserProfileIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads";
import PollsIcon from "@vector-im/compound-design-tokens/assets/web/icons/polls";
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin";
import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock-solid.svg";
@ -82,10 +85,22 @@ interface IProps {
focusRoomSearch?: boolean;
}
const onRoomMembersClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
};
const onRoomThreadsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.ThreadPanel }, true);
};
const onRoomFilesClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
};
const onRoomExtensionsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.Extensions }, true);
};
const onRoomPinsClick = (): void => {
PosthogTrackers.trackInteraction("PinnedMessageRoomInfoButton");
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true);
@ -254,7 +269,7 @@ const RoomSummaryCard: React.FC<IProps> = ({
);
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const header = (
const roomInfo = (
<header className="mx_RoomSummaryCard_container">
<RoomAvatar room={room} size="80px" viewAvatarOnClick />
<RoomName room={room}>
@ -322,42 +337,34 @@ const RoomSummaryCard: React.FC<IProps> = ({
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const header = onSearchChange && (
<Form.Root className="mx_RoomSummaryCard_search" onSubmit={(e) => e.preventDefault()}>
<Search
placeholder={_t("room|search|placeholder")}
name="room_message_search"
onChange={onSearchChange}
className="mx_no_textinput"
ref={searchInputRef}
autoFocus={focusRoomSearch}
onKeyDown={(e) => {
if (searchInputRef.current && e.key === Key.ESCAPE) {
searchInputRef.current.value = "";
onSearchCancel?.();
}
}}
/>
</Form.Root>
);
return (
<BaseCard
hideHeaderButtons
id="room-summary-panel"
className="mx_RoomSummaryCard"
ariaLabelledBy="room-summary-panel-tab"
role="tabpanel"
header={header}
>
<Flex
as="header"
className="mx_RoomSummaryCard_header"
gap="var(--cpd-space-3x)"
align="center"
justify="space-between"
>
{onSearchChange && (
<Form.Root className="mx_RoomSummaryCard_search" onSubmit={(e) => e.preventDefault()}>
<Search
placeholder={_t("room|search|placeholder")}
name="room_message_search"
onChange={onSearchChange}
className="mx_no_textinput"
ref={searchInputRef}
autoFocus={focusRoomSearch}
onKeyDown={(e) => {
if (searchInputRef.current && e.key === Key.ESCAPE) {
searchInputRef.current.value = "";
onSearchCancel?.();
}
}}
/>
</Form.Root>
)}
</Flex>
{header}
{roomInfo}
<Separator />
@ -379,6 +386,8 @@ const RoomSummaryCard: React.FC<IProps> = ({
<Separator />
<MenuItem Icon={UserProfileIcon} label={_t("common|people")} onSelect={onRoomMembersClick} />
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={onRoomThreadsClick} />
{!isVideoRoom && (
<>
<ReleaseAnnouncement
@ -401,6 +410,11 @@ const RoomSummaryCard: React.FC<IProps> = ({
</div>
</ReleaseAnnouncement>
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
<MenuItem
Icon={ExtensionsIcon}
label={_t("right_panel|extensions_button")}
onSelect={onRoomExtensionsClick}
/>
</>
)}

View file

@ -36,7 +36,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import JumpToBottomButton from "../rooms/JumpToBottomButton";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import Measured from "../elements/Measured";
import Heading from "../typography/Heading";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SdkContextClass } from "../../../contexts/SDKContext";
@ -185,16 +184,6 @@ export default class TimelineCard extends React.Component<IProps, IState> {
}
};
private renderTimelineCardHeader = (): JSX.Element => {
return (
<div className="mx_BaseCard_header_title">
<Heading size="4" className="mx_BaseCard_header_title_heading">
{_t("right_panel|video_room_chat|title")}
</Heading>
</div>
);
};
public render(): React.ReactNode {
const highlightedEventId = this.state.isInitialEventHighlighted ? this.state.initialEventId : undefined;
@ -226,7 +215,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
className={this.props.classNames}
onClose={this.props.onClose}
withoutScrollContainer={true}
header={this.renderTimelineCardHeader()}
header={_t("right_panel|video_room_chat|title")}
ref={this.card}
>
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}

View file

@ -55,7 +55,6 @@ const SHOW_MORE_INCREMENT = 100;
interface IProps {
roomId: string;
searchQuery: string;
hideHeaderButtons?: boolean;
onClose(): void;
onSearchQueryChanged: (query: string) => void;
}
@ -355,7 +354,7 @@ export default class MemberList extends React.Component<IProps, IState> {
className="mx_MemberList"
ariaLabelledBy="memberlist-panel-tab"
role="tabpanel"
hideHeaderButtons={this.props.hideHeaderButtons}
header={_t("common|people")}
onClose={this.props.onClose}
>
<Spinner />
@ -420,7 +419,7 @@ export default class MemberList extends React.Component<IProps, IState> {
className="mx_MemberList"
ariaLabelledBy="memberlist-panel-tab"
role="tabpanel"
hideHeaderButtons={this.props.hideHeaderButtons}
header={_t("common|people")}
footer={footer}
onClose={this.props.onClose}
>

View file

@ -125,7 +125,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
}
return (
<BaseCard onClose={this.props.onClose}>
<BaseCard onClose={this.props.onClose} header={_t("common|profile")}>
<Flex className="mx_ThirdPartyMemberInfo" direction="column" gap="var(--cpd-space-4x)">
<Flex direction="column" as="section" justify="start" gap="var(--cpd-space-2x)">
{/* same as userinfo name style */}