/* Copyright 2026 Element Creations Ltd. 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 React, { type ComponentProps, type Dispatch, type ReactNode, type RefCallback, type SetStateAction, type JSX, useCallback, useEffect, useLayoutEffect, useRef, useState, useContext, } from "react"; import { DragDropContext, Draggable, Droppable, type DroppableProvidedProps } from "react-beautiful-dnd"; import classNames from "classnames"; import { type Room } from "matrix-js-sdk/src/matrix"; import { HomeSolidIcon, RoomIcon, VideoCallSolidIcon, PlusIcon, ChevronRightIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { useCreateAutoDisposedViewModel, UserMenu } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import { useContextMenu } from "../../structures/ContextMenu"; import SpaceCreateMenu from "./SpaceCreateMenu"; import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter"; import { getMetaSpaceName, MetaSpace, type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/spaces"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { UPDATE_STATUS_INDICATOR } from "../../../stores/notifications/RoomNotificationStateStore"; import type SpaceContextMenu from "../context_menus/SpaceContextMenu"; import IconizedContextMenu, { IconizedContextMenuCheckbox, IconizedContextMenuOptionList, } from "../context_menus/IconizedContextMenu"; import SettingsStore from "../../../settings/SettingsStore"; import { SettingLevel } from "../../../settings/SettingLevel"; import UIStore from "../../../stores/UIStore"; import QuickSettingsButton from "./QuickSettingsButton"; import { useSettingValue } from "../../../hooks/useSettings"; import IndicatorScrollbar from "../../structures/IndicatorScrollbar"; import { useDispatcher } from "../../../hooks/useDispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { type ActionPayload } from "../../../dispatcher/payloads"; import { Action } from "../../../dispatcher/actions"; import { type NotificationState } from "../../../stores/notifications/NotificationState"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { ThreadsActivityCentre } from "./threads-activity-centre/"; import AccessibleButton from "../elements/AccessibleButton"; import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation"; import { KeyboardShortcut } from "../settings/KeyboardShortcut"; import { ModuleApi } from "../../../modules/Api.ts"; import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts"; import { UserMenuViewModel } from "../../../viewmodels/menus/UserMenuViewModel.ts"; import { SDKContext } from "../../../contexts/SDKContext.ts"; import { OwnProfileStore } from "../../../stores/OwnProfileStore.ts"; import { type SDKContextClass } from "../../../contexts/SDKContextClass.ts"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { const sdkContext = useContext(SDKContext); const invites = useEventEmitterState(sdkContext.spaceStore, UPDATE_INVITED_SPACES, () => { return sdkContext.spaceStore.invitedSpaces; }); const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>( sdkContext.spaceStore, UPDATE_TOP_LEVEL_SPACES, () => [sdkContext.spaceStore.enabledMetaSpaces, sdkContext.spaceStore.spacePanelSpaces], ); const activeSpace = useEventEmitterState(sdkContext.spaceStore, UPDATE_SELECTED_SPACE, () => { return sdkContext.spaceStore.activeSpace; }); return [invites, metaSpaces, actualSpaces, activeSpace]; }; export const HomeButtonContextMenu: React.FC> = ({ onFinished, hideHeader, ...props }) => { const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); return ( {!hideHeader &&
{_t("common|home")}
} { onFinished(); SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome); }} />
); }; interface IMetaSpaceButtonProps extends ComponentProps { selected: boolean; isPanelCollapsed: boolean; icon: JSX.Element; } type MetaSpaceButtonProps = Pick; const MetaSpaceButton: React.FC = ({ selected, isPanelCollapsed, size = "32px", ...props }) => { return (
  • ); }; const getHomeNotificationState = (sdkContext: SDKContextClass): NotificationState => { return sdkContext.spaceStore.allRoomsInHome ? sdkContext.roomNotificationStateStore.globalState : sdkContext.spaceStore.getNotificationState(MetaSpace.Home); }; const HomeButton: React.FC = ({ selected, isPanelCollapsed }) => { const sdkContext = useContext(SDKContext); const allRoomsInHome = useEventEmitterState(sdkContext.spaceStore, UPDATE_HOME_BEHAVIOUR, () => { return sdkContext.spaceStore.allRoomsInHome; }); const [notificationState, setNotificationState] = useState(getHomeNotificationState(sdkContext)); const updateNotificationState = useCallback(() => { setNotificationState(getHomeNotificationState(sdkContext)); }, [sdkContext]); useEffect(updateNotificationState, [updateNotificationState, allRoomsInHome]); useEventEmitter(sdkContext.roomNotificationStateStore, UPDATE_STATUS_INDICATOR, updateNotificationState); return ( } /> ); }; const OrphansButton: React.FC = ({ selected, isPanelCollapsed }) => { const sdkContext = useContext(SDKContext); return ( } /> ); }; const VideoRoomsButton: React.FC = ({ selected, isPanelCollapsed }) => { const sdkContext = useContext(SDKContext); return ( } /> ); }; const CreateSpaceButton: React.FC> = ({ isPanelCollapsed, setPanelCollapsed, }) => { const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); useEffect(() => { if (!isPanelCollapsed && menuDisplayed) { closeMenu(); } }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps let contextMenu: JSX.Element | undefined; if (menuDisplayed) { contextMenu = ; } const onNewClick = menuDisplayed ? closeMenu : () => { if (!isPanelCollapsed) setPanelCollapsed(true); openMenu(); }; return (
  • } /> {contextMenu}
  • ); }; const metaSpaceComponentMap: Record = { [MetaSpace.Home]: HomeButton, [MetaSpace.Orphans]: OrphansButton, [MetaSpace.VideoRooms]: VideoRoomsButton, }; interface IInnerSpacePanelProps extends DroppableProvidedProps { children?: ReactNode; isPanelCollapsed: boolean; setPanelCollapsed: Dispatch>; isDraggingOver: boolean; innerRef: RefCallback; } // Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation const InnerSpacePanel = React.memo( ({ children, isPanelCollapsed, setPanelCollapsed, isDraggingOver, innerRef, ...props }) => { const sdkContext = useContext(SDKContext); const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; const moduleSpaceItems = useModuleSpacePanelItems(ModuleApi.instance.extras); const metaSpacesSection = metaSpaces .filter((key) => !(key === MetaSpace.VideoRooms && !SettingsStore.getValue("feature_video_rooms"))) .map((key) => { const Component = metaSpaceComponentMap[key]; return ; }); return ( {metaSpacesSection} {invites.map((s) => ( setPanelCollapsed(false)} /> ))} {actualSpaces.map((s, i) => ( {(provided, snapshot) => ( setPanelCollapsed(false)} /> )} ))} {children} {moduleSpaceItems.map((item) => (
  • { sdkContext.spaceStore.setActiveSpace(item.spaceKey); item.onSelected?.(); }} />
  • ))} {shouldShowComponent(UIComponent.CreateSpaces) && ( )}
    ); }, ); const SpacePanel: React.FC = () => { const sdkContext = useContext(SDKContext); const client = sdkContext.client!; const [dragging, setDragging] = useState(false); const [isPanelCollapsed, setPanelCollapsed] = useState(true); const ref = useRef(null); useLayoutEffect(() => { if (ref.current) UIStore.instance.trackElementDimensions("SpacePanel", ref.current); return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel"); }, []); useDispatcher(defaultDispatcher, (payload: ActionPayload) => { if (payload.action === Action.ToggleSpacePanel) { setPanelCollapsed(!isPanelCollapsed); } }); const userMenuVm = useCreateAutoDisposedViewModel( () => new UserMenuViewModel( { ownProfileStore: OwnProfileStore.instance }, defaultDispatcher, client, isPanelCollapsed, ), ); useDispatcher(defaultDispatcher, (payload) => { if (payload.action === Action.ToggleUserMenu) { userMenuVm.setOpen(!userMenuVm.getSnapshot().open); } }); useEffect(() => { userMenuVm.setExpanded(!isPanelCollapsed); }, [userMenuVm, isPanelCollapsed]); return ( {({ onKeyDownHandler, onDragEndHandler }) => ( { setDragging(true); }} onDragEnd={(result) => { setDragging(false); if (!result.destination) return; // dropped outside the list sdkContext.spaceStore.moveRootSpace(result.source.index, result.destination.index); onDragEndHandler(); }} > )} ); }; export default SpacePanel;