Files
ThreadNet-Web/src/components/views/right_panel/RoomSummaryCard.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

596 lines
23 KiB
TypeScript
Raw Normal View History

2020-09-08 08:48:03 +01:00
/*
Copyright 2020 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, { SyntheticEvent, useCallback, useContext, useEffect, useMemo, useState } from "react";
2020-09-08 08:48:03 +01:00
import classNames from "classnames";
import {
MenuItem,
Tooltip,
Separator,
ToggleMenuItem,
Text,
Badge,
Heading,
IconButton,
Link,
} from "@vector-im/compound-web";
2023-10-20 14:30:37 +01:00
import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.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 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 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 LeaveIcon } from "@vector-im/compound-design-tokens/icons/leave.svg";
import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg";
import { Icon as PollsIcon } from "@vector-im/compound-design-tokens/icons/polls.svg";
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin.svg";
import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock-solid.svg";
2023-11-11 07:24:48 +00:00
import { Icon as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
import { Icon as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
2020-09-08 08:48:03 +01:00
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard, { Group } from "./BaseCard";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
2020-09-08 08:48:03 +01:00
import defaultDispatcher from "../../../dispatcher/dispatcher";
2022-01-05 16:14:44 +01:00
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
2020-09-08 08:48:03 +01:00
import Modal from "../../../Modal";
import ShareDialog from "../dialogs/ShareDialog";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
2020-09-08 08:48:03 +01:00
import WidgetUtils from "../../../utils/WidgetUtils";
2021-06-29 13:11:58 +01:00
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
2020-09-08 08:48:03 +01:00
import SettingsStore from "../../../settings/SettingsStore";
2020-10-12 13:16:55 +01:00
import WidgetAvatar from "../avatars/WidgetAvatar";
2021-06-29 13:11:58 +01:00
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
2020-09-11 09:04:06 -06:00
import { E2EStatus } from "../../../utils/ShieldUtils";
2023-02-28 09:39:55 +13:00
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
2020-09-11 09:04:06 -06:00
import RoomContext from "../../../contexts/RoomContext";
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
2021-06-29 13:11:58 +01:00
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "./PinnedMessagesCard";
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import RoomName from "../elements/RoomName";
import UIStore from "../../../stores/UIStore";
import ExportDialog from "../dialogs/ExportDialog";
2022-01-05 16:14:44 +01:00
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import PosthogTrackers from "../../../PosthogTrackers";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
2023-10-20 14:30:37 +01:00
import { Flex } from "../../utils/Flex";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { tagRoom } from "../../../utils/room/tagRoom";
import { canInviteTo } from "../../../utils/room/canInviteTo";
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
2023-11-11 07:24:48 +00:00
import { useAccountData } from "../../../hooks/useAccountData";
import { useRoomState } from "../../../hooks/useRoomState";
import { useTopic } from "../../../hooks/room/useTopic";
import { Linkify, topicToHtml } from "../../../HtmlUtils";
import { Box } from "../../utils/Box";
import { onRoomTopicLinkClick } from "../elements/RoomTopic";
2020-09-08 08:48:03 +01:00
interface IProps {
room: Room;
2023-02-28 09:39:55 +13:00
permalinkCreator: RoomPermalinkCreator;
2020-09-08 08:48:03 +01:00
onClose(): void;
2023-09-01 10:45:50 +01:00
onSearchClick?: () => void;
2020-09-08 08:48:03 +01:00
}
interface IAppsSectionProps {
room: Room;
}
export const useWidgets = (room: Room): IApp[] => {
const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
2020-09-08 08:48:03 +01:00
const updateApps = useCallback(() => {
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
2020-10-15 11:14:48 +01:00
setApps([...WidgetStore.instance.getApps(room.roomId)]);
2020-09-08 08:48:03 +01:00
}, [room]);
2021-01-20 13:49:15 +00:00
useEffect(updateApps, [room, updateApps]);
2020-09-08 08:48:03 +01:00
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
2020-09-08 08:48:03 +01:00
return apps;
};
interface IAppRowProps {
app: IApp;
room: Room;
}
const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
const name = WidgetUtils.getWidgetName(app);
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
const subtitle = dataTitle && " - " + dataTitle;
const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
useEffect(() => {
setCanModifyWidget(WidgetUtils.canUserModifyWidgets(room.client, room.roomId));
}, [room.client, room.roomId]);
const onOpenWidgetClick = (): void => {
RightPanelStore.instance.pushCard({
phase: RightPanelPhases.Widget,
2022-01-05 16:14:44 +01:00
state: { widgetId: app.id },
});
};
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
const togglePin = isPinned
? () => {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
}
: () => {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
};
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
let contextMenu;
if (menuDisplayed) {
const rect = handle.current?.getBoundingClientRect();
const rightMargin = rect?.right ?? 0;
const topMargin = rect?.top ?? 0;
2020-10-12 09:51:49 +01:00
contextMenu = (
<WidgetContextMenu
chevronFace={ChevronFace.None}
right={UIStore.instance.windowWidth - rightMargin}
bottom={UIStore.instance.windowHeight - topMargin}
onFinished={closeMenu}
app={app}
/>
);
}
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
2020-10-12 09:51:49 +01:00
let pinTitle: string;
if (cannotPin) {
pinTitle = _t("right_panel|pinned_messages|limits", { count: MAX_PINNED });
2020-10-12 09:51:49 +01:00
} else {
pinTitle = isPinned ? _t("action|unpin") : _t("action|pin");
2020-10-12 09:51:49 +01:00
}
2021-11-16 15:43:18 +01:00
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
const toggleMaximised = isMaximised
? () => {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
}
: () => {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
};
const maximiseTitle = isMaximised ? _t("action|close") : _t("action|maximise");
2021-11-16 15:43:18 +01:00
let openTitle = "";
if (isPinned) {
openTitle = _t("widget|unpin_to_view_right_panel");
2021-11-16 15:43:18 +01:00
} else if (isMaximised) {
openTitle = _t("widget|close_to_view_right_panel");
2021-11-16 15:43:18 +01:00
}
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
mx_RoomSummaryCard_Button_pinned: isPinned,
mx_RoomSummaryCard_Button_maximised: isMaximised,
});
2020-10-15 11:14:48 +01:00
return (
<div className={classes} ref={handle}>
<AccessibleButton
2020-10-15 11:14:48 +01:00
className="mx_RoomSummaryCard_icon_app"
onClick={onOpenWidgetClick}
// only show a tooltip if the widget is pinned
title={!(isPinned || isMaximised) ? undefined : openTitle}
2021-11-16 15:43:18 +01:00
disabled={isPinned || isMaximised}
>
<WidgetAvatar app={app} size="20px" />
<span>{name}</span>
{subtitle}
</AccessibleButton>
{canModifyWidget && (
<ContextMenuTooltipButton
className="mx_RoomSummaryCard_app_options"
isExpanded={menuDisplayed}
onClick={openMenu}
title={_t("common|options")}
/>
)}
2020-10-15 11:14:48 +01:00
<AccessibleButton
2020-10-15 11:14:48 +01:00
className="mx_RoomSummaryCard_app_pinToggle"
onClick={togglePin}
title={pinTitle}
disabled={cannotPin}
/>
<AccessibleButton
className="mx_RoomSummaryCard_app_maximiseToggle"
2021-11-16 15:43:18 +01:00
onClick={toggleMaximised}
title={maximiseTitle}
/>
{contextMenu}
</div>
);
};
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
2020-09-08 08:48:03 +01:00
const apps = useWidgets(room);
// Filter out virtual widgets
const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]);
2020-09-08 08:48:03 +01:00
const onManageIntegrations = (): void => {
2020-09-08 08:48:03 +01:00
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
2022-05-06 12:46:26 -06:00
// noinspection JSIgnoredPromiseFromCall
managers.getPrimaryManager()?.open(room);
2020-09-08 08:48:03 +01:00
}
};
let copyLayoutBtn: JSX.Element | null = null;
if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
copyLayoutBtn = (
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
{_t("widget|set_room_layout")}
</AccessibleButton>
);
}
2020-09-18 16:22:35 +01:00
return (
<Group className="mx_RoomSummaryCard_appsGroup" title={_t("right_panel|widgets_section")}>
{realApps.map((app) => (
<AppRow key={app.id} app={app} room={room} />
))}
{copyLayoutBtn}
2020-09-08 08:48:03 +01:00
<AccessibleButton kind="link" onClick={onManageIntegrations}>
{realApps.length > 0 ? _t("right_panel|edit_integrations") : _t("right_panel|add_integrations")}
2020-09-08 08:48:03 +01:00
</AccessibleButton>
</Group>
);
};
const onRoomFilesClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
2020-09-08 08:48:03 +01:00
};
const onRoomPinsClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true);
};
const onRoomSettingsClick = (ev: Event): void => {
2020-09-08 08:48:03 +01:00
defaultDispatcher.dispatch({ action: "open_room_settings" });
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev);
2020-09-08 08:48:03 +01:00
};
const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null => {
const [expanded, setExpanded] = useState(false);
const topic = useTopic(room);
const body = topicToHtml(topic?.text, topic?.html);
const onEditClick = (e: SyntheticEvent): void => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "open_room_settings" });
};
if (!body) {
return (
<Flex
as="section"
direction="column"
justify="center"
gap="var(--cpd-space-2x)"
className="mx_RoomSummaryCard_topic"
>
<Box flex="1">
<Link kind="primary" onClick={onEditClick}>
<Text size="sm" weight="regular">
{_t("right_panel|add_topic")}
</Text>
</Link>
</Box>
</Flex>
);
}
const content = expanded ? <Linkify>{body}</Linkify> : body;
return (
<Flex
as="section"
direction="column"
justify="center"
gap="var(--cpd-space-2x)"
className={classNames("mx_RoomSummaryCard_topic", {
mx_RoomSummaryCard_topic_collapsed: !expanded,
})}
>
<Box flex="1" className="mx_RoomSummaryCard_topic_container">
<Text
size="sm"
weight="regular"
onClick={(ev: React.MouseEvent): void => {
if (ev.target instanceof HTMLAnchorElement) {
onRoomTopicLinkClick(ev);
return;
}
setExpanded(!expanded);
}}
>
{content}
</Text>
<IconButton
className="mx_RoomSummaryCard_topic_chevron"
size="24px"
onClick={() => setExpanded(!expanded)}
>
<ChevronDownIcon />
</IconButton>
</Box>
{expanded && (
<Box flex="1" className="mx_RoomSummaryCard_topic_edit">
<Link kind="primary" onClick={onEditClick}>
<Text size="sm" weight="regular">
{_t("action|edit")}
</Text>
</Link>
</Box>
)}
</Flex>
);
};
2023-09-01 10:45:50 +01:00
const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, onSearchClick }) => {
2020-09-08 08:48:03 +01:00
const cli = useContext(MatrixClientContext);
const onShareRoomClick = (): void => {
2022-06-14 17:51:51 +01:00
Modal.createDialog(ShareDialog, {
2020-09-08 08:48:03 +01:00
target: room,
});
};
const onRoomExportClick = async (): Promise<void> => {
2022-06-14 17:51:51 +01:00
Modal.createDialog(ExportDialog, {
room,
});
};
2023-02-02 09:57:48 +13:00
const onRoomPollHistoryClick = (): void => {
Modal.createDialog(PollHistoryDialog, {
2023-02-28 09:39:55 +13:00
room,
matrixClient: cli,
2023-02-28 09:39:55 +13:00
permalinkCreator,
2023-02-02 09:57:48 +13:00
});
};
const onLeaveRoomClick = (): void => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
};
const onRoomMembersClick = (ev: Event): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
};
2020-09-08 08:48:03 +01:00
const isRoomEncrypted = useIsEncrypted(cli, room);
2020-09-11 09:04:06 -06:00
const roomContext = useContext(RoomContext);
const e2eStatus = roomContext.e2eStatus;
2022-09-16 11:12:27 -04:00
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom =
videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()));
2023-11-11 07:24:48 +00:00
const roomState = useRoomState(room);
const directRoomsList = useAccountData<Record<string, string[]>>(room.client, EventType.Direct);
const [isDirectMessage, setDirectMessage] = useState(false);
useEffect(() => {
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
if (dmRoomList.includes(room?.roomId ?? "")) {
setDirectMessage(true);
break;
}
}
}, [room, directRoomsList]);
2020-09-08 08:48:03 +01:00
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const header = (
2023-10-20 14:30:37 +01:00
<header className="mx_RoomSummaryCard_container">
2023-11-11 07:24:48 +00:00
<RoomAvatar room={room} size="80px" viewAvatarOnClick />
<RoomName room={room}>
{(name) => (
2023-11-11 07:24:48 +00:00
<Heading
as="h1"
size="md"
weight="semibold"
className="mx_RoomSummaryCard_roomName text-primary"
title={name}
>
{name}
2023-11-11 07:24:48 +00:00
</Heading>
)}
</RoomName>
2023-11-11 07:24:48 +00:00
<Text
as="div"
size="sm"
weight="semibold"
className="mx_RoomSummaryCard_alias text-secondary"
title={alias}
>
2020-09-08 08:48:03 +01:00
{alias}
2023-11-11 07:24:48 +00:00
</Text>
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
{!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && (
<Badge kind="default">
<PublicIcon width="1em" />
{_t("common|public_room")}
</Badge>
)}
{isRoomEncrypted && e2eStatus !== E2EStatus.Warning && (
<Badge kind="success">
<LockIcon width="1em" />
{_t("common|encrypted")}
</Badge>
)}
{!e2eStatus && (
<Badge kind="default">
<LockOffIcon width="1em" />
{_t("common|unencrypted")}
</Badge>
)}
{e2eStatus === E2EStatus.Warning && (
<Badge kind="critical">
<ErrorIcon width="1em" />
{_t("common|not_trusted")}
</Badge>
)}
</Flex>
<RoomTopic room={room} />
2023-10-20 14:30:37 +01:00
</header>
2020-09-08 08:48:03 +01:00
);
const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
2020-09-08 08:48:03 +01:00
return (
2023-10-20 14:30:37 +01:00
<BaseCard header={null} className="mx_RoomSummaryCard" onClose={onClose}>
<Flex
as="header"
className="mx_RoomSummaryCard_header"
gap="var(--cpd-space-3x)"
align="center"
justify="space-between"
>
2024-04-12 14:56:23 +02:00
<Tooltip label={_t("action|search")} placement="right">
2023-10-20 14:30:37 +01:00
<button
className="mx_RoomSummaryCard_searchBtn"
data-testid="summary-search"
onClick={() => {
onSearchClick?.();
}}
aria-label={_t("action|search")}
>
<SearchIcon width="100%" height="100%" />
</button>
</Tooltip>
<AccessibleButton
data-testid="base-card-close-button"
className="mx_BaseCard_close"
onClick={onClose}
title={_t("action|close")}
/>
</Flex>
{header}
<Separator />
<div role="menubar" aria-orientation="vertical">
<ToggleMenuItem
Icon={FavouriteIcon}
label={_t("room|context_menu|favourite")}
checked={isFavorite}
onChange={() => tagRoom(room, DefaultTagID.Favourite)}
// XXX: https://github.com/element-hq/compound/issues/288
onSelect={() => {}}
/>
<MenuItem
Icon={UserAddIcon}
label={_t("action|invite")}
disabled={!canInviteToState}
onSelect={() => inviteToRoom(room)}
/>
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onSelect={onShareRoomClick} />
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onSelect={onRoomSettingsClick} />
<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 && (
<>
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onSelect={onRoomFilesClick} />
<MenuItem
Icon={PollsIcon}
label={_t("right_panel|polls_button")}
onSelect={onRoomPollHistoryClick}
/>
{pinningEnabled && (
<MenuItem
Icon={PinIcon}
label={_t("right_panel|pinned_messages_button")}
onSelect={onRoomPinsClick}
>
<Text as="span" size="sm">
{pinCount}
</Text>
</MenuItem>
)}
<MenuItem
Icon={ExportArchiveIcon}
label={_t("export_chat|title")}
onSelect={onRoomExportClick}
/>
</>
)}
<Separator />
<MenuItem
Icon={LeaveIcon}
kind="critical"
label={_t("action|leave_room")}
onSelect={onLeaveRoomClick}
/>
</div>
2020-09-08 08:48:03 +01:00
{SettingsStore.getValue(UIFeature.Widgets) &&
!isVideoRoom &&
shouldShowComponent(UIComponent.AddIntegrations) && <AppsSection room={room} />}
2020-09-08 08:48:03 +01:00
</BaseCard>
);
};
export default RoomSummaryCard;