Files
ThreadNet-Web/src/components/views/rooms/RoomListHeader.tsx
T
Michael TelatynskiandGitHub 3bfb33fe48 Migrate the majority of TooltipTarget consumers to Compound Tooltip (#12086)
* Migrate the majority of TooltipTarget consumers to Compound Tooltip

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

* Fix tests

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

* Update snapshot

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

* Fix CSS stacking contexts for Dialogs & PersistedElement

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

* Switch to PersistedElement sharing a CSS stacking context for z-index to continue functioning

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

* Fix Widget PIP overlay being under the widget and dragging being broken

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

* Fix border-radius on widget pip

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

* Fix majority of tests

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

* Fix jest retryTimes applying outside of CI

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

* Fix remaining tests

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

* Fix React unique key warnings

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

* Fix sticker picker

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

* id not class

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

* Fix widget pip button colour in light theme

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-01-09 11:46:31 +00:00

435 lines
18 KiB
TypeScript

/*
Copyright 2021 - 2022 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 { EventType, RoomType, Room, RoomEvent, ClientEvent } from "matrix-js-sdk/src/matrix";
import React, { useContext, useEffect, useState } from "react";
import { Tooltip } from "@vector-im/compound-web";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { Action } from "../../../dispatcher/actions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { useDispatcher } from "../../../hooks/useDispatcher";
import { useEventEmitterState, useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { _t } from "../../../languageHandler";
import PosthogTrackers from "../../../PosthogTrackers";
import { UIComponent } from "../../../settings/UIFeature";
import {
getMetaSpaceName,
MetaSpace,
SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
shouldShowSpaceInvite,
showAddExistingRooms,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite,
} from "../../../utils/space";
import {
ChevronFace,
ContextMenuTooltipButton,
useContextMenu,
MenuProps,
ContextMenuButton,
} from "../../structures/ContextMenu";
import { BetaPill } from "../beta/BetaCard";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import InlineSpinner from "../elements/InlineSpinner";
import { HomeButtonContextMenu } from "../spaces/SpacePanel";
const contextMenuBelow = (elementRect: DOMRect): MenuProps => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.scrollX;
const top = elementRect.bottom + window.scrollY + 12;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
// Long-running actions that should trigger a spinner
enum PendingActionType {
JoinRoom,
BulkRedact,
}
const usePendingActions = (): Map<PendingActionType, Set<string>> => {
const cli = useContext(MatrixClientContext);
const [actions, setActions] = useState(new Map<PendingActionType, Set<string>>());
const addAction = (type: PendingActionType, key: string): void => {
const keys = new Set(actions.get(type));
keys.add(key);
setActions(new Map(actions).set(type, keys));
};
const removeAction = (type: PendingActionType, key: string): void => {
const keys = new Set(actions.get(type));
if (keys.delete(key)) {
setActions(new Map(actions).set(type, keys));
}
};
useDispatcher(defaultDispatcher, (payload) => {
switch (payload.action) {
case Action.JoinRoom:
addAction(PendingActionType.JoinRoom, payload.roomId);
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
removeAction(PendingActionType.JoinRoom, payload.roomId);
break;
case Action.BulkRedactStart:
addAction(PendingActionType.BulkRedact, payload.roomId);
break;
case Action.BulkRedactEnd:
removeAction(PendingActionType.BulkRedact, payload.roomId);
break;
}
});
useTypedEventEmitter(cli, ClientEvent.Room, (room: Room) => removeAction(PendingActionType.JoinRoom, room.roomId));
return actions;
};
interface IProps {
onVisibilityChange?(): void;
}
const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
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 videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const pendingActions = usePendingActions();
const canShowMainMenu = activeSpace || spaceKey === MetaSpace.Home;
useEffect(() => {
if (mainMenuDisplayed && !canShowMainMenu) {
// Space changed under us and we no longer has a main menu to draw
closeMainMenu();
}
}, [closeMainMenu, canShowMainMenu, mainMenuDisplayed]);
const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name);
useEffect(() => {
onVisibilityChange?.();
}, [onVisibilityChange]);
const canExploreRooms = shouldShowComponent(UIComponent.ExploreRooms);
const canCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
const canCreateSpaces = shouldShowComponent(UIComponent.CreateSpaces);
const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent(
EventType.SpaceChild,
cli.getUserId()!,
);
const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms;
const canAddSubSpaces = hasPermissionToAddSpaceChild && canCreateSpaces;
// If the user can't do anything on the plus menu, don't show it. This aims to target the
// plus menu shown on the Home tab primarily: the user has options to use the menu for
// communities and spaces, but is at risk of no options on the Home tab.
const canShowPlusMenu = canCreateRooms || canExploreRooms || canCreateSpaces || activeSpace;
let contextMenu: JSX.Element | undefined;
if (mainMenuDisplayed && mainMenuHandle.current) {
let ContextMenuComponent;
if (activeSpace) {
ContextMenuComponent = SpaceContextMenu;
} else {
ContextMenuComponent = HomeButtonContextMenu;
}
contextMenu = (
<ContextMenuComponent
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
space={activeSpace!}
onFinished={closeMainMenu}
hideHeader={true}
/>
);
} else if (plusMenuDisplayed && activeSpace) {
let inviteOption: JSX.Element | undefined;
if (shouldShowSpaceInvite(activeSpace)) {
inviteOption = (
<IconizedContextMenuOption
label={_t("action|invite")}
iconClassName="mx_RoomListHeader_iconInvite"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showSpaceInvite(activeSpace);
closePlusMenu();
}}
/>
);
}
let newRoomOptions: JSX.Element | undefined;
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()!)) {
newRoomOptions = (
<>
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewRoom"
label={_t("action|new_room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("action|new_video_room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewRoom(
activeSpace,
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
);
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</>
);
}
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current!.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{inviteOption}
{newRoomOptions}
<IconizedContextMenuOption
label={_t("action|explore_rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined, // other
});
closePlusMenu();
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
}}
/>
<IconizedContextMenuOption
label={_t("action|add_existing_room")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showAddExistingRooms(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubRooms}
tooltip={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined}
/>
{canCreateSpaces && (
<IconizedContextMenuOption
label={_t("room_list|add_space_label")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCreateNewSubspace(activeSpace);
closePlusMenu();
}}
disabled={!canAddSubSpaces}
tooltip={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
} else if (plusMenuDisplayed) {
let newRoomOpts: JSX.Element | undefined;
let joinRoomOpt: JSX.Element | undefined;
if (canCreateRooms) {
newRoomOpts = (
<>
<IconizedContextMenuOption
label={_t("action|start_new_chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_chat" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
closePlusMenu();
}}
/>
<IconizedContextMenuOption
label={_t("action|new_room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: "view_create_room" });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
closePlusMenu();
}}
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("action|new_video_room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({
action: "view_create_room",
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
});
closePlusMenu();
}}
>
<BetaPill />
</IconizedContextMenuOption>
)}
</>
);
}
if (canExploreRooms) {
joinRoomOpt = (
<IconizedContextMenuOption
label={_t("room_list|join_public_room_label")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory });
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
closePlusMenu();
}}
/>
);
}
contextMenu = (
<IconizedContextMenu
{...contextMenuBelow(plusMenuHandle.current!.getBoundingClientRect())}
onFinished={closePlusMenu}
compact
>
<IconizedContextMenuOptionList first>
{newRoomOpts}
{joinRoomOpt}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
let title: string;
if (activeSpace && spaceName) {
title = spaceName;
} else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
}
const pendingActionSummary = [...pendingActions.entries()]
.filter(([type, keys]) => keys.size > 0)
.map(([type, keys]) => {
switch (type) {
case PendingActionType.JoinRoom:
return _t("room_list|joining_rooms_status", { count: keys.size });
case PendingActionType.BulkRedact:
return _t("room_list|redacting_messages_status", { count: keys.size });
}
})
.join("\n");
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{title}</div>;
if (canShowMainMenu) {
const commonProps = {
ref: mainMenuHandle,
onClick: openMainMenu,
isExpanded: mainMenuDisplayed,
className: "mx_RoomListHeader_contextMenuButton",
children: title,
};
if (!!activeSpace) {
contextMenuButton = (
<ContextMenuButton
{...commonProps}
label={_t("room_list|space_menu_label", { spaceName: spaceName ?? activeSpace.name })}
/>
);
} else {
contextMenuButton = <ContextMenuTooltipButton {...commonProps} title={_t("room_list|home_menu_label")} />;
}
}
return (
<aside className="mx_RoomListHeader" aria-label={_t("room|context_menu|title")}>
{contextMenuButton}
{pendingActionSummary ? (
<Tooltip label={pendingActionSummary} isTriggerInteractive={false}>
<InlineSpinner />
</Tooltip>
) : null}
{canShowPlusMenu && (
<ContextMenuTooltipButton
ref={plusMenuHandle}
onClick={openPlusMenu}
isExpanded={plusMenuDisplayed}
className="mx_RoomListHeader_plusButton"
title={_t("action|add")}
/>
)}
{contextMenu}
</aside>
);
};
export default RoomListHeader;