diff --git a/components.json b/components.json index af85921aed..a79d372cf3 100644 --- a/components.json +++ b/components.json @@ -7,6 +7,7 @@ "src/components/views/elements/RoomName.tsx": "src/components/views/elements/RoomName.tsx", "src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx": "src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx", "src/components/views/avatars/BaseAvatar.tsx": "src/components/views/avatars/BaseAvatar.tsx", - "src/editor/commands.tsx": "src/editor/commands.tsx", - "src/hooks/useRoomName.ts": "src/hooks/useRoomName.ts" + "src/components/views/spaces/SpacePanel.tsx": "src/components/views/spaces/SpacePanel.tsx", + "src/hooks/useRoomName.ts": "src/hooks/useRoomName.ts", + "src/editor/commands.tsx": "src/editor/commands.tsx" } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx new file mode 100644 index 0000000000..b8395d6459 --- /dev/null +++ b/src/components/views/spaces/SpacePanel.tsx @@ -0,0 +1,419 @@ +/* +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 classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/matrix"; +import { IS_MAC, Key } from "matrix-react-sdk/src/Keyboard"; +import { ALTERNATE_KEY_NAME } from "matrix-react-sdk/src/accessibility/KeyboardShortcuts"; +import { RovingTabIndexProvider } from "matrix-react-sdk/src/accessibility/RovingTabIndex"; +import { useContextMenu } from "matrix-react-sdk/src/components/structures/ContextMenu"; +import IndicatorScrollbar from "matrix-react-sdk/src/components/structures/IndicatorScrollbar"; +import UserMenu from "matrix-react-sdk/src/components/structures/UserMenu"; +import IconizedContextMenu, { + IconizedContextMenuCheckbox, + IconizedContextMenuOptionList, +} from "matrix-react-sdk/src/components/views/context_menus/IconizedContextMenu"; +import SpaceContextMenu from "matrix-react-sdk/src/components/views/context_menus/SpaceContextMenu"; +import AccessibleTooltipButton from "matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton"; +import QuickSettingsButton from "matrix-react-sdk/src/components/views/spaces/QuickSettingsButton"; +import SpaceCreateMenu from "matrix-react-sdk/src/components/views/spaces/SpaceCreateMenu"; +import { SpaceButton, SpaceItem } from "matrix-react-sdk/src/components/views/spaces/SpaceTreeLevel"; +import { shouldShowComponent } from "matrix-react-sdk/src/customisations/helpers/UIComponents"; +import { Action } from "matrix-react-sdk/src/dispatcher/actions"; +import defaultDispatcher from "matrix-react-sdk/src/dispatcher/dispatcher"; +import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads"; +import { useDispatcher } from "matrix-react-sdk/src/hooks/useDispatcher"; +import { useEventEmitter, useEventEmitterState } from "matrix-react-sdk/src/hooks/useEventEmitter"; +import { useSettingValue } from "matrix-react-sdk/src/hooks/useSettings"; +import { _t } from "matrix-react-sdk/src/languageHandler"; +import { SettingLevel } from "matrix-react-sdk/src/settings/SettingLevel"; +import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore"; +import { UIComponent } from "matrix-react-sdk/src/settings/UIFeature"; +import UIStore from "matrix-react-sdk/src/stores/UIStore"; +import { NotificationState } from "matrix-react-sdk/src/stores/notifications/NotificationState"; +import { + RoomNotificationStateStore, + UPDATE_STATUS_INDICATOR, +} from "matrix-react-sdk/src/stores/notifications/RoomNotificationStateStore"; +import { + MetaSpace, + SpaceKey, + UPDATE_HOME_BEHAVIOUR, + UPDATE_INVITED_SPACES, + UPDATE_SELECTED_SPACE, + UPDATE_TOP_LEVEL_SPACES, + getMetaSpaceName, +} from "matrix-react-sdk/src/stores/spaces"; +import SpaceStore from "matrix-react-sdk/src/stores/spaces/SpaceStore"; +import React, { + ComponentProps, + Dispatch, + ReactNode, + RefCallback, + SetStateAction, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { DragDropContext, Draggable, Droppable, DroppableProvidedProps } from "react-beautiful-dnd"; +import SuperheroDexButton from "./SuperheroDexButton"; +import MintTokenButton from "./MintTokenButton"; + +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 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, +}; + +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.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(); + }} + > +
    + + setPanelCollapsed(!isPanelCollapsed)} + title={isPanelCollapsed ? _t("action|expand") : _t("action|collapse")} + tooltip={ +
    +
    + {isPanelCollapsed ? _t("action|expand") : _t("action|collapse")} +
    +
    + {IS_MAC + ? "⌘ + ⇧ + D" + : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + + " + " + + _t(ALTERNATE_KEY_NAME[Key.SHIFT]) + + " + D"} +
    +
    + } + /> +
    + + {(provided, snapshot) => ( + + {provided.placeholder} + + )} + + + + + +
    +
    + )} +
    + ); +}; + +export default SpacePanel;