element-portable/src/components/views/right_panel/RoomSummaryCard.tsx

341 lines
13 KiB
TypeScript

/*
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, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
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, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import Modal from "../../../Modal";
import ShareDialog from '../dialogs/ShareDialog';
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import WidgetUtils from "../../../utils/WidgetUtils";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import TextWithTooltip from "../elements/TextWithTooltip";
import WidgetAvatar from "../avatars/WidgetAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext";
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
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";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import PosthogTrackers from "../../../PosthogTrackers";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
interface IProps {
room: Room;
onClose(): void;
}
interface IAppsSectionProps {
room: Room;
}
interface IButtonProps {
className: string;
onClick(ev: ButtonEvent): void;
}
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
return <AccessibleButton
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
onClick={onClick}
>
{ children }
</AccessibleButton>;
};
export const useWidgets = (room: Room) => {
const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
const updateApps = useCallback(() => {
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
setApps([...WidgetStore.instance.getApps(room.roomId)]);
}, [room]);
useEffect(updateApps, [room, updateApps]);
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
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.roomId));
}, [room.roomId]);
const onOpenWidgetClick = () => {
RightPanelStore.instance.pushCard({
phase: RightPanelPhases.Widget,
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();
contextMenu = <WidgetContextMenu
chevronFace={ChevronFace.None}
right={UIStore.instance.windowWidth - rect.right}
bottom={UIStore.instance.windowHeight - rect.top}
onFinished={closeMenu}
app={app}
/>;
}
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
let pinTitle: string;
if (cannotPin) {
pinTitle = _t("You can only pin up to %(count)s widgets", { count: MAX_PINNED });
} else {
pinTitle = isPinned ? _t("Unpin") : _t("Pin");
}
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("Close") : _t("Maximise");
let openTitle = "";
if (isPinned) {
openTitle = _t("Unpin this widget to view it in this panel");
} else if (isMaximised) {
openTitle =_t("Close this widget to view it in this panel");
}
const classes = classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", {
mx_RoomSummaryCard_Button_pinned: isPinned,
mx_RoomSummaryCard_Button_maximised: isMaximised,
});
return <div className={classes} ref={handle}>
<AccessibleTooltipButton
className="mx_RoomSummaryCard_icon_app"
onClick={onOpenWidgetClick}
// only show a tooltip if the widget is pinned
title={openTitle}
forceHide={!(isPinned || isMaximised)}
disabled={isPinned || isMaximised}
>
<WidgetAvatar app={app} />
<span>{ name }</span>
{ subtitle }
</AccessibleTooltipButton>
{ canModifyWidget && <ContextMenuTooltipButton
className="mx_RoomSummaryCard_app_options"
isExpanded={menuDisplayed}
onClick={openMenu}
title={_t("Options")}
/> }
<AccessibleTooltipButton
className="mx_RoomSummaryCard_app_pinToggle"
onClick={togglePin}
title={pinTitle}
disabled={cannotPin}
/>
<AccessibleTooltipButton
className="mx_RoomSummaryCard_app_maximiseToggle"
onClick={toggleMaximised}
title={maximiseTitle}
/>
{ contextMenu }
</div>;
};
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
const apps = useWidgets(room);
// Filter out virtual widgets
const realApps = useMemo(() => apps.filter(app => app.eventId !== undefined), [apps]);
const onManageIntegrations = () => {
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
// noinspection JSIgnoredPromiseFromCall
managers.getPrimaryManager().open(room);
}
};
let copyLayoutBtn: JSX.Element | null = null;
if (realApps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
copyLayoutBtn = (
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
{ _t("Set my room layout for everyone") }
</AccessibleButton>
);
}
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
{ realApps.map(app => <AppRow key={app.id} app={app} room={room} />) }
{ copyLayoutBtn }
<AccessibleButton kind="link" onClick={onManageIntegrations}>
{ realApps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
</AccessibleButton>
</Group>;
};
const onRoomMembersClick = (ev: ButtonEvent) => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
};
const onRoomFilesClick = () => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
};
const onRoomPinsClick = () => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, true);
};
const onRoomSettingsClick = (ev: ButtonEvent) => {
defaultDispatcher.dispatch({ action: "open_room_settings" });
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoSettingsButton", ev);
};
const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
const cli = useContext(MatrixClientContext);
const onShareRoomClick = () => {
Modal.createDialog(ShareDialog, {
target: room,
});
};
const onRoomExportClick = async () => {
Modal.createDialog(ExportDialog, {
room,
});
};
const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useContext(RoomContext);
const e2eStatus = roomContext.e2eStatus;
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const isVideoRoom = videoRoomsEnabled && (
room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())
);
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const header = <React.Fragment>
<div className="mx_RoomSummaryCard_avatar" role="presentation">
<RoomAvatar room={room} height={54} width={54} viewAvatarOnClick />
<TextWithTooltip
tooltip={isRoomEncrypted ? _t("Encrypted") : _t("Not encrypted")}
class={classNames("mx_RoomSummaryCard_e2ee", {
mx_RoomSummaryCard_e2ee_normal: isRoomEncrypted,
mx_RoomSummaryCard_e2ee_warning: isRoomEncrypted && e2eStatus === E2EStatus.Warning,
mx_RoomSummaryCard_e2ee_verified: isRoomEncrypted && e2eStatus === E2EStatus.Verified,
})}
/>
</div>
<RoomName room={room}>
{ name => (
<h2 title={name}>
{ name }
</h2>
) }
</RoomName>
<div className="mx_RoomSummaryCard_alias" title={alias}>
{ alias }
</div>
</React.Fragment>;
const memberCount = useRoomMemberCount(room);
const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
{ _t("People") }
<span className="mx_BaseCard_Button_sublabel">
{ memberCount }
</span>
</Button>
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
{ _t("Files") }
</Button> }
{ pinningEnabled && !isVideoRoom &&
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
{ _t("Pinned") }
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
{ pinCount }
</span> }
</Button> }
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
{ _t("Export chat") }
</Button> }
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
{ _t("Share room") }
</Button>
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
{ _t("Room settings") }
</Button>
</Group>
{
SettingsStore.getValue(UIFeature.Widgets)
&& !isVideoRoom
&& shouldShowComponent(UIComponent.AddIntegrations)
&& <AppsSection room={room} />
}
</BaseCard>;
};
export default RoomSummaryCard;