/* Copyright 2024 New Vector Ltd. Copyright 2021, 2022 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 { ClientEvent, EventType, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; import React, { type JSX, 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 { type 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, type 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, ContextMenuButton, ContextMenuTooltipButton, type MenuProps, useContextMenu, } 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> => { const cli = useContext(MatrixClientContext); const [actions, setActions] = useState(new Map>()); 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 LegacyRoomListHeader: React.FC = ({ onVisibilityChange }) => { const cli = useContext(MatrixClientContext); const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu(); const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu(); 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 = ( ); } else if (plusMenuDisplayed && activeSpace) { let inviteOption: JSX.Element | undefined; if (shouldShowSpaceInvite(activeSpace)) { inviteOption = ( { e.preventDefault(); e.stopPropagation(); showSpaceInvite(activeSpace); closePlusMenu(); }} /> ); } let newRoomOptions: JSX.Element | undefined; if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()!)) { newRoomOptions = ( <> { e.preventDefault(); e.stopPropagation(); showCreateNewRoom(activeSpace); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); closePlusMenu(); }} /> {videoRoomsEnabled && ( { e.preventDefault(); e.stopPropagation(); showCreateNewRoom( activeSpace, elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo, ); closePlusMenu(); }} > )} ); } contextMenu = ( {inviteOption} {newRoomOptions} { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: activeSpace.roomId, metricsTrigger: undefined, // other }); closePlusMenu(); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e); }} /> { e.preventDefault(); e.stopPropagation(); showAddExistingRooms(activeSpace); closePlusMenu(); }} disabled={!canAddSubRooms} title={!canAddSubRooms ? _t("spaces|error_no_permission_add_room") : undefined} /> {canCreateSpaces && ( { e.preventDefault(); e.stopPropagation(); showCreateNewSubspace(activeSpace); closePlusMenu(); }} disabled={!canAddSubSpaces} title={!canAddSubSpaces ? _t("spaces|error_no_permission_add_space") : undefined} > )} ); } else if (plusMenuDisplayed) { let newRoomOpts: JSX.Element | undefined; let joinRoomOpt: JSX.Element | undefined; if (canCreateRooms) { newRoomOpts = ( <> { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: Action.CreateChat }); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e); closePlusMenu(); }} /> { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: Action.CreateRoom }); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); closePlusMenu(); }} /> {videoRoomsEnabled && ( { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: Action.CreateRoom, type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo, }); closePlusMenu(); }} > )} ); } if (canExploreRooms) { joinRoomOpt = ( { e.preventDefault(); e.stopPropagation(); defaultDispatcher.dispatch({ action: Action.ViewRoomDirectory }); PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e); closePlusMenu(); }} /> ); } contextMenu = ( {newRoomOpts} {joinRoomOpt} ); } 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 =
{title}
; if (canShowMainMenu) { const commonProps = { ref: mainMenuHandle, onClick: openMainMenu, isExpanded: mainMenuDisplayed, className: "mx_LegacyRoomListHeader_contextMenuButton", children: title, }; if (!!activeSpace) { contextMenuButton = ( ); } else { contextMenuButton = ; } } return ( ); }; export default LegacyRoomListHeader;