Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Allow reordering of the space panel via Drag and Drop #6137

Merged
merged 24 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
da13ec1
Merge branch 't3chguy/fix/17529' of github.com:matrix-org/matrix-reac…
t3chguy Jun 2, 2021
079a5c1
Respect space ordering field in m.tag for top level spaces
t3chguy Jun 2, 2021
3f12b72
Make AutoHideScrollbar pass through all unknown props
t3chguy Jun 3, 2021
e334ce8
First cut of space panel drag-and-drop ordering
t3chguy Jun 3, 2021
dbaa394
i18n
t3chguy Jun 3, 2021
271f544
Stash
t3chguy Jun 7, 2021
21fc386
Move over to new lexicographic string sorting
t3chguy Jun 10, 2021
a4fa277
Iterate lexicographic ordering implementation
t3chguy Jun 11, 2021
3d44113
write a shedload more tests
t3chguy Jun 11, 2021
4af2675
stash bigint support
t3chguy Jun 14, 2021
8fd72fc
Iterate algorithm, base it on new js-sdk string lib
t3chguy Jun 14, 2021
2879b90
Use alphabet from js-sdk
t3chguy Jun 14, 2021
66fce64
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Jun 14, 2021
b9f86d5
Update yarn.lock
t3chguy Jun 14, 2021
a63d922
Clear outstanding TODOs
t3chguy Jun 15, 2021
cee294f
iterate PR
t3chguy Jun 16, 2021
bceee79
improve naming of tests
t3chguy Jun 16, 2021
d4e3762
Break down the SpacePanel component
t3chguy Jun 16, 2021
e7fde26
remove unused imports
t3chguy Jun 16, 2021
7948aa6
Iterate PR, improve jsdoc and switch function style
t3chguy Jun 22, 2021
6e3c647
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Jun 22, 2021
99e3aea
i18n and regen yarn lock
t3chguy Jun 22, 2021
49d20d2
consolidate the two onRoomAccountData listeners
t3chguy Jun 22, 2021
d212175
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into…
t3chguy Jun 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"qrcode": "^1.4.4",
"re-resizable": "^6.9.0",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1",
Expand Down Expand Up @@ -132,6 +133,7 @@
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "^17.0.2",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.2",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1",
Expand Down
7 changes: 6 additions & 1 deletion res/css/structures/_SpacePanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color;
// Create another flexbox so the Panel fills the container
display: flex;
flex-direction: column;
overflow-y: auto;

.mx_SpacePanel_spaceTreeWrapper {
flex: 1;
Expand Down Expand Up @@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color;
cursor: pointer;
}

.mx_SpaceItem_dragging {
.mx_SpaceButton_toggleCollapse {
visibility: hidden;
}
}

.mx_SpaceTreeLevel {
display: flex;
flex-direction: column;
Expand Down
18 changes: 11 additions & 7 deletions src/components/structures/AutoHideScrollbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import React, { HTMLAttributes } from "react";

interface IProps {
interface IProps extends HTMLAttributes<HTMLDivElement> {
className?: string;
onScroll?: () => void;
onWheel?: () => void;
Expand Down Expand Up @@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
}

public render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;

return (<div
{...otherProps}
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
ref={this.containerRef}
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onWheel={this.props.onWheel}
tabIndex={this.props.tabIndex}
style={style}
className={["mx_AutoHideScrollbar", className].join(" ")}
onWheel={onWheel}
tabIndex={tabIndex}
>
{ this.props.children }
{ children }
</div>);
}
}
11 changes: 7 additions & 4 deletions src/components/structures/IndicatorScrollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
};

render() {
// eslint-disable-next-line no-unused-vars
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;

const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
const leftOverflowIndicator = this.props.trackHorizontalOverflow
const leftOverflowIndicator = trackHorizontalOverflow
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
const rightOverflowIndicator = this.props.trackHorizontalOverflow
const rightOverflowIndicator = trackHorizontalOverflow
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;

return (<AutoHideScrollbar
ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller}
onWheel={this.onMouseWheel}
{...this.props}
{...otherProps}
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
>
{ leftOverflowIndicator }
{ this.props.children }
{ children }
{ rightOverflowIndicator }
</AutoHideScrollbar>);
}
Expand Down
30 changes: 15 additions & 15 deletions src/components/structures/SpaceRoomDirectory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, {ReactNode, useMemo, useState} from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import classNames from "classnames";
import {sortBy} from "lodash";
import { sortBy } from "lodash";

