element-portable/src/components/views/rooms/RoomListHeader.tsx

372 lines
15 KiB
TypeScript

/*
Copyright 2021 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, { ComponentProps, useContext, useEffect, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import { HomeButtonContextMenu } from "../spaces/SpacePanel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { shouldShowSpaceInvite, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { ButtonEvent } from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import ErrorDialog from "../dialogs/ErrorDialog";
import { showCommunityInviteDialog } from "../../../RoomInvite";
import { useDispatcher } from "../../../hooks/useDispatcher";
import InlineSpinner from "../elements/InlineSpinner";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import {
getMetaSpaceName,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import TooltipTarget from "../elements/TooltipTarget";
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset;
const top = elementRect.bottom + window.pageYOffset + 12;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
const PrototypeCommunityContextMenu = (props: ComponentProps<typeof SpaceContextMenu>) => {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let settingsOption;
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
const onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
props.onFinished();
};
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={onCommunitySettingsClick}
/>
);
}
const onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch({
action: Action.ViewRoom,
room_id: chat.roomId,
}, true);
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList }, undefined, chat.roomId);
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
props.onFinished();
};
return <IconizedContextMenu {...props} compact>
<IconizedContextMenuOptionList first>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={onCommunityMembersClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
const useJoiningRooms = (): Set<string> => {
const cli = useContext(MatrixClientContext);
const [joiningRooms, setJoiningRooms] = useState(new Set<string>());
useDispatcher(defaultDispatcher, payload => {
switch (payload.action) {
case Action.JoinRoom:
setJoiningRooms(new Set(joiningRooms.add(payload.roomId)));
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
if (joiningRooms.delete(payload.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
break;
}
});
useEventEmitter(cli, "Room", (room: Room) => {
if (joiningRooms.delete(room.roomId)) {
setJoiningRooms(new Set(joiningRooms));
}
});
return joiningRooms;
};
interface IProps {
spacePanelDisabled: boolean;
onVisibilityChange?(): void;
}
const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
const cli = useContext(MatrixClientContext);
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>(
SpaceStore.instance,
UPDATE_SELECTED_SPACE,
() => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom],
);
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
});
const joiningRooms = useJoiningRooms();
const count = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
return Object.values(RoomListStore.instance.orderedLists).flat(1).length;
} else {
return null;
}
});
const spaceName = useEventEmitterState(activeSpace, "Room.name", () => activeSpace?.name);
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count === "number") {
return <div className="mx_LeftPanel_roomListFilterCount">
{ _t("%(count)s results", { count }) }
</div>;
} else if (spacePanelDisabled) {
return null;
}
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let contextMenu: JSX.Element;
if (mainMenuDisplayed) {
let ContextMenuComponent;
if (activeSpace) {
ContextMenuComponent = SpaceContextMenu;
} else if (communityId) {
ContextMenuComponent = PrototypeCommunityContextMenu;
} else {
ContextMenuComponent = HomeButtonContextMenu;
}
contextMenu = <ContextMenuComponent
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
space={activeSpace}
onFinished={closeMainMenu}
hideHeader={true}
/>;
} else if (plusMenuDisplayed && activeSpace) {
let inviteOption: JSX.Element;
if (shouldShowSpaceInvite(activeSpace)) {
inviteOption = <IconizedContextMenuOption
label={_t("Invite")}
iconClassName="mx_RoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showSpaceInvite(activeSpace);
closePlusMenu();
}}
/>;
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconInvite"
label={_t("Invite")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
closePlusMenu();
}}
/>;
}
let createNewRoomOption: JSX.Element;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
createNewRoomOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconCreateRoom"
label={_t("Create new room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace);
closePlusMenu();
}}
/>;
}
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
{ createNewRoomOption }
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
} else if (plusMenuDisplayed) {
contextMenu = <IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Start new chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomListHeader_iconCreateRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("Join public room")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
closePlusMenu();
}}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
let title: string;
if (activeSpace) {
title = spaceName;
} else if (communityId) {
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
} else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
}
let pendingRoomJoinSpinner: JSX.Element;
if (joiningRooms.size) {
pendingRoomJoinSpinner = <TooltipTarget
label={_t("Currently joining %(count)s rooms", { count: joiningRooms.size })}
>
<InlineSpinner />
</TooltipTarget>;
}
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
if (activeSpace || spaceKey === MetaSpace.Home) {
contextMenuButton = <ContextMenuTooltipButton
inputRef={mainMenuHandle}
onClick={openMainMenu}
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName })
: _t("Home options")}
>
{ title }
</ContextMenuTooltipButton>;
}
return <div className="mx_RoomListHeader">
{ contextMenuButton }
{ pendingRoomJoinSpinner }
<ContextMenuTooltipButton
inputRef={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("Add")}
/>
{ contextMenu }
</div>;
};
export default RoomListHeader;