/* 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 Please see LICENSE files in the repository root for full details. */ import React, { ComponentProps, Dispatch, ReactNode, RefCallback, SetStateAction, useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { DragDropContext, Draggable, Droppable, DroppableProvidedProps } from "react-beautiful-dnd"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/matrix"; 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 SpaceStore from "../../../stores/spaces/SpaceStore"; import { getMetaSpaceName, MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/spaces"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RoomNotificationStateStore, UPDATE_STATUS_INDICATOR, } from "../../../stores/notifications/RoomNotificationStateStore"; import 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 UserMenu from "../../structures/UserMenu"; import IndicatorScrollbar from "../../structures/IndicatorScrollbar"; import { useDispatcher } from "../../../hooks/useDispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { ActionPayload } from "../../../dispatcher/payloads"; import { Action } from "../../../dispatcher/actions"; import { 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"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { return SpaceStore.instance.invitedSpaces; }); const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>( SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => [SpaceStore.instance.enabledMetaSpaces, SpaceStore.instance.spacePanelSpaces], ); const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { return SpaceStore.instance.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; } type MetaSpaceButtonProps = Pick; const MetaSpaceButton: React.FC = ({ selected, isPanelCollapsed, size = "32px", ...props }) => { return (
  • ); }; const getHomeNotificationState = (): NotificationState => { return SpaceStore.instance.allRoomsInHome ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(MetaSpace.Home); }; const HomeButton: React.FC = ({ selected, isPanelCollapsed }) => { const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { return SpaceStore.instance.allRoomsInHome; }); const [notificationState, setNotificationState] = useState(getHomeNotificationState()); const updateNotificationState = useCallback(() => { setNotificationState(getHomeNotificationState()); }, []); useEffect(updateNotificationState, [updateNotificationState, allRoomsInHome]); useEventEmitter(RoomNotificationStateStore.instance, UPDATE_STATUS_INDICATOR, updateNotificationState); return ( ); }; const FavouritesButton: React.FC = ({ selected, isPanelCollapsed }) => { return ( ); }; const PeopleButton: React.FC = ({ selected, isPanelCollapsed }) => { return ( ); }; const OrphansButton: React.FC = ({ selected, isPanelCollapsed }) => { return ( ); }; const VideoRoomsButton: React.FC = ({ selected, isPanelCollapsed }) => { 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.Favourites]: FavouritesButton, [MetaSpace.People]: PeopleButton, [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 [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; 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} {shouldShowComponent(UIComponent.CreateSpaces) && ( )} ); }, ); const SpacePanel: React.FC = () => { 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); } }); return ( {({ onKeyDownHandler, onDragEndHandler }) => ( { setDragging(true); }} onDragEnd={(result) => { setDragging(false); if (!result.destination) return; // dropped outside the list SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index); onDragEndHandler(); }} > )} ); }; export default SpacePanel;