import {MatrixClientPeg} from "../../MatrixClientPeg";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
import { _t } from "../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {EnhancedMap} from "../../utils/maps";
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
import { EnhancedMap } from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media";
import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle";
import {getOrder} from "../../stores/SpaceStore";
import { useStateToggle } from "../../hooks/useStateToggle";
import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {linkifyElement} from "../../HtmlUtils";
import { linkifyElement } from "../../HtmlUtils";

interface IHierarchyProps {
space: Room;
Expand Down Expand Up @@ -286,7 +286,7 @@ export const HierarchyLevel = ({
const children = Array.from(relations.get(spaceId)?.values() || []);
const sortedChildren = sortBy(children, ev => {
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
return getOrder(ev.content.order, null, ev.state_key);
return getChildOrder(ev.content.order, null, ev.state_key);
});
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
Expand Down
185 changes: 117 additions & 68 deletions src/components/views/spaces/SpacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useEffect, useState } from "react";
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import { Room } from "matrix-js-sdk/src/models/room";

import {_t} from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import {useContextMenu} from "../../structures/ContextMenu";
import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
import {SpaceItem} from "./SpaceTreeLevel";
import { SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore, {
HOME_SPACE,
UPDATE_INVITED_SPACES,
Expand All @@ -38,9 +39,9 @@ import {
RovingAccessibleTooltipButton,
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
import {Key} from "../../../Keyboard";
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
import {NotificationState} from "../../../stores/notifications/NotificationState";
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import SettingsStore from "../../../settings/SettingsStore";

interface IButtonProps {
Expand Down Expand Up @@ -122,11 +123,65 @@ const useSpaces = (): [Room[], Room[], Room | null] => {
return [invites, spaces, activeSpace];
};

interface IInnerSpacePanelProps {
children?: ReactNode;
isPanelCollapsed: boolean;
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
}

// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
const [invites, spaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];

const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);

return <div className="mx_SpaceTreeLevel">
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
notificationState={homeNotificationState}
isNarrow={isPanelCollapsed}
/>
{ invites.map(s => (
<SpaceItem
key={s.roomId}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>
)) }
{ spaces.map((s, i) => (
<Draggable key={s.roomId} draggableId={s.roomId} index={i}>
{(provided, snapshot) => (
<SpaceItem
{...provided.draggableProps}
{...provided.dragHandleProps}
key={s.roomId}
innerRef={provided.innerRef}
className={snapshot.isDragging
? "mx_SpaceItem_dragging"
: undefined}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>
)}
</Draggable>
)) }
{ children }
</div>;
});

const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
const [invites, spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);

useEffect(() => {
Expand All @@ -135,10 +190,6 @@ const SpacePanel = () => {
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps

const newClasses = classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
});

let contextMenu = null;
if (menuDisplayed) {
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
Expand Down Expand Up @@ -205,63 +256,61 @@ const SpacePanel = () => {
}
};

const activeSpaces = activeSpace ? [activeSpace] : [];
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
const onNewClick = menuDisplayed ? closeMenu : () => {
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
};

const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
return (
<DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
}}>
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({onKeyDownHandler}) => (
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler}
>
<Droppable droppableId="top-level-spaces">
{(provided, snapshot) => (
<AutoHideScrollbar
{...provided.droppableProps}
wrappedRef={provided.innerRef}
className="mx_SpacePanel_spaceTreeWrapper"
style={snapshot.isDraggingOver ? {
pointerEvents: "none",
} : undefined}
>
<InnerSpacePanel
isPanelCollapsed={isPanelCollapsed}
setPanelCollapsed={setPanelCollapsed}
>
{ provided.placeholder }
</InnerSpacePanel>

// TODO drag and drop for re-arranging order
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({onKeyDownHandler}) => (
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler}
>
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
<div className="mx_SpaceTreeLevel">
<SpaceButton
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
notificationState={homeNotificationState}
isNarrow={isPanelCollapsed}
<SpaceButton
className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
})}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={onNewClick}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
)}
</Droppable>
<AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", { expanded: !isPanelCollapsed })}
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
{ invites.map(s => <SpaceItem
key={s.roomId}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>) }
{ spaces.map(s => <SpaceItem
key={s.roomId}
space={s}
activeSpaces={activeSpaces}
isPanelCollapsed={isPanelCollapsed}
onExpand={() => setPanelCollapsed(false)}
/>) }
</div>
<SpaceButton
className={newClasses}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={menuDisplayed ? closeMenu : () => {
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
}}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
<AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
title={expandCollapseButtonTitle}
/>
{ contextMenu }
</ul>
)}
</RovingTabIndexProvider>
{ contextMenu }
</ul>
)}
</RovingTabIndexProvider>
</DragDropContext>
);
};

export default SpacePanel;
Loading