* Remove stale max-len disablements * Remove stale camelCase & naming-convention disablements * Remove stale ban-ts-comment disablements * Remove stale no-var disablements * Remove stale no-empty-property disablements * Remove stale react rule disablements * Remove stale no-constant-condition disablements * Remove stale no-unused-vars disablements * Remove stale disablements for disabled rules * fixup camelcase * Remove dead code * Tidy code * Tweak oxlint config * Use oxlint to apply jsx/tsx extension consistently * Fix import * Fix imports * Rename affected snapshots * Update more imports
324 lines
12 KiB
TypeScript
324 lines
12 KiB
TypeScript
/*
|
|
Copyright 2024, 2025 New Vector Ltd.
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React, { useEffect, useState, type JSX } from "react";
|
|
import classNames from "classnames";
|
|
import {
|
|
MenuItem,
|
|
Separator,
|
|
ToggleMenuItem,
|
|
Text,
|
|
Badge,
|
|
Heading,
|
|
IconButton,
|
|
Link,
|
|
Search,
|
|
Form,
|
|
} from "@vector-im/compound-web";
|
|
import FavouriteIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite";
|
|
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
|
|
import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link";
|
|
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
|
|
import ExportArchiveIcon from "@vector-im/compound-design-tokens/assets/web/icons/export-archive";
|
|
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 LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
|
|
import LockOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-off";
|
|
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
|
import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid";
|
|
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
|
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
|
import { Box, Flex, HistoryVisibilityBadge, LinkedText, StatusTextView } from "@element-hq/web-shared-components";
|
|
|
|
import BaseCard from "./BaseCard.tsx";
|
|
import { _t } from "../../../languageHandler";
|
|
import RoomAvatar from "../avatars/RoomAvatar.tsx";
|
|
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
|
|
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
|
|
import { topicToHtml } from "../../../HtmlUtils.tsx";
|
|
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel";
|
|
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel";
|
|
import { useRoomName } from "../../../hooks/useRoomName.ts";
|
|
|
|
interface IProps {
|
|
room: Room;
|
|
permalinkCreator: RoomPermalinkCreator;
|
|
onSearchChange?: (term: string) => void;
|
|
onSearchCancel?: () => void;
|
|
focusRoomSearch?: boolean;
|
|
searchTerm?: string;
|
|
}
|
|
|
|
const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null => {
|
|
const vm = useRoomTopicViewModel(room);
|
|
|
|
const body = topicToHtml(vm.topic?.text, vm.topic?.html);
|
|
|
|
if (!body && !vm.canEditTopic) {
|
|
return null;
|
|
}
|
|
|
|
if (!body) {
|
|
return (
|
|
<Flex
|
|
as="section"
|
|
direction="column"
|
|
justify="center"
|
|
gap="var(--cpd-space-2x)"
|
|
className="mx_RoomSummaryCard_topic"
|
|
>
|
|
<Box flex="1" className="mx_RoomSummaryCard_topic_box">
|
|
<Link kind="primary" onClick={vm.onEditClick}>
|
|
<Text size="sm" weight="regular">
|
|
{_t("right_panel|add_topic")}
|
|
</Text>
|
|
</Link>
|
|
</Box>
|
|
</Flex>
|
|
);
|
|
}
|
|
|
|
const content = vm.expanded ? <LinkedText as="span">{body}</LinkedText> : body;
|
|
|
|
return (
|
|
<Flex
|
|
as="section"
|
|
direction="column"
|
|
justify="center"
|
|
gap="var(--cpd-space-2x)"
|
|
className={classNames("mx_RoomSummaryCard_topic", {
|
|
mx_RoomSummaryCard_topic_collapsed: !vm.expanded,
|
|
})}
|
|
>
|
|
<Box flex="1" className="mx_RoomSummaryCard_topic_container mx_RoomSummaryCard_topic_box">
|
|
<Text size="sm" weight="regular" onClick={vm.onTopicLinkClick}>
|
|
{content}
|
|
</Text>
|
|
<IconButton className="mx_RoomSummaryCard_topic_chevron" size="24px" onClick={vm.onExpandedClick}>
|
|
<ChevronDownIcon />
|
|
</IconButton>
|
|
</Box>
|
|
{vm.expanded && vm.canEditTopic && (
|
|
<Box flex="1" className="mx_RoomSummaryCard_topic_edit">
|
|
<Link kind="primary" onClick={vm.onEditClick}>
|
|
<Text size="sm" weight="regular">
|
|
{_t("action|edit")}
|
|
</Text>
|
|
</Link>
|
|
</Box>
|
|
)}
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
const RoomSummaryCardView: React.FC<IProps> = ({
|
|
room,
|
|
permalinkCreator,
|
|
onSearchChange,
|
|
onSearchCancel,
|
|
focusRoomSearch,
|
|
searchTerm = "",
|
|
}) => {
|
|
const vm = useRoomSummaryCardViewModel(room, permalinkCreator, onSearchCancel);
|
|
// XXX: this name should be part of the view model
|
|
const name = useRoomName(room);
|
|
|
|
// The search field is controlled and onSearchChange is debounced in RoomView,
|
|
// so we need to set the value of the input right away
|
|
const [searchValue, setSearchValue] = useState(searchTerm);
|
|
useEffect(() => {
|
|
setSearchValue(searchTerm);
|
|
}, [searchTerm]);
|
|
|
|
const roomInfo = (
|
|
<header className="mx_RoomSummaryCard_container">
|
|
<RoomAvatar room={room} size="80px" viewAvatarOnClick />
|
|
<Heading
|
|
as="h1"
|
|
size="md"
|
|
weight="semibold"
|
|
className="mx_RoomSummaryCard_roomName text-primary"
|
|
title={name}
|
|
>
|
|
{name}
|
|
</Heading>
|
|
{vm.userStatus && <StatusTextView status={vm.userStatus} />}
|
|
<Text
|
|
as="div"
|
|
size="sm"
|
|
weight="semibold"
|
|
className="mx_RoomSummaryCard_alias text-secondary"
|
|
title={vm.alias}
|
|
>
|
|
{vm.alias}
|
|
</Text>
|
|
|
|
<Flex
|
|
as="section"
|
|
justify="center"
|
|
gap="var(--cpd-space-2x)"
|
|
wrap="wrap"
|
|
className="mx_RoomSummaryCard_badges"
|
|
>
|
|
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
|
|
<Badge kind="blue">
|
|
<PublicIcon width="1rem" height="1rem" color="var(--cpd-color-icon-info-primary)" />
|
|
{_t("common|public_room")}
|
|
</Badge>
|
|
)}
|
|
|
|
{vm.isRoomEncrypted && vm.e2eStatus !== E2EStatus.Warning && (
|
|
<Badge kind="green">
|
|
<LockIcon width="1rem" height="1rem" />
|
|
{_t("common|encrypted")}
|
|
</Badge>
|
|
)}
|
|
|
|
{!vm.isRoomEncrypted && (
|
|
<Badge kind="blue">
|
|
<LockOffIcon width="1rem" height="1rem" color="var(--cpd-color-icon-info-primary)" />
|
|
{_t("common|unencrypted")}
|
|
</Badge>
|
|
)}
|
|
|
|
{vm.e2eStatus === E2EStatus.Warning && (
|
|
<Badge kind="red">
|
|
<ErrorSolidIcon width="1rem" height="1rem" />
|
|
{_t("common|not_trusted")}
|
|
</Badge>
|
|
)}
|
|
|
|
<HistoryVisibilityBadge historyVisibility={vm.historyVisibility} />
|
|
</Flex>
|
|
|
|
<RoomTopic room={room} />
|
|
</header>
|
|
);
|
|
|
|
const header = onSearchChange && (
|
|
<Form.Root className="mx_RoomSummaryCard_search" onSubmit={(e) => e.preventDefault()}>
|
|
<Search
|
|
placeholder={_t("room|search|placeholder")}
|
|
name="room_message_search"
|
|
onChange={(e) => {
|
|
setSearchValue(e.currentTarget.value);
|
|
onSearchChange(e.currentTarget.value);
|
|
}}
|
|
value={searchValue}
|
|
className="mx_no_textinput"
|
|
ref={vm.searchInputRef}
|
|
autoFocus={focusRoomSearch}
|
|
onKeyDown={vm.onUpdateSearchInput}
|
|
/>
|
|
</Form.Root>
|
|
);
|
|
|
|
return (
|
|
<BaseCard
|
|
id="room-summary-panel"
|
|
className="mx_RoomSummaryCard"
|
|
ariaLabelledBy="room-summary-panel-tab"
|
|
role="tabpanel"
|
|
header={header}
|
|
>
|
|
{roomInfo}
|
|
|
|
<Separator />
|
|
|
|
<div role="menubar" aria-orientation="vertical">
|
|
<ToggleMenuItem
|
|
Icon={FavouriteIcon}
|
|
label={_t("room|context_menu|favourite")}
|
|
checked={vm.isFavorite}
|
|
onSelect={vm.onFavoriteToggleClick}
|
|
/>
|
|
<MenuItem
|
|
Icon={UserAddIcon}
|
|
label={_t("action|invite")}
|
|
disabled={!vm.canInviteToState}
|
|
onSelect={vm.onInviteToRoomClick}
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<MenuItem Icon={UserProfileIcon} label={_t("common|people")} onSelect={vm.onRoomMembersClick} />
|
|
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
|
|
{!vm.isVideoRoom && (
|
|
<>
|
|
<MenuItem
|
|
Icon={PinIcon}
|
|
label={_t("right_panel|pinned_messages_button")}
|
|
onSelect={vm.onRoomPinsClick}
|
|
>
|
|
<Text as="span" size="sm">
|
|
{vm.pinCount}
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
Icon={FilesIcon}
|
|
label={_t("right_panel|files_button")}
|
|
onSelect={vm.onRoomFilesClick}
|
|
/>
|
|
<MenuItem
|
|
Icon={ExtensionsIcon}
|
|
label={_t("right_panel|extensions_button")}
|
|
onSelect={vm.onRoomExtensionsClick}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onSelect={vm.onShareRoomClick} />
|
|
|
|
{!vm.isVideoRoom && (
|
|
<>
|
|
<MenuItem
|
|
Icon={PollsIcon}
|
|
label={_t("right_panel|polls_button")}
|
|
onSelect={vm.onRoomPollHistoryClick}
|
|
/>
|
|
<MenuItem
|
|
Icon={ExportArchiveIcon}
|
|
label={_t("export_chat|title")}
|
|
onSelect={vm.onRoomExportClick}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={vm.onRoomSettingsClick} />
|
|
|
|
<Separator />
|
|
<div className="mx_RoomSummaryCard_bottomOptions">
|
|
<MenuItem
|
|
Icon={ErrorIcon}
|
|
kind="critical"
|
|
label={_t("action|report_room")}
|
|
onSelect={vm.onReportRoomClick}
|
|
/>
|
|
<MenuItem
|
|
className="mx_RoomSummaryCard_leave"
|
|
Icon={LeaveIcon}
|
|
kind="critical"
|
|
label={_t("action|leave_room")}
|
|
onSelect={vm.onLeaveRoomClick}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</BaseCard>
|
|
);
|
|
};
|
|
|
|
export default RoomSummaryCardView;
|