From 079a5c10ad8191d6d3c357400c948253b0738997 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Jun 2021 16:43:38 +0100 Subject: [PATCH 01/20] Respect space ordering field in m.tag for top level spaces --- .../structures/SpaceRoomDirectory.tsx | 4 +-- src/stores/SpaceStore.tsx | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 8d59fe6c68a..2b4fb24c1bb 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -39,7 +39,7 @@ 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 {getChildOrder} from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {linkifyElement} from "../../HtmlUtils"; @@ -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; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 40997d30a89..1333fc5d371 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps"; import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; +import { arrayHasOrderChange } from "../utils/arrays"; interface IState {} @@ -60,8 +61,16 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; +const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; + +const getSpaceTagOrdering = (space: Room): number | undefined => { + return space?.getAccountData(EventType.Tag)?.getContent()?.tags?.[SpaceTagOrderingField]?.order; +}; + +const sortRootSpaces = (spaces: Room[]): Room[] => sortBy(spaces, [getSpaceTagOrdering, "roomId"]); + // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` -export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => { +export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; if (typeof order === "string" && Array.from(order).every((c: string) => { @@ -214,7 +223,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomId = ev.getStateKey(); const childRoom = this.matrixClient?.getRoom(roomId); const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); - return getOrder(ev.getContent().order, createTs, roomId); + return getChildOrder(ev.getContent().order, createTs, roomId); }).map(ev => { return this.matrixClient.getRoom(ev.getStateKey()); }).filter(room => { @@ -326,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.rootSpaces = rootSpaces; + this.rootSpaces = sortRootSpaces(rootSpaces); this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection @@ -338,7 +347,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch - this._invitedSpaces = new Set(invitedSpaces); + this._invitedSpaces = new Set(sortRootSpaces(invitedSpaces)); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); @@ -472,6 +481,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { + if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + + const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; + const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; + if (order !== lastOrder) { + const rootSpaces = sortRootSpaces(this.rootSpaces); + if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { + this.rootSpaces = rootSpaces; + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + } + } + }; + private onRoomState = (ev: MatrixEvent) => { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; @@ -516,6 +539,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (this.matrixClient) { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); + this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); } await this.reset(); @@ -525,6 +549,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); + this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); await this.onSpaceUpdate(); // trigger an initial update From 3f12b7280d801ac505caec71d2a2c095ab68d3c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:31:06 +0100 Subject: [PATCH 02/20] Make AutoHideScrollbar pass through all unknown props --- .../structures/AutoHideScrollbar.tsx | 18 +++++++++++------- .../structures/IndicatorScrollbar.js | 11 +++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 66f998b6162..e5fa124fedd 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -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 { className?: string; onScroll?: () => void; onWheel?: () => void; @@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component { } public render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props; + return (
- { this.props.children } + { children }
); } } diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 51a3b287f01..25dcaeed398 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -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 ?
: null; - const rightOverflowIndicator = this.props.trackHorizontalOverflow + const rightOverflowIndicator = trackHorizontalOverflow ?
: null; return ( { leftOverflowIndicator } - { this.props.children } + { children } { rightOverflowIndicator } ); } From e334ce81920723832c9260b0f009df88805e22a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:32:36 +0100 Subject: [PATCH 03/20] First cut of space panel drag-and-drop ordering --- package.json | 2 + res/css/structures/_SpacePanel.scss | 7 +- src/components/views/spaces/SpacePanel.tsx | 136 +++++++++++------- .../views/spaces/SpaceTreeLevel.tsx | 27 ++-- src/stores/SpaceStore.tsx | 71 +++++++-- yarn.lock | 100 ++++++++++++- 6 files changed, 263 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 270c86ddba5..2d2506e1df6 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "qs": "^6.9.6", "re-resizable": "^6.9.0", "react": "^16.14.0", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^16.14.0", "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", @@ -135,6 +136,7 @@ "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "^16.9", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^16.9.10", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index c433ccf275a..e64057d16c9 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -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; @@ -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; diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index eb63b21f0eb..27f097e9d4d 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -15,8 +15,9 @@ limitations under the License. */ import React, { 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 RoomAvatar from "../avatars/RoomAvatar"; @@ -204,58 +205,89 @@ const SpacePanel = () => { }; const activeSpaces = activeSpace ? [activeSpace] : []; - const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel"); - // TODO drag and drop for re-arranging order - return - {({onKeyDownHandler}) => ( -
    - -
    - SpaceStore.instance.setActiveSpace(null)} - selected={!activeSpace} - tooltip={_t("All rooms")} - notificationState={RoomNotificationStateStore.instance.globalState} - isNarrow={isPanelCollapsed} + return ( + { + if (!result.destination) return; // dropped outside the list + SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index); + }}> + + {({onKeyDownHandler}) => ( +
      + + {(provided, snapshot) => ( + +
      + SpaceStore.instance.setActiveSpace(null)} + selected={!activeSpace} + tooltip={_t("All rooms")} + notificationState={RoomNotificationStateStore.instance.globalState} + isNarrow={isPanelCollapsed} + /> + { invites.map(s => ( + setPanelCollapsed(false)} + /> + )) } + { spaces.map((s, i) => ( + + {(provided, snapshot) => ( + setPanelCollapsed(false)} + /> + )} + + )) } + { provided.placeholder } +
      + { + if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); + }} + isNarrow={isPanelCollapsed} + /> +
      + )} +
      + setPanelCollapsed(!isPanelCollapsed)} + title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")} /> - { invites.map(s => setPanelCollapsed(false)} - />) } - { spaces.map(s => setPanelCollapsed(false)} - />) } -
    - { - if (!isPanelCollapsed) setPanelCollapsed(true); - openMenu(); - }} - isNarrow={isPanelCollapsed} - /> -
    - setPanelCollapsed(!isPanelCollapsed)} - title={expandCollapseButtonTitle} - /> - { contextMenu } -
- )} -
+ { contextMenu } + + )} + + + ); }; export default SpacePanel; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f34baf256b0..7ac863b2394 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {InputHTMLAttributes, LegacyRef} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; @@ -49,13 +49,14 @@ import {EventType} from "matrix-js-sdk/src/@types/event"; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import {NotificationColor} from "../../../stores/notifications/NotificationColor"; -interface IItemProps { +interface IItemProps extends InputHTMLAttributes { space?: Room; activeSpaces: Room[]; isNested?: boolean; isPanelCollapsed?: boolean; onExpand?: Function; parents?: Set; + innerRef?: LegacyRef; } interface IItemState { @@ -300,18 +301,18 @@ export class SpaceItem extends React.PureComponent { } render() { - const {space, activeSpaces, isNested} = this.props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, + ...otherProps } = this.props; - const forceCollapsed = this.props.isPanelCollapsed; - const isNarrow = this.props.isPanelCollapsed; - const collapsed = this.state.collapsed || forceCollapsed; + const collapsed = this.state.collapsed || isPanelCollapsed; const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId) - .filter(s => !this.props.parents?.has(s.roomId)); + .filter(s => !parents?.has(s.roomId)); const isActive = activeSpaces.includes(space); - const itemClasses = classNames({ + const itemClasses = classNames(this.props.className, { "mx_SpaceItem": true, - "mx_SpaceItem_narrow": isNarrow, + "mx_SpaceItem_narrow": isPanelCollapsed, "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); @@ -320,7 +321,7 @@ export class SpaceItem extends React.PureComponent { const classes = classNames("mx_SpaceButton", { mx_SpaceButton_active: isActive, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, - mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_narrow: isPanelCollapsed, mx_SpaceButton_invite: isInvite, }); const notificationState = isInvite @@ -333,7 +334,7 @@ export class SpaceItem extends React.PureComponent { spaces={childSpaces} activeSpaces={activeSpaces} isNested={true} - parents={new Set(this.props.parents).add(this.props.space.roomId)} + parents={new Set(parents).add(space.roomId)} />; } @@ -353,7 +354,7 @@ export class SpaceItem extends React.PureComponent { /> : null; let button; - if (isNarrow) { + if (isPanelCollapsed) { button = ( { } return ( -
  • +
  • { button } { childItems }
  • diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 1333fc5d371..9ef961ce2d6 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -63,12 +63,6 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; -const getSpaceTagOrdering = (space: Room): number | undefined => { - return space?.getAccountData(EventType.Tag)?.getContent()?.tags?.[SpaceTagOrderingField]?.order; -}; - -const sortRootSpaces = (spaces: Room[]): Room[] => sortBy(spaces, [getSpaceTagOrdering, "roomId"]); - // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; @@ -104,6 +98,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); + private spaceOrderLocalEchoMap = new Map(); public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -335,7 +330,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.rootSpaces = sortRootSpaces(rootSpaces); + this.rootSpaces = this.sortRootSpaces(rootSpaces); this.parentMap = backrefs; // if the currently selected space no longer exists, remove its selection @@ -347,7 +342,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); // build initial state of invited spaces as we would have missed the emitted events about the room at launch - this._invitedSpaces = new Set(sortRootSpaces(invitedSpaces)); + this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces)); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); @@ -484,17 +479,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; if (order !== lastOrder) { - const rootSpaces = sortRootSpaces(this.rootSpaces); - if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { - this.rootSpaces = rootSpaces; - this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); - } + this.notifyIfOrderChanged(); } }; + private notifyIfOrderChanged(): void { + const rootSpaces = this.sortRootSpaces(this.rootSpaces); + if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { + this.rootSpaces = rootSpaces; + this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces); + } + } + private onRoomState = (ev: MatrixEvent) => { const room = this.matrixClient.getRoom(ev.getRoomId()); if (!room) return; @@ -624,6 +624,51 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); } + + private getSpaceTagOrdering = (space: Room): number | undefined => { + if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); + return space.tags?.[SpaceTagOrderingField]?.order; + }; + + private sortRootSpaces(spaces: Room[]): Room[] { + return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); + } + + public moveRootSpace(fromIndex: number, toIndex: number): void { + if ( + fromIndex < 0 || toIndex < 0 || + fromIndex > this.rootSpaces.length || toIndex > this.rootSpaces.length || + fromIndex === toIndex + ) { + return; + } + const space = this.rootSpaces[fromIndex]; + const orders = this.rootSpaces.map(this.getSpaceTagOrdering); + + let prevOrder = orders[toIndex - 1]; + let nextOrder = orders[toIndex]; // accounts for downwards displacement of existing inhabitant of this index + + if (prevOrder === undefined && nextOrder === undefined) { + // TODO WHAT A PAIN + } + + prevOrder = prevOrder || 0.0; + nextOrder = nextOrder || 1.0; + + if (prevOrder !== nextOrder) { + const order = prevOrder + ((nextOrder - prevOrder) / 2); + this.spaceOrderLocalEchoMap.set(space.roomId, order); + this.matrixClient.setRoomAccountData(space.roomId, EventType.Tag, { + tags: { + ...space.tags, + [SpaceTagOrderingField]: { order }, + }, + }); + this.notifyIfOrderChanged(); + } else { + // TODO REBUILD + } + } } export default class SpaceStore { diff --git a/yarn.lock b/yarn.lock index 2c84237730a..7e24c220e59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1024,6 +1024,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1504,6 +1511,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1620,6 +1635,13 @@ dependencies: "@types/node" "*" +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.9.10": version "16.9.10" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" @@ -1627,6 +1649,16 @@ dependencies: "@types/react" "^16" +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -2696,6 +2728,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-select@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" @@ -4202,6 +4241,13 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -5717,6 +5763,11 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -6632,6 +6683,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -6659,6 +6715,19 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-clientside-effect@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.3.tgz#95c95f520addfb71743608b990bfe01eb002012b" @@ -6688,7 +6757,7 @@ react-focus-lock@^2.5.0: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" -react-is@^16.13.1, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6698,6 +6767,18 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-redux@^7.2.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" @@ -6818,6 +6899,13 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux@^4.0.0, redux@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -7765,6 +7853,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -8070,6 +8163,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-sidecar@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46" From dbaa394d65c640581f3ef89f52aba13e4801cc3a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:54:30 +0100 Subject: [PATCH 04/20] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85647a17e56..2a5297122f9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1018,9 +1018,9 @@ "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", + "All rooms": "All rooms", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", - "All rooms": "All rooms", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", From 271f5446371b44dad91e0e60dd76d45ffd98620e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 08:59:57 +0100 Subject: [PATCH 05/20] Stash --- src/stores/SpaceStore.tsx | 62 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 9ef961ce2d6..5e09b617a7a 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -61,8 +61,6 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -const SpaceTagOrderingField = "org.matrix.mscXXXX.space"; - // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { let validatedOrder: string = null; @@ -98,7 +96,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); - private spaceOrderLocalEchoMap = new Map(); + private spaceOrderLocalEchoMap = new Map(); public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -477,11 +475,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { - if (!room.isSpaceRoom() || ev.getType() !== EventType.Tag) return; + if (!room.isSpaceRoom() || ev.getType() !== EventType.SpaceOrder) return; this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo - const order = ev.getContent()?.tags?.[SpaceTagOrderingField]?.order; - const lastOrder = lastEv?.getContent()?.tags?.[SpaceTagOrderingField]?.order; + const order = ev.getContent()?.order; + const lastOrder = lastEv?.getContent()?.order; if (order !== lastOrder) { this.notifyIfOrderChanged(); } @@ -625,15 +623,21 @@ export class SpaceStoreClass extends AsyncStoreWithClient { childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); } - private getSpaceTagOrdering = (space: Room): number | undefined => { + private getSpaceTagOrdering = (space: Room): string | undefined => { if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); - return space.tags?.[SpaceTagOrderingField]?.order; + const order = space.getAccountData(EventType.SpaceOrder)?.getContent()?.order; + return typeof order === "string" ? order : undefined; }; private sortRootSpaces(spaces: Room[]): Room[] { return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); } + private setRootSpaceOrder(space: Room, order: string): void { + this.spaceOrderLocalEchoMap.set(space.roomId, order); + this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); + } + public moveRootSpace(fromIndex: number, toIndex: number): void { if ( fromIndex < 0 || toIndex < 0 || @@ -645,29 +649,43 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const space = this.rootSpaces[fromIndex]; const orders = this.rootSpaces.map(this.getSpaceTagOrdering); - let prevOrder = orders[toIndex - 1]; - let nextOrder = orders[toIndex]; // accounts for downwards displacement of existing inhabitant of this index + let prevOrder: string; + let nextOrder: string; - if (prevOrder === undefined && nextOrder === undefined) { - // TODO WHAT A PAIN + if (toIndex > fromIndex) { + prevOrder = toIndex >= 0 ? orders[toIndex] : "aaaaa"; + nextOrder = toIndex <= orders.length ? orders[toIndex + 1] : "zzzzz"; + } else { + // accounts for downwards displacement of existing inhabitant of this index + prevOrder = toIndex > 0 ? orders[toIndex - 1] : "aaaaa"; + nextOrder = toIndex < orders.length ? orders[toIndex] : "zzzzz"; } + console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); + + if (prevOrder === undefined) { + const firstUndefinedIndex = orders.indexOf(undefined); + const numUndefined = orders.length - firstUndefinedIndex; + const lastOrder = orders[firstUndefinedIndex - 1]; + console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder}); + nextOrder = lastOrder + step; + for (let i = firstUndefinedIndex; i < toIndex; i++, nextOrder += step) { + console.log("@@ preset", {i, nextOrder}); + this.setRootSpaceOrder(this.rootSpaces[i], nextOrder); + } - prevOrder = prevOrder || 0.0; - nextOrder = nextOrder || 1.0; + prevOrder = nextOrder; + nextOrder += step; + } if (prevOrder !== nextOrder) { const order = prevOrder + ((nextOrder - prevOrder) / 2); - this.spaceOrderLocalEchoMap.set(space.roomId, order); - this.matrixClient.setRoomAccountData(space.roomId, EventType.Tag, { - tags: { - ...space.tags, - [SpaceTagOrderingField]: { order }, - }, - }); - this.notifyIfOrderChanged(); + console.log("@@ set", {prevOrder, nextOrder, order}); + this.setRootSpaceOrder(space, order); } else { // TODO REBUILD } + + this.notifyIfOrderChanged(); } } From 21fc386317d8bc41f5bdbf416c17263a4f7e055d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 10 Jun 2021 11:40:10 +0100 Subject: [PATCH 06/20] Move over to new lexicographic string sorting --- src/stores/SpaceStore.tsx | 65 +++++++++++++++---------- src/utils/stringOrderField.ts | 56 ++++++++++++++++++++++ test/utils/stringOrderField-test.ts | 73 +++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 src/utils/stringOrderField.ts create mode 100644 test/utils/stringOrderField-test.ts diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 5e09b617a7a..47c735285cf 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -34,6 +34,12 @@ import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; import { arrayHasOrderChange } from "../utils/arrays"; +import { + ALPHABET_END, + ALPHABET_START, + averageBetweenStrings, + midPointsBetweenStrings, +} from "../utils/stringOrderField"; interface IState {} @@ -61,18 +67,19 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` -export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { - let validatedOrder: string = null; - - if (typeof order === "string" && Array.from(order).every((c: string) => { +const validOrder = (order: string): string | null => { + if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); return charCode >= 0x20 && charCode <= 0x7E; })) { - validatedOrder = order; + return order; } + return undefined; +}; - return [validatedOrder, creationTs, roomId]; +// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` +export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => { + return [validOrder(order), creationTs, roomId]; } const getRoomFn: FetchRoomFn = (room: Room) => { @@ -625,8 +632,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private getSpaceTagOrdering = (space: Room): string | undefined => { if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId); - const order = space.getAccountData(EventType.SpaceOrder)?.getContent()?.order; - return typeof order === "string" ? order : undefined; + return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order); }; private sortRootSpaces(spaces: Room[]): Room[] { @@ -635,7 +641,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private setRootSpaceOrder(space: Room, order: string): void { this.spaceOrderLocalEchoMap.set(space.roomId, order); - this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); + this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); // TODO retrying, failure } public moveRootSpace(fromIndex: number, toIndex: number): void { @@ -653,32 +659,42 @@ export class SpaceStoreClass extends AsyncStoreWithClient { let nextOrder: string; if (toIndex > fromIndex) { - prevOrder = toIndex >= 0 ? orders[toIndex] : "aaaaa"; - nextOrder = toIndex <= orders.length ? orders[toIndex + 1] : "zzzzz"; + // moving down + prevOrder = orders[toIndex]; + nextOrder = orders[toIndex + 1]; } else { // accounts for downwards displacement of existing inhabitant of this index - prevOrder = toIndex > 0 ? orders[toIndex - 1] : "aaaaa"; - nextOrder = toIndex < orders.length ? orders[toIndex] : "zzzzz"; + prevOrder = toIndex > 0 ? orders[toIndex - 1] : String.fromCharCode(ALPHABET_START).repeat(5); // TODO + nextOrder = orders[toIndex]; } console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); if (prevOrder === undefined) { + // to be able to move to this toIndex we will first need to insert a bunch of orders for earlier elements const firstUndefinedIndex = orders.indexOf(undefined); const numUndefined = orders.length - firstUndefinedIndex; - const lastOrder = orders[firstUndefinedIndex - 1]; - console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder}); - nextOrder = lastOrder + step; - for (let i = firstUndefinedIndex; i < toIndex; i++, nextOrder += step) { - console.log("@@ preset", {i, nextOrder}); - this.setRootSpaceOrder(this.rootSpaces[i], nextOrder); - } + const lastOrder = orders[firstUndefinedIndex - 1] ?? String.fromCharCode(ALPHABET_START); // TODO + nextOrder = String.fromCharCode(ALPHABET_END).repeat(lastOrder.length + 1); + const newOrders = midPointsBetweenStrings(lastOrder, nextOrder, numUndefined); + + if (newOrders.length === numUndefined) { + console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder, newOrders}); + for (let i = firstUndefinedIndex, j = 0; i <= toIndex; i++, j++) { + if (i === toIndex && toIndex < fromIndex) continue; + if (i === fromIndex) continue; + const newOrder = newOrders[j]; + console.log("@@ preset", {i, j, newOrder}); + this.setRootSpaceOrder(this.rootSpaces[i], newOrder); + } - prevOrder = nextOrder; - nextOrder += step; + prevOrder = newOrders[newOrders.length - 1]; + } else { + prevOrder = nextOrder; // rebuild + } } if (prevOrder !== nextOrder) { - const order = prevOrder + ((nextOrder - prevOrder) / 2); + const order = averageBetweenStrings(prevOrder, nextOrder ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1)); console.log("@@ set", {prevOrder, nextOrder, order}); this.setRootSpaceOrder(space, order); } else { @@ -686,6 +702,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } this.notifyIfOrderChanged(); + console.log("@@ done", this.rootSpaces.map(this.getSpaceTagOrdering)); } } diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts new file mode 100644 index 00000000000..fce859ddb8b --- /dev/null +++ b/src/utils/stringOrderField.ts @@ -0,0 +1,56 @@ +/* +Copyright 2021 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. +*/ + +export const ALPHABET_START = 0x20; +export const ALPHABET_END = 0x7E; +export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) + .fill(undefined) + .map((_, i) => String.fromCharCode(ALPHABET_START + i)) + .join(""); + +export const baseToString = (base: number, alphabet = ALPHABET): string => { + base = Math.floor(base); + if (base < alphabet.length) return alphabet[base]; + return baseToString(Math.floor(base / alphabet.length), alphabet) + alphabet[base % alphabet.length]; +}; + +export const stringToBase = (str: string, alphabet = ALPHABET): number => { + let result = 0; + for (let i = str.length - 1, j = 0; i >= 0; i--, j++) { + result += (str.charCodeAt(i) - alphabet.charCodeAt(0)) * (alphabet.length ** j); + } + return result; +}; + +const pad = (str: string, length: number, alphabet = ALPHABET): string => str.padEnd(length, alphabet[0]); + +export const averageBetweenStrings = (a: string, b: string, alphabet = ALPHABET): string => { + const n = Math.max(a.length, b.length); + const aBase = stringToBase(pad(a, n, alphabet), alphabet); + const bBase = stringToBase(pad(b, n, alphabet), alphabet); + return baseToString((aBase + bBase) / 2, alphabet); +}; + +export const midPointsBetweenStrings = (a: string, b: string, count: number, alphabet = ALPHABET): string[] => { + const n = Math.max(a.length, b.length); + const aBase = stringToBase(pad(a, n, alphabet), alphabet); + const bBase = stringToBase(pad(b, n, alphabet), alphabet); + const step = (bBase - aBase) / (count + 1); + if (step < 1) { + return []; + } + return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet)); +}; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts new file mode 100644 index 00000000000..5b8c2f3febe --- /dev/null +++ b/test/utils/stringOrderField-test.ts @@ -0,0 +1,73 @@ +/* +Copyright 2021 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 { + ALPHABET, + averageBetweenStrings, + baseToString, + midPointsBetweenStrings, + stringToBase, +} from "../../src/utils/stringOrderField"; + +describe("stringOrderField", () => { + it("stringToBase", () => { + expect(stringToBase(" ")).toBe(0); + expect(stringToBase("a")).toBe(65); + expect(stringToBase("aa")).toBe(6240); + expect(stringToBase("cat")).toBe(610934); + expect(stringToBase("doggo")).toBe(5607022724); + expect(stringToBase(" ")).toEqual(0); + expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(0); + expect(stringToBase("a")).toEqual(65); + expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2); + expect(stringToBase("ab")).toEqual(6241); + expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53); + }); + + it("baseToString", () => { + expect(baseToString(10)).toBe(ALPHABET[10]); + expect(baseToString(10, "abcdefghijklmnopqrstuvwxyz")).toEqual("k"); + expect(baseToString(6241)).toEqual("ab"); + expect(baseToString(53, "abcdefghijklmnopqrstuvwxyz")).toEqual("cb"); + expect(baseToString(1234)).toBe(",~"); + }); + + it("averageBetweenStrings", () => { + [ + { a: "a", b: "z", output: `m` }, + { a: "ba", b: "z", output: `n@` }, + { a: "z", b: "ba", output: `n@` }, + { a: "# ", b: "$8888", output: `#[[[[` }, + { a: "cat", b: "doggo", output: `d9>Cw` }, + { a: "cat", b: "doggo", output: "cumqh", alphabet: "abcdefghijklmnopqrstuvwxyz" }, + { a: "aa", b: "zz", output: "mz", alphabet: "abcdefghijklmnopqrstuvwxyz" }, + { a: "a", b: "z", output: "m", alphabet: "abcdefghijklmnopqrstuvwxyz" }, + { a: "AA", b: "zz", output: "^." }, + { a: "A", b: "z", output: "]" }, + ].forEach((c) => { + // assert that the output string falls lexicographically between `a` and `b` + expect([c.a, c.b, c.output].sort()[1]).toBe(c.output); + expect(averageBetweenStrings(c.a, c.b, c.alphabet)).toBe(c.output); + }); + }); + + it("midPointsBetweenStrings", () => { + expect(midPointsBetweenStrings("a", "e", 3)).toStrictEqual(["b", "c", "d"]); + expect(midPointsBetweenStrings("a", "e", 0)).toStrictEqual([]); + expect(midPointsBetweenStrings("a", "e", 4)).toStrictEqual([]); + }); +}); + From a4fa2779d4a40bd8a2eeeb65d96fa61cacd836e5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Jun 2021 10:33:00 +0100 Subject: [PATCH 07/20] Iterate lexicographic ordering implementation --- src/stores/SpaceStore.tsx | 69 ++++--------------------- src/utils/arrays.ts | 17 ++++++- src/utils/stringOrderField.ts | 79 +++++++++++++++++++++++++++++ test/utils/stringOrderField-test.ts | 76 +++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 61 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 47c735285cf..d0ec5733062 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -34,12 +34,7 @@ import {setHasDiff} from "../utils/sets"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; import { arrayHasOrderChange } from "../utils/arrays"; -import { - ALPHABET_END, - ALPHABET_START, - averageBetweenStrings, - midPointsBetweenStrings, -} from "../utils/stringOrderField"; +import { reorderLexicographically } from "../utils/stringOrderField"; interface IState {} @@ -645,64 +640,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public moveRootSpace(fromIndex: number, toIndex: number): void { - if ( - fromIndex < 0 || toIndex < 0 || - fromIndex > this.rootSpaces.length || toIndex > this.rootSpaces.length || - fromIndex === toIndex - ) { - return; - } - const space = this.rootSpaces[fromIndex]; - const orders = this.rootSpaces.map(this.getSpaceTagOrdering); - - let prevOrder: string; - let nextOrder: string; - - if (toIndex > fromIndex) { - // moving down - prevOrder = orders[toIndex]; - nextOrder = orders[toIndex + 1]; - } else { - // accounts for downwards displacement of existing inhabitant of this index - prevOrder = toIndex > 0 ? orders[toIndex - 1] : String.fromCharCode(ALPHABET_START).repeat(5); // TODO - nextOrder = orders[toIndex]; - } - console.log("@@ start", {fromIndex, toIndex, orders, prevOrder, nextOrder}); - - if (prevOrder === undefined) { - // to be able to move to this toIndex we will first need to insert a bunch of orders for earlier elements - const firstUndefinedIndex = orders.indexOf(undefined); - const numUndefined = orders.length - firstUndefinedIndex; - const lastOrder = orders[firstUndefinedIndex - 1] ?? String.fromCharCode(ALPHABET_START); // TODO - nextOrder = String.fromCharCode(ALPHABET_END).repeat(lastOrder.length + 1); - const newOrders = midPointsBetweenStrings(lastOrder, nextOrder, numUndefined); - - if (newOrders.length === numUndefined) { - console.log("@@ precalc", {firstUndefinedIndex, numUndefined, lastOrder, newOrders}); - for (let i = firstUndefinedIndex, j = 0; i <= toIndex; i++, j++) { - if (i === toIndex && toIndex < fromIndex) continue; - if (i === fromIndex) continue; - const newOrder = newOrders[j]; - console.log("@@ preset", {i, j, newOrder}); - this.setRootSpaceOrder(this.rootSpaces[i], newOrder); - } + const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering); + const changes = reorderLexicographically(currentOrders, fromIndex, toIndex); - prevOrder = newOrders[newOrders.length - 1]; - } else { - prevOrder = nextOrder; // rebuild - } - } + changes.forEach(({ index, order }) => { + this.setRootSpaceOrder(this.rootSpaces[index], order); + }); - if (prevOrder !== nextOrder) { - const order = averageBetweenStrings(prevOrder, nextOrder ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1)); - console.log("@@ set", {prevOrder, nextOrder, order}); - this.setRootSpaceOrder(space, order); + if (changes.length) { + this.notifyIfOrderChanged(); } else { - // TODO REBUILD + // TODO } - - this.notifyIfOrderChanged(); - console.log("@@ done", this.rootSpaces.map(this.getSpaceTagOrdering)); } } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index e527f43c299..d319631d930 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {percentageOf, percentageWithin} from "./numbers"; +import { percentageOf, percentageWithin } from "./numbers"; /** * Quickly resample an array to have less/more data points. If an input which is larger @@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] { }, new Set())); } +/** + * Moves a single element from fromIndex to toIndex. + * @param list the list from which to construct the new list. + * @param fromIndex the index of the element to move. + * @param toIndex the index of where to put the element. + * @returns A new array with the requested value moved. + */ +export function reorder(list: T[], fromIndex: number, toIndex: number): T[] { + const result = Array.from(list); + const [removed] = result.splice(fromIndex, 1); + result.splice(toIndex, 0, removed); + + return result; +} + /** * Helper functions to perform LINQ-like queries on arrays. */ diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index fce859ddb8b..ab65a46cb2e 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { reorder } from "./arrays"; + export const ALPHABET_START = 0x20; export const ALPHABET_END = 0x7E; export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) @@ -54,3 +56,80 @@ export const midPointsBetweenStrings = (a: string, b: string, count: number, alp } return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet)); }; + +interface IEntry { + index: number; + order: string; +} + +export const reorderLexicographically = ( + orders: Array, + fromIndex: number, + toIndex: number, +): IEntry[] => { + if ( + fromIndex < 0 || toIndex < 0 || + fromIndex > orders.length || toIndex > orders.length || + fromIndex === toIndex + ) { + return []; + } + + const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order })); + const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); + + const isMoveTowardsRight = toIndex > fromIndex; + const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; + + let leftBoundIdx = toIndex; + let rightBoundIdx = toIndex; + + const canDisplaceLeft = isMoveTowardsRight || orderToLeftUndefined || true; // TODO + if (canDisplaceLeft) { + const nextBase = newOrder[toIndex + 1]?.order !== undefined + ? stringToBase(newOrder[toIndex + 1].order) + : Number.MAX_VALUE; + for (let i = toIndex - 1, j = 0; i >= 0; i--, j++) { + if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; + leftBoundIdx = i; + } + } + + const canDisplaceRight = !orderToLeftUndefined; + // TODO check if there is enough space on the right hand side at all, + // I guess find the last set order and then compare it to prevBase + $requiredGap + if (canDisplaceRight) { + const prevBase = newOrder[toIndex - 1]?.order !== undefined + ? stringToBase(newOrder[toIndex - 1]?.order) + : Number.MIN_VALUE; + for (let i = toIndex + 1, j = 0; i < newOrder.length; i++, j++) { + if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify + rightBoundIdx = i; + } + } + + const leftDiff = toIndex - leftBoundIdx; + const rightDiff = rightBoundIdx - toIndex; + + if (orderToLeftUndefined || leftDiff < rightDiff) { + rightBoundIdx = toIndex; + } else { + leftBoundIdx = toIndex; + } + + const prevOrder = newOrder[leftBoundIdx - 1]?.order + ?? String.fromCharCode(ALPHABET_START).repeat(5); // TODO + const nextOrder = newOrder[rightBoundIdx + 1]?.order + ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1); // TODO + + const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx); + // TODO If we exceed maxLen then reorder EVERYTHING + + console.log("@@ test", { prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined }); + + return changes.map((order, i) => { + const index = newOrder[leftBoundIdx + i].index; + + return { index, order }; + }); +}; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 5b8c2f3febe..8e3ae06b795 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -14,14 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { sortBy } from "lodash"; + import { ALPHABET, averageBetweenStrings, baseToString, midPointsBetweenStrings, + reorderLexicographically, stringToBase, } from "../../src/utils/stringOrderField"; +const moveLexicographicallyTest = ( + orders: Array, + fromIndex: number, + toIndex: number, + expectedIndices: number[], +): void => { + const ops = reorderLexicographically(orders, fromIndex, toIndex); + expect(ops.map(o => o.index).sort()).toStrictEqual(expectedIndices.sort()); + + const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]); + ops.forEach(({ index, order }) => { + zipped[index][1] = order; + }); + + const newOrders = sortBy(zipped, i => i[1]); + console.log("@@ moveLexicographicallyTest", {orders, zipped, newOrders, fromIndex, toIndex, ops}); + expect(newOrders[toIndex][0]).toBe(fromIndex); +}; + describe("stringOrderField", () => { it("stringToBase", () => { expect(stringToBase(" ")).toBe(0); @@ -35,6 +57,9 @@ describe("stringOrderField", () => { expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2); expect(stringToBase("ab")).toEqual(6241); expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53); + expect(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual(4.5115969857961825e+78); + expect(stringToBase("~".repeat(50))).toEqual(7.694497527671333e+98); + // expect(typeof stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual("bigint"); }); it("baseToString", () => { @@ -57,11 +82,18 @@ describe("stringOrderField", () => { { a: "a", b: "z", output: "m", alphabet: "abcdefghijklmnopqrstuvwxyz" }, { a: "AA", b: "zz", output: "^." }, { a: "A", b: "z", output: "]" }, + { + a: "A".repeat(50), + b: "Z".repeat(50), + output: "M}M}M}N ba`54Qpt\\\\Z+kNA#O(9}z>@2jJm]%Y^$m<8lRzz/2[Y", + }, ].forEach((c) => { // assert that the output string falls lexicographically between `a` and `b` expect([c.a, c.b, c.output].sort()[1]).toBe(c.output); expect(averageBetweenStrings(c.a, c.b, c.alphabet)).toBe(c.output); }); + + expect(averageBetweenStrings("Q#!x+k", "V6yr>L")).toBe("S\\Mu5,"); }); it("midPointsBetweenStrings", () => { @@ -69,5 +101,49 @@ describe("stringOrderField", () => { expect(midPointsBetweenStrings("a", "e", 0)).toStrictEqual([]); expect(midPointsBetweenStrings("a", "e", 4)).toStrictEqual([]); }); + + it("moveLexicographically left", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, [2]); + }); + + it("moveLexicographically right", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, [1]); + }); + + it("moveLexicographically all undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + [0, 4], + ); + }); + + it("moveLexicographically all undefined to end", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 1, + 4, + [0, 1, 2, 3, 4], + ); + }); + + it("moveLexicographically some undefined move left", () => { + moveLexicographicallyTest( + ["a", "c", "e", undefined, undefined, undefined], + 5, + 2, + [5], + ); + }); + + it("moveLexicographically some undefined move left close", () => { + moveLexicographicallyTest( + ["a", "a", "e", undefined, undefined, undefined], + 5, + 1, + [1, 5], + ); + }); }); From 3d4411390f0f041241768dad7524f9e2cb465983 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Jun 2021 16:28:07 +0100 Subject: [PATCH 08/20] write a shedload more tests --- src/utils/stringOrderField.ts | 46 ++++--- test/utils/stringOrderField-test.ts | 191 ++++++++++++++++++++++------ 2 files changed, 181 insertions(+), 56 deletions(-) diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index ab65a46cb2e..8c5d7260e7d 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -39,21 +39,32 @@ export const stringToBase = (str: string, alphabet = ALPHABET): number => { const pad = (str: string, length: number, alphabet = ALPHABET): string => str.padEnd(length, alphabet[0]); -export const averageBetweenStrings = (a: string, b: string, alphabet = ALPHABET): string => { - const n = Math.max(a.length, b.length); - const aBase = stringToBase(pad(a, n, alphabet), alphabet); - const bBase = stringToBase(pad(b, n, alphabet), alphabet); - return baseToString((aBase + bBase) / 2, alphabet); -}; - -export const midPointsBetweenStrings = (a: string, b: string, count: number, alphabet = ALPHABET): string[] => { - const n = Math.max(a.length, b.length); - const aBase = stringToBase(pad(a, n, alphabet), alphabet); - const bBase = stringToBase(pad(b, n, alphabet), alphabet); - const step = (bBase - aBase) / (count + 1); - if (step < 1) { +export const midPointsBetweenStrings = ( + a: string, + b: string, + count: number, + maxLen: number, + alphabet = ALPHABET, +): string[] => { + const n = Math.min(maxLen, Math.max(a.length, b.length)); + const aPadded = pad(a, n, alphabet); + const bPadded = pad(b, n, alphabet); + const aBase = stringToBase(aPadded, alphabet); + const bBase = stringToBase(bPadded, alphabet); + if (bBase - aBase - 1 < count) { + if (n < maxLen) { + // this recurses once at most due to the new limit of n+1 + return midPointsBetweenStrings( + pad(aPadded, n + 1, alphabet), + pad(bPadded, n + 1, alphabet), + count, + n + 1, + alphabet, + ); + } return []; } + const step = (bBase - aBase) / (count + 1); return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet)); }; @@ -66,6 +77,7 @@ export const reorderLexicographically = ( orders: Array, fromIndex: number, toIndex: number, + maxLen = 50, ): IEntry[] => { if ( fromIndex < 0 || toIndex < 0 || @@ -89,7 +101,7 @@ export const reorderLexicographically = ( const nextBase = newOrder[toIndex + 1]?.order !== undefined ? stringToBase(newOrder[toIndex + 1].order) : Number.MAX_VALUE; - for (let i = toIndex - 1, j = 0; i >= 0; i--, j++) { + for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; leftBoundIdx = i; } @@ -102,7 +114,7 @@ export const reorderLexicographically = ( const prevBase = newOrder[toIndex - 1]?.order !== undefined ? stringToBase(newOrder[toIndex - 1]?.order) : Number.MIN_VALUE; - for (let i = toIndex + 1, j = 0; i < newOrder.length; i++, j++) { + for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify rightBoundIdx = i; } @@ -122,10 +134,10 @@ export const reorderLexicographically = ( const nextOrder = newOrder[rightBoundIdx + 1]?.order ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1); // TODO - const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx); + const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); // TODO If we exceed maxLen then reorder EVERYTHING - console.log("@@ test", { prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined }); + console.log("@@ test", { prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined, leftDiff, rightDiff }); return changes.map((order, i) => { const index = newOrder[leftBoundIdx + i].index; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 8e3ae06b795..335db028a84 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -18,7 +18,6 @@ import { sortBy } from "lodash"; import { ALPHABET, - averageBetweenStrings, baseToString, midPointsBetweenStrings, reorderLexicographically, @@ -29,10 +28,10 @@ const moveLexicographicallyTest = ( orders: Array, fromIndex: number, toIndex: number, - expectedIndices: number[], + expectedChanges: number, + maxLength?: number, ): void => { - const ops = reorderLexicographically(orders, fromIndex, toIndex); - expect(ops.map(o => o.index).sort()).toStrictEqual(expectedIndices.sort()); + const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength); const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]); ops.forEach(({ index, order }) => { @@ -42,6 +41,7 @@ const moveLexicographicallyTest = ( const newOrders = sortBy(zipped, i => i[1]); console.log("@@ moveLexicographicallyTest", {orders, zipped, newOrders, fromIndex, toIndex, ops}); expect(newOrders[toIndex][0]).toBe(fromIndex); + expect(ops).toHaveLength(expectedChanges); }; describe("stringOrderField", () => { @@ -70,44 +70,20 @@ describe("stringOrderField", () => { expect(baseToString(1234)).toBe(",~"); }); - it("averageBetweenStrings", () => { - [ - { a: "a", b: "z", output: `m` }, - { a: "ba", b: "z", output: `n@` }, - { a: "z", b: "ba", output: `n@` }, - { a: "# ", b: "$8888", output: `#[[[[` }, - { a: "cat", b: "doggo", output: `d9>Cw` }, - { a: "cat", b: "doggo", output: "cumqh", alphabet: "abcdefghijklmnopqrstuvwxyz" }, - { a: "aa", b: "zz", output: "mz", alphabet: "abcdefghijklmnopqrstuvwxyz" }, - { a: "a", b: "z", output: "m", alphabet: "abcdefghijklmnopqrstuvwxyz" }, - { a: "AA", b: "zz", output: "^." }, - { a: "A", b: "z", output: "]" }, - { - a: "A".repeat(50), - b: "Z".repeat(50), - output: "M}M}M}N ba`54Qpt\\\\Z+kNA#O(9}z>@2jJm]%Y^$m<8lRzz/2[Y", - }, - ].forEach((c) => { - // assert that the output string falls lexicographically between `a` and `b` - expect([c.a, c.b, c.output].sort()[1]).toBe(c.output); - expect(averageBetweenStrings(c.a, c.b, c.alphabet)).toBe(c.output); - }); - - expect(averageBetweenStrings("Q#!x+k", "V6yr>L")).toBe("S\\Mu5,"); - }); - it("midPointsBetweenStrings", () => { - expect(midPointsBetweenStrings("a", "e", 3)).toStrictEqual(["b", "c", "d"]); - expect(midPointsBetweenStrings("a", "e", 0)).toStrictEqual([]); - expect(midPointsBetweenStrings("a", "e", 4)).toStrictEqual([]); + const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); + expect(midpoints[0]).toBe("a"); + expect(midpoints[4]).toBe("e"); + expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); + expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); }); it("moveLexicographically left", () => { - moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, [2]); + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1); }); it("moveLexicographically right", () => { - moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, [1]); + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1); }); it("moveLexicographically all undefined", () => { @@ -115,7 +91,7 @@ describe("stringOrderField", () => { [undefined, undefined, undefined, undefined, undefined, undefined], 4, 1, - [0, 4], + 2, ); }); @@ -124,7 +100,7 @@ describe("stringOrderField", () => { [undefined, undefined, undefined, undefined, undefined, undefined], 1, 4, - [0, 1, 2, 3, 4], + 5, ); }); @@ -133,7 +109,7 @@ describe("stringOrderField", () => { ["a", "c", "e", undefined, undefined, undefined], 5, 2, - [5], + 1, ); }); @@ -142,7 +118,144 @@ describe("stringOrderField", () => { ["a", "a", "e", undefined, undefined, undefined], 5, 1, - [1, 5], + 2, + ); + }); + + it("test A moving to the start when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 2, + 0, + 1, + ); + }); + + it("test B moving to the end when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 3, + 4, + ); + }); + + it("test C moving left when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + 2, + ); + }); + + it("test D moving right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 2, + 3, + ); + }); + + it("test E moving more right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 5, + ); + }); + + it("test F moving left when right is undefined", () => { + moveLexicographicallyTest( + ["20", undefined, undefined, undefined, undefined, undefined], + 4, + 2, + 2, + ); + }); + + it("test G moving right when right is undefined", () => { + moveLexicographicallyTest( + ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 4, + ); + }); + + it("test H moving left when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", undefined, undefined], + 3, + 1, + 1, + ); + }); + + it("test I moving right when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", "50", undefined], + 1, + 3, + 1, + ); + }); + + it("test J moving left when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 2, + 1, + 1, + ); + }); + + it("test K moving right when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 1, + 2, + 1, + ); + }); + + it("test L moving left into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "19"], + 3, + 1, + 2, + 2, + ); + }); + + it("test M moving right into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 1, + 3, + 2, + 2, + ); + }); + + it("test N moving right into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "15", "16", undefined], + 1, + 3, + 3, + ); + }); + + it("test O moving left into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 4, + 3, + 2, ); }); }); From 4af2675e235ec1cee07d4a1c760653b94274b58c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 14:37:05 +0100 Subject: [PATCH 09/20] stash bigint support --- src/utils/stringOrderField.ts | 77 +++++++++------ test/utils/stringOrderField-test.ts | 147 +++++++++++++++++++++------- 2 files changed, 160 insertions(+), 64 deletions(-) diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index 8c5d7260e7d..d837dd4cbfd 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -23,16 +23,17 @@ export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) .map((_, i) => String.fromCharCode(ALPHABET_START + i)) .join(""); -export const baseToString = (base: number, alphabet = ALPHABET): string => { - base = Math.floor(base); - if (base < alphabet.length) return alphabet[base]; - return baseToString(Math.floor(base / alphabet.length), alphabet) + alphabet[base % alphabet.length]; +export const baseToString = (base: bigint, alphabet = ALPHABET): string => { + const len = BigInt(alphabet.length); + if (base < len) return alphabet[Number(base)]; + return baseToString(base / len, alphabet) + alphabet[Number(base % len)]; }; -export const stringToBase = (str: string, alphabet = ALPHABET): number => { - let result = 0; - for (let i = str.length - 1, j = 0; i >= 0; i--, j++) { - result += (str.charCodeAt(i) - alphabet.charCodeAt(0)) * (alphabet.length ** j); +export const stringToBase = (str: string, alphabet = ALPHABET): bigint => { + let result = BigInt(0); + const len = BigInt(alphabet.length); + for (let i = str.length - 1, j = BigInt(0); i >= 0; i--, j++) { + result += BigInt(str.charCodeAt(i) - alphabet.charCodeAt(0)) * (len ** j); } return result; }; @@ -51,7 +52,7 @@ export const midPointsBetweenStrings = ( const bPadded = pad(b, n, alphabet); const aBase = stringToBase(aPadded, alphabet); const bBase = stringToBase(bPadded, alphabet); - if (bBase - aBase - 1 < count) { + if (bBase - aBase - BigInt(1) < count) { if (n < maxLen) { // this recurses once at most due to the new limit of n+1 return midPointsBetweenStrings( @@ -64,8 +65,9 @@ export const midPointsBetweenStrings = ( } return []; } - const step = (bBase - aBase) / (count + 1); - return Array(count).fill(undefined).map((_, i) => baseToString(aBase + step + (i * step), alphabet)); + const step = (bBase - aBase) / BigInt(count + 1); + const start = BigInt(aBase + step); + return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); }; interface IEntry { @@ -79,6 +81,7 @@ export const reorderLexicographically = ( toIndex: number, maxLen = 50, ): IEntry[] => { + // sanity check inputs if ( fromIndex < 0 || toIndex < 0 || fromIndex > orders.length || toIndex > orders.length || @@ -87,41 +90,56 @@ export const reorderLexicographically = ( return []; } + // zip orders with their indices to simplify later index wrangling const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order })); + // apply the fundamental order update to the zipped array const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); - const isMoveTowardsRight = toIndex > fromIndex; const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; let leftBoundIdx = toIndex; let rightBoundIdx = toIndex; - const canDisplaceLeft = isMoveTowardsRight || orderToLeftUndefined || true; // TODO - if (canDisplaceLeft) { - const nextBase = newOrder[toIndex + 1]?.order !== undefined - ? stringToBase(newOrder[toIndex + 1].order) - : Number.MAX_VALUE; - for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { - if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; - leftBoundIdx = i; - } + let canMoveLeft = true; + const nextBase = newOrder[toIndex + 1]?.order !== undefined + ? stringToBase(newOrder[toIndex + 1].order) + : BigInt(Number.MAX_VALUE); + + for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { + if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; + leftBoundIdx = i; + } + + if (leftBoundIdx === 0 && + newOrder[0].order !== undefined && + nextBase - stringToBase(newOrder[0].order) < toIndex + ) { + canMoveLeft = false; } const canDisplaceRight = !orderToLeftUndefined; - // TODO check if there is enough space on the right hand side at all, - // I guess find the last set order and then compare it to prevBase + $requiredGap + let canMoveRight = canDisplaceRight; if (canDisplaceRight) { const prevBase = newOrder[toIndex - 1]?.order !== undefined ? stringToBase(newOrder[toIndex - 1]?.order) - : Number.MIN_VALUE; + : BigInt(Number.MIN_VALUE); + for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify rightBoundIdx = i; } + + if (rightBoundIdx === newOrder.length - 1 && + (newOrder[rightBoundIdx] + ? stringToBase(newOrder[rightBoundIdx].order) + : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex) + ) { + canMoveRight = false; + } } - const leftDiff = toIndex - leftBoundIdx; - const rightDiff = rightBoundIdx - toIndex; + const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER; + const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER; if (orderToLeftUndefined || leftDiff < rightDiff) { rightBoundIdx = toIndex; @@ -130,14 +148,13 @@ export const reorderLexicographically = ( } const prevOrder = newOrder[leftBoundIdx - 1]?.order - ?? String.fromCharCode(ALPHABET_START).repeat(5); // TODO + ?? String.fromCharCode(ALPHABET_START).repeat(5); const nextOrder = newOrder[rightBoundIdx + 1]?.order - ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length + 1); // TODO + ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length); const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); - // TODO If we exceed maxLen then reorder EVERYTHING - console.log("@@ test", { prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined, leftDiff, rightDiff }); + console.log("@@ test", { canMoveLeft, canMoveRight, prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined, leftDiff, rightDiff }); return changes.map((order, i) => { const index = newOrder[leftBoundIdx + i].index; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 335db028a84..9f92774acb1 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -46,28 +46,28 @@ const moveLexicographicallyTest = ( describe("stringOrderField", () => { it("stringToBase", () => { - expect(stringToBase(" ")).toBe(0); - expect(stringToBase("a")).toBe(65); - expect(stringToBase("aa")).toBe(6240); - expect(stringToBase("cat")).toBe(610934); - expect(stringToBase("doggo")).toBe(5607022724); - expect(stringToBase(" ")).toEqual(0); - expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(0); - expect(stringToBase("a")).toEqual(65); - expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2); - expect(stringToBase("ab")).toEqual(6241); - expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53); - expect(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual(4.5115969857961825e+78); - expect(stringToBase("~".repeat(50))).toEqual(7.694497527671333e+98); + expect(Number(stringToBase(" "))).toBe(0); + expect(Number(stringToBase("a"))).toBe(65); + expect(Number(stringToBase("aa"))).toBe(6240); + expect(Number(stringToBase("cat"))).toBe(610934); + expect(Number(stringToBase("doggo"))).toBe(5607022724); + expect(Number(stringToBase(" "))).toEqual(0); + expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(0); + expect(Number(stringToBase("a"))).toEqual(65); + expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(2); + expect(Number(stringToBase("ab"))).toEqual(6241); + expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(53); + expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.511596985796182e+78); + expect(Number(stringToBase("~".repeat(50)))).toEqual(7.694497527671333e+98); // expect(typeof stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual("bigint"); }); it("baseToString", () => { - expect(baseToString(10)).toBe(ALPHABET[10]); - expect(baseToString(10, "abcdefghijklmnopqrstuvwxyz")).toEqual("k"); - expect(baseToString(6241)).toEqual("ab"); - expect(baseToString(53, "abcdefghijklmnopqrstuvwxyz")).toEqual("cb"); - expect(baseToString(1234)).toBe(",~"); + expect(baseToString(BigInt(10))).toBe(ALPHABET[10]); + expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("k"); + expect(baseToString(BigInt(6241))).toEqual("ab"); + expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("cb"); + expect(baseToString(BigInt(1234))).toBe(",~"); }); it("midPointsBetweenStrings", () => { @@ -122,7 +122,7 @@ describe("stringOrderField", () => { ); }); - it("test A moving to the start when all is undefined", () => { + it("test moving to the start when all is undefined", () => { moveLexicographicallyTest( [undefined, undefined, undefined, undefined], 2, @@ -131,7 +131,7 @@ describe("stringOrderField", () => { ); }); - it("test B moving to the end when all is undefined", () => { + it("test moving to the end when all is undefined", () => { moveLexicographicallyTest( [undefined, undefined, undefined, undefined], 1, @@ -140,7 +140,7 @@ describe("stringOrderField", () => { ); }); - it("test C moving left when all is undefined", () => { + it("test moving left when all is undefined", () => { moveLexicographicallyTest( [undefined, undefined, undefined, undefined, undefined, undefined], 4, @@ -149,7 +149,7 @@ describe("stringOrderField", () => { ); }); - it("test D moving right when all is undefined", () => { + it("test moving right when all is undefined", () => { moveLexicographicallyTest( [undefined, undefined, undefined, undefined], 1, @@ -158,7 +158,7 @@ describe("stringOrderField", () => { ); }); - it("test E moving more right when all is undefined", () => { + it("test moving more right when all is undefined", () => { moveLexicographicallyTest( [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined], 1, @@ -167,7 +167,7 @@ describe("stringOrderField", () => { ); }); - it("test F moving left when right is undefined", () => { + it("test moving left when right is undefined", () => { moveLexicographicallyTest( ["20", undefined, undefined, undefined, undefined, undefined], 4, @@ -176,7 +176,7 @@ describe("stringOrderField", () => { ); }); - it("test G moving right when right is undefined", () => { + it("test moving right when right is undefined", () => { moveLexicographicallyTest( ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined], 1, @@ -185,7 +185,7 @@ describe("stringOrderField", () => { ); }); - it("test H moving left when right is defined", () => { + it("test moving left when right is defined", () => { moveLexicographicallyTest( ["10", "20", "30", "40", undefined, undefined], 3, @@ -194,7 +194,7 @@ describe("stringOrderField", () => { ); }); - it("test I moving right when right is defined", () => { + it("test moving right when right is defined", () => { moveLexicographicallyTest( ["10", "20", "30", "40", "50", undefined], 1, @@ -203,7 +203,7 @@ describe("stringOrderField", () => { ); }); - it("test J moving left when all is defined", () => { + it("test moving left when all is defined", () => { moveLexicographicallyTest( ["11", "13", "15", "17", "19"], 2, @@ -212,7 +212,7 @@ describe("stringOrderField", () => { ); }); - it("test K moving right when all is defined", () => { + it("test moving right when all is defined", () => { moveLexicographicallyTest( ["11", "13", "15", "17", "19"], 1, @@ -221,7 +221,7 @@ describe("stringOrderField", () => { ); }); - it("test L moving left into no left space", () => { + it.skip("test moving left into no left space", () => { moveLexicographicallyTest( ["11", "12", "13", "14", "19"], 3, @@ -231,17 +231,17 @@ describe("stringOrderField", () => { ); }); - it("test M moving right into no right space", () => { + it("test moving right into no right space", () => { moveLexicographicallyTest( ["15", "16", "17", "18", "19"], 1, 3, - 2, + 3, 2, ); }); - it("test N moving right into no left space", () => { + it("test moving right into no left space", () => { moveLexicographicallyTest( ["11", "12", "13", "14", "15", "16", undefined], 1, @@ -250,13 +250,92 @@ describe("stringOrderField", () => { ); }); - it("test O moving left into no right space", () => { + it("test moving left into no right space", () => { moveLexicographicallyTest( ["15", "16", "17", "18", "19"], 4, 3, + 4, 2, ); }); + + it("test moving left into no left space", () => { + moveLexicographicallyTest( + [ + ALPHABET.charAt(0), + ALPHABET.charAt(1), + ALPHABET.charAt(2), + ALPHABET.charAt(3), + ALPHABET.charAt(4), + ALPHABET.charAt(5), + ], + 5, + 1, + 5, + 1, + ); + }); + + it("test moving right into no right space", () => { + moveLexicographicallyTest( + [ + ALPHABET.charAt(ALPHABET.length - 5), + ALPHABET.charAt(ALPHABET.length - 4), + ALPHABET.charAt(ALPHABET.length - 3), + ALPHABET.charAt(ALPHABET.length - 2), + ALPHABET.charAt(ALPHABET.length - 1), + ], + 1, + 3, + 3, + 1, + ); + }); + + it("test moving right into no left space", () => { + moveLexicographicallyTest( + ["0", "1", "2", "3", "4", "5"], + 1, + 3, + 3, + 1, + ); + }); + + it("test moving left into no right space", () => { + moveLexicographicallyTest( + [ + ALPHABET.charAt(ALPHABET.length - 5), + ALPHABET.charAt(ALPHABET.length - 4), + ALPHABET.charAt(ALPHABET.length - 3), + ALPHABET.charAt(ALPHABET.length - 2), + ALPHABET.charAt(ALPHABET.length - 1), + ], + 4, + 3, + 4, + 1, + ); + }); + + const prev = (str: string) => baseToString(stringToBase(str) - BigInt(1)); + const next = (str: string) => baseToString(stringToBase(str) + BigInt(1)); + + it("baseN calculation is correctly consecutive", () => { + const str = "this-is-a-test"; + expect(next(prev(str))).toBe(str); + }); + + it("rolls over sanely", () => { + const maxSpaceValue = "~".repeat(50); + const fiftyFirstChar = "!" + " ".repeat(50); + expect(next(maxSpaceValue)).toBe(fiftyFirstChar); + expect(prev(fiftyFirstChar)).toBe(maxSpaceValue); + expect(stringToBase(ALPHABET[0])).toEqual(BigInt(0)); + expect(stringToBase(ALPHABET[1])).toEqual(BigInt(1)); + expect(ALPHABET[ALPHABET.length - 1]).toBe("~"); + expect(ALPHABET[0]).toBe(" "); + }); }); From 8fd72fcf795659d5d40e25e066dec0e68ad7efbb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 21:28:32 +0100 Subject: [PATCH 10/20] Iterate algorithm, base it on new js-sdk string lib --- src/utils/stringOrderField.ts | 77 ++++++++---------- test/utils/stringOrderField-test.ts | 121 +++++++++++++++------------- 2 files changed, 98 insertions(+), 100 deletions(-) diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index d837dd4cbfd..e09f7fbea43 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { alphabetPad, baseToString, stringToBase } from "matrix-js-sdk/src/utils"; + import { reorder } from "./arrays"; export const ALPHABET_START = 0x20; @@ -23,23 +25,6 @@ export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) .map((_, i) => String.fromCharCode(ALPHABET_START + i)) .join(""); -export const baseToString = (base: bigint, alphabet = ALPHABET): string => { - const len = BigInt(alphabet.length); - if (base < len) return alphabet[Number(base)]; - return baseToString(base / len, alphabet) + alphabet[Number(base % len)]; -}; - -export const stringToBase = (str: string, alphabet = ALPHABET): bigint => { - let result = BigInt(0); - const len = BigInt(alphabet.length); - for (let i = str.length - 1, j = BigInt(0); i >= 0; i--, j++) { - result += BigInt(str.charCodeAt(i) - alphabet.charCodeAt(0)) * (len ** j); - } - return result; -}; - -const pad = (str: string, length: number, alphabet = ALPHABET): string => str.padEnd(length, alphabet[0]); - export const midPointsBetweenStrings = ( a: string, b: string, @@ -47,26 +32,28 @@ export const midPointsBetweenStrings = ( maxLen: number, alphabet = ALPHABET, ): string[] => { - const n = Math.min(maxLen, Math.max(a.length, b.length)); - const aPadded = pad(a, n, alphabet); - const bPadded = pad(b, n, alphabet); - const aBase = stringToBase(aPadded, alphabet); - const bBase = stringToBase(bPadded, alphabet); - if (bBase - aBase - BigInt(1) < count) { - if (n < maxLen) { + const padN = Math.min(Math.max(a.length, b.length), maxLen); + const padA = alphabetPad(a, padN, alphabet); + const padB = alphabetPad(b, padN, alphabet); + const baseA = stringToBase(padA, alphabet); + const baseB = stringToBase(padB, alphabet); + + if (baseB - baseA - BigInt(1) < count) { + if (padN < maxLen) { // this recurses once at most due to the new limit of n+1 return midPointsBetweenStrings( - pad(aPadded, n + 1, alphabet), - pad(bPadded, n + 1, alphabet), + alphabetPad(padA, padN + 1, alphabet), + alphabetPad(padB, padN + 1, alphabet), count, - n + 1, + padN + 1, alphabet, ); } return []; } - const step = (bBase - aBase) / BigInt(count + 1); - const start = BigInt(aBase + step); + + const step = (baseB - baseA) / BigInt(count + 1); + const start = BigInt(baseA + step); return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); }; @@ -95,6 +82,7 @@ export const reorderLexicographically = ( // apply the fundamental order update to the zipped array const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); + // check if we have to fill undefined orders to complete placement const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; let leftBoundIdx = toIndex; @@ -105,14 +93,19 @@ export const reorderLexicographically = ( ? stringToBase(newOrder[toIndex + 1].order) : BigInt(Number.MAX_VALUE); + // check how far left we would have to mutate to fit in that direction for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) { if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break; leftBoundIdx = i; } + // verify the left move would be sufficient + const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order); + const bigToIndex = BigInt(toIndex); if (leftBoundIdx === 0 && - newOrder[0].order !== undefined && - nextBase - stringToBase(newOrder[0].order) < toIndex + firstOrderBase !== undefined && + nextBase - firstOrderBase <= bigToIndex && + firstOrderBase <= bigToIndex ) { canMoveLeft = false; } @@ -124,11 +117,13 @@ export const reorderLexicographically = ( ? stringToBase(newOrder[toIndex - 1]?.order) : BigInt(Number.MIN_VALUE); + // check how far right we would have to mutate to fit in that direction for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) { - if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; // TODO verify + if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break; rightBoundIdx = i; } + // verify the right move would be sufficient if (rightBoundIdx === newOrder.length - 1 && (newOrder[rightBoundIdx] ? stringToBase(newOrder[rightBoundIdx].order) @@ -138,27 +133,23 @@ export const reorderLexicographically = ( } } + // pick the cheaper direction const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER; const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER; - if (orderToLeftUndefined || leftDiff < rightDiff) { rightBoundIdx = toIndex; } else { leftBoundIdx = toIndex; } - const prevOrder = newOrder[leftBoundIdx - 1]?.order - ?? String.fromCharCode(ALPHABET_START).repeat(5); + const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? ""; const nextOrder = newOrder[rightBoundIdx + 1]?.order - ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length); + ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length || 1); const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); - console.log("@@ test", { canMoveLeft, canMoveRight, prevOrder, nextOrder, changes, leftBoundIdx, rightBoundIdx, orders, fromIndex, toIndex, newOrder, orderToLeftUndefined, leftDiff, rightDiff }); - - return changes.map((order, i) => { - const index = newOrder[leftBoundIdx + i].index; - - return { index, order }; - }); + return changes.map((order, i) => ({ + index: newOrder[leftBoundIdx + i].index, + order, + })); }; diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index 9f92774acb1..d5671ebe76b 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -15,13 +15,12 @@ limitations under the License. */ import { sortBy } from "lodash"; +import { stringToBase, baseToString, averageBetweenStrings } from "matrix-js-sdk/src/utils"; import { ALPHABET, - baseToString, midPointsBetweenStrings, reorderLexicographically, - stringToBase, } from "../../src/utils/stringOrderField"; const moveLexicographicallyTest = ( @@ -39,43 +38,58 @@ const moveLexicographicallyTest = ( }); const newOrders = sortBy(zipped, i => i[1]); - console.log("@@ moveLexicographicallyTest", {orders, zipped, newOrders, fromIndex, toIndex, ops}); expect(newOrders[toIndex][0]).toBe(fromIndex); expect(ops).toHaveLength(expectedChanges); }; describe("stringOrderField", () => { it("stringToBase", () => { - expect(Number(stringToBase(" "))).toBe(0); - expect(Number(stringToBase("a"))).toBe(65); - expect(Number(stringToBase("aa"))).toBe(6240); - expect(Number(stringToBase("cat"))).toBe(610934); - expect(Number(stringToBase("doggo"))).toBe(5607022724); - expect(Number(stringToBase(" "))).toEqual(0); - expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(0); - expect(Number(stringToBase("a"))).toEqual(65); - expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(2); - expect(Number(stringToBase("ab"))).toEqual(6241); - expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(53); - expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.511596985796182e+78); - expect(Number(stringToBase("~".repeat(50)))).toEqual(7.694497527671333e+98); - // expect(typeof stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toEqual("bigint"); + expect(Number(stringToBase(""))).toBe(0); + expect(Number(stringToBase(" "))).toBe(1); + expect(Number(stringToBase("a"))).toBe(66); + expect(Number(stringToBase(" !"))).toBe(97); + expect(Number(stringToBase("aa"))).toBe(6336); + expect(Number(stringToBase("cat"))).toBe(620055); + expect(Number(stringToBase("doggo"))).toBe(5689339845); + expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(1); + expect(Number(stringToBase("a"))).toEqual(66); + expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(3); + expect(Number(stringToBase("ab"))).toEqual(6337); + expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(80); + expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.648312045971824e+78); + expect(Number(stringToBase("~".repeat(50)))).toEqual(7.776353884348688e+98); + expect(Number(stringToBase(" "))).toEqual(7820126496); + expect(Number(stringToBase(" "))).toEqual(96); + expect(Number(stringToBase(" !"))).toEqual(97); + expect(Number(stringToBase("S:J\\~"))).toEqual(4258975590); + expect(Number(stringToBase("!'Tu:}"))).toEqual(16173443434); }); it("baseToString", () => { - expect(baseToString(BigInt(10))).toBe(ALPHABET[10]); - expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("k"); - expect(baseToString(BigInt(6241))).toEqual("ab"); - expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("cb"); - expect(baseToString(BigInt(1234))).toBe(",~"); + expect(baseToString(BigInt(10))).toBe(ALPHABET[9]); + expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("j"); + expect(baseToString(BigInt(6241))).toEqual("`a"); + expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("ba"); + expect(baseToString(BigInt(1234))).toBe("+}"); + expect(baseToString(BigInt(0))).toBe(""); // TODO + expect(baseToString(BigInt(1))).toBe(" "); + expect(baseToString(BigInt(95))).toBe("~"); + expect(baseToString(BigInt(96))).toBe(" "); + expect(baseToString(BigInt(97))).toBe(" !"); + expect(baseToString(BigInt(98))).toBe(' "'); + expect(baseToString(BigInt(1))).toBe(" "); }); it("midPointsBetweenStrings", () => { + expect(averageBetweenStrings("!!", "##")).toBe('""'); const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); expect(midpoints[0]).toBe("a"); expect(midpoints[4]).toBe("e"); expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); + expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]); + expect(averageBetweenStrings(" ", "!!")).toBe(" P"); + expect(averageBetweenStrings("! ", "!!")).toBe("! "); }); it("moveLexicographically left", () => { @@ -221,7 +235,7 @@ describe("stringOrderField", () => { ); }); - it.skip("test moving left into no left space", () => { + it("test moving left into no left space", () => { moveLexicographicallyTest( ["11", "12", "13", "14", "19"], 3, @@ -229,41 +243,11 @@ describe("stringOrderField", () => { 2, 2, ); - }); - - it("test moving right into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 1, - 3, - 3, - 2, - ); - }); - - it("test moving right into no left space", () => { - moveLexicographicallyTest( - ["11", "12", "13", "14", "15", "16", undefined], - 1, - 3, - 3, - ); - }); - it("test moving left into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 4, - 3, - 4, - 2, - ); - }); - - it("test moving left into no left space", () => { moveLexicographicallyTest( [ ALPHABET.charAt(0), + // Target ALPHABET.charAt(1), ALPHABET.charAt(2), ALPHABET.charAt(3), @@ -278,6 +262,14 @@ describe("stringOrderField", () => { }); it("test moving right into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 1, + 3, + 3, + 2, + ); + moveLexicographicallyTest( [ ALPHABET.charAt(ALPHABET.length - 5), @@ -294,6 +286,13 @@ describe("stringOrderField", () => { }); it("test moving right into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "15", "16", undefined], + 1, + 3, + 3, + ); + moveLexicographicallyTest( ["0", "1", "2", "3", "4", "5"], 1, @@ -304,6 +303,14 @@ describe("stringOrderField", () => { }); it("test moving left into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 4, + 3, + 4, + 2, + ); + moveLexicographicallyTest( [ ALPHABET.charAt(ALPHABET.length - 5), @@ -329,11 +336,11 @@ describe("stringOrderField", () => { it("rolls over sanely", () => { const maxSpaceValue = "~".repeat(50); - const fiftyFirstChar = "!" + " ".repeat(50); + const fiftyFirstChar = " ".repeat(51); expect(next(maxSpaceValue)).toBe(fiftyFirstChar); expect(prev(fiftyFirstChar)).toBe(maxSpaceValue); - expect(stringToBase(ALPHABET[0])).toEqual(BigInt(0)); - expect(stringToBase(ALPHABET[1])).toEqual(BigInt(1)); + expect(Number(stringToBase(ALPHABET[0]))).toEqual(1); + expect(Number(stringToBase(ALPHABET[1]))).toEqual(2); expect(ALPHABET[ALPHABET.length - 1]).toBe("~"); expect(ALPHABET[0]).toBe(" "); }); From 2879b9086c1ace24fc3307e6af5b404fff0059c2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 21:32:11 +0100 Subject: [PATCH 11/20] Use alphabet from js-sdk --- src/utils/stringOrderField.ts | 13 ++------ test/utils/stringOrderField-test.ts | 50 +++++++++++++---------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index e09f7fbea43..4336583b9d0 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -14,23 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { alphabetPad, baseToString, stringToBase } from "matrix-js-sdk/src/utils"; +import { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; import { reorder } from "./arrays"; -export const ALPHABET_START = 0x20; -export const ALPHABET_END = 0x7E; -export const ALPHABET = new Array(1 + ALPHABET_END - ALPHABET_START) - .fill(undefined) - .map((_, i) => String.fromCharCode(ALPHABET_START + i)) - .join(""); - export const midPointsBetweenStrings = ( a: string, b: string, count: number, maxLen: number, - alphabet = ALPHABET, + alphabet = DEFAULT_ALPHABET, ): string[] => { const padN = Math.min(Math.max(a.length, b.length), maxLen); const padA = alphabetPad(a, padN, alphabet); @@ -144,7 +137,7 @@ export const reorderLexicographically = ( const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? ""; const nextOrder = newOrder[rightBoundIdx + 1]?.order - ?? String.fromCharCode(ALPHABET_END).repeat(prevOrder.length || 1); + ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1); const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen); diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index d5671ebe76b..a8bc00eeb92 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -15,13 +15,9 @@ limitations under the License. */ import { sortBy } from "lodash"; -import { stringToBase, baseToString, averageBetweenStrings } from "matrix-js-sdk/src/utils"; +import { stringToBase, baseToString, averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; -import { - ALPHABET, - midPointsBetweenStrings, - reorderLexicographically, -} from "../../src/utils/stringOrderField"; +import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField"; const moveLexicographicallyTest = ( orders: Array, @@ -66,7 +62,7 @@ describe("stringOrderField", () => { }); it("baseToString", () => { - expect(baseToString(BigInt(10))).toBe(ALPHABET[9]); + expect(baseToString(BigInt(10))).toBe(DEFAULT_ALPHABET[9]); expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("j"); expect(baseToString(BigInt(6241))).toEqual("`a"); expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("ba"); @@ -246,13 +242,13 @@ describe("stringOrderField", () => { moveLexicographicallyTest( [ - ALPHABET.charAt(0), + DEFAULT_ALPHABET.charAt(0), // Target - ALPHABET.charAt(1), - ALPHABET.charAt(2), - ALPHABET.charAt(3), - ALPHABET.charAt(4), - ALPHABET.charAt(5), + DEFAULT_ALPHABET.charAt(1), + DEFAULT_ALPHABET.charAt(2), + DEFAULT_ALPHABET.charAt(3), + DEFAULT_ALPHABET.charAt(4), + DEFAULT_ALPHABET.charAt(5), ], 5, 1, @@ -272,11 +268,11 @@ describe("stringOrderField", () => { moveLexicographicallyTest( [ - ALPHABET.charAt(ALPHABET.length - 5), - ALPHABET.charAt(ALPHABET.length - 4), - ALPHABET.charAt(ALPHABET.length - 3), - ALPHABET.charAt(ALPHABET.length - 2), - ALPHABET.charAt(ALPHABET.length - 1), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), ], 1, 3, @@ -313,11 +309,11 @@ describe("stringOrderField", () => { moveLexicographicallyTest( [ - ALPHABET.charAt(ALPHABET.length - 5), - ALPHABET.charAt(ALPHABET.length - 4), - ALPHABET.charAt(ALPHABET.length - 3), - ALPHABET.charAt(ALPHABET.length - 2), - ALPHABET.charAt(ALPHABET.length - 1), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), ], 4, 3, @@ -339,10 +335,10 @@ describe("stringOrderField", () => { const fiftyFirstChar = " ".repeat(51); expect(next(maxSpaceValue)).toBe(fiftyFirstChar); expect(prev(fiftyFirstChar)).toBe(maxSpaceValue); - expect(Number(stringToBase(ALPHABET[0]))).toEqual(1); - expect(Number(stringToBase(ALPHABET[1]))).toEqual(2); - expect(ALPHABET[ALPHABET.length - 1]).toBe("~"); - expect(ALPHABET[0]).toBe(" "); + expect(Number(stringToBase(DEFAULT_ALPHABET[0]))).toEqual(1); + expect(Number(stringToBase(DEFAULT_ALPHABET[1]))).toEqual(2); + expect(DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1]).toBe("~"); + expect(DEFAULT_ALPHABET[0]).toBe(" "); }); }); From b9f86d54c3712bcf438038376a01fb9a90c96e9f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Jun 2021 22:07:25 +0100 Subject: [PATCH 12/20] Update yarn.lock --- yarn.lock | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8e6b4fa7326..289d33088f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6659,7 +6659,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6809,7 +6809,12 @@ react-focus-lock@^2.5.0: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== From a63d9220d2474ffd592c728371107bcfda1ae070 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Jun 2021 08:26:46 +0100 Subject: [PATCH 13/20] Clear outstanding TODOs --- src/stores/SpaceStore.tsx | 17 ++++++++++------- test/utils/stringOrderField-test.ts | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d0ec5733062..9ffb4eb776c 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -634,9 +634,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]); } - private setRootSpaceOrder(space: Room, order: string): void { + private async setRootSpaceOrder(space: Room, order: string): Promise { this.spaceOrderLocalEchoMap.set(space.roomId, order); - this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); // TODO retrying, failure + try { + await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); + } catch (e) { + console.log("Failed to set root space order", e); + if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) { + this.spaceOrderLocalEchoMap.delete(space.roomId); + } + } } public moveRootSpace(fromIndex: number, toIndex: number): void { @@ -647,11 +654,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.setRootSpaceOrder(this.rootSpaces[index], order); }); - if (changes.length) { - this.notifyIfOrderChanged(); - } else { - // TODO - } + this.notifyIfOrderChanged(); } } diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index a8bc00eeb92..a5238720237 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -67,7 +67,7 @@ describe("stringOrderField", () => { expect(baseToString(BigInt(6241))).toEqual("`a"); expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("ba"); expect(baseToString(BigInt(1234))).toBe("+}"); - expect(baseToString(BigInt(0))).toBe(""); // TODO + expect(baseToString(BigInt(0))).toBe(""); expect(baseToString(BigInt(1))).toBe(" "); expect(baseToString(BigInt(95))).toBe("~"); expect(baseToString(BigInt(96))).toBe(" "); From cee294f5a7378737bc0cbaf9707250eeea5a195d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 09:23:06 +0100 Subject: [PATCH 14/20] iterate PR --- .../structures/AutoHideScrollbar.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 28 +++++++++---------- .../views/spaces/SpaceTreeLevel.tsx | 28 +++++++++---------- src/stores/SpaceStore.tsx | 5 ++-- src/utils/arrays.ts | 2 +- src/utils/stringOrderField.ts | 4 +-- 6 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index e5fa124fedd..8650224fb3c 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {HTMLAttributes} from "react"; +import React, { HTMLAttributes } from "react"; interface IProps extends HTMLAttributes { className?: string; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 2b4fb24c1bb..497f525a005 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -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 {getChildOrder} 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; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 7ac863b2394..416b4cc6f1c 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,23 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {InputHTMLAttributes, LegacyRef} from "react"; +import React, { InputHTMLAttributes, LegacyRef } from "react"; import classNames from "classnames"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/SpaceStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; -import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; -import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; +import { RovingAccessibleButton } from "../../../accessibility/roving/RovingAccessibleButton"; +import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList, } from "../context_menus/IconizedContextMenu"; -import {_t} from "../../../languageHandler"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; -import {toRightOf} from "../../structures/ContextMenu"; +import { _t } from "../../../languageHandler"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { toRightOf } from "../../structures/ContextMenu"; import { shouldShowSpaceSettings, showAddExistingRooms, @@ -39,15 +39,15 @@ import { showSpaceSettings, } from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; +import { Action } from "../../../dispatcher/actions"; import RoomViewStore from "../../../stores/RoomViewStore"; -import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; -import {NotificationColor} from "../../../stores/notifications/NotificationColor"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IItemProps extends InputHTMLAttributes { space?: Room; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 9ffb4eb776c..b0099b33066 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -62,14 +62,13 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -const validOrder = (order: string): string | null => { +const validOrder = (order: string): string | undefined => { if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); return charCode >= 0x20 && charCode <= 0x7E; })) { return order; } - return undefined; }; // For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` @@ -639,7 +638,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { try { await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order }); } catch (e) { - console.log("Failed to set root space order", e); + console.warn("Failed to set root space order", e); if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) { this.spaceOrderLocalEchoMap.delete(space.roomId); } diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index d319631d930..148861e5d35 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -230,7 +230,7 @@ export function arrayMerge(...a: T[][]): T[] { * @param toIndex the index of where to put the element. * @returns A new array with the requested value moved. */ -export function reorder(list: T[], fromIndex: number, toIndex: number): T[] { +export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] { const result = Array.from(list); const [removed] = result.splice(fromIndex, 1); result.splice(toIndex, 0, removed); diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index 4336583b9d0..b312b85b085 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -16,7 +16,7 @@ limitations under the License. import { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; -import { reorder } from "./arrays"; +import { moveElement } from "./arrays"; export const midPointsBetweenStrings = ( a: string, @@ -73,7 +73,7 @@ export const reorderLexicographically = ( // zip orders with their indices to simplify later index wrangling const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order })); // apply the fundamental order update to the zipped array - const newOrder = reorder(ordersWithIndices, fromIndex, toIndex); + const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex); // check if we have to fill undefined orders to complete placement const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined; From bceee7978edc0a972f67cc612a250d5f0f717dfd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 09:30:47 +0100 Subject: [PATCH 15/20] improve naming of tests --- test/utils/stringOrderField-test.ts | 543 +++++++++++++--------------- 1 file changed, 245 insertions(+), 298 deletions(-) diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index a5238720237..ece3043d868 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -39,306 +39,253 @@ const moveLexicographicallyTest = ( }; describe("stringOrderField", () => { - it("stringToBase", () => { - expect(Number(stringToBase(""))).toBe(0); - expect(Number(stringToBase(" "))).toBe(1); - expect(Number(stringToBase("a"))).toBe(66); - expect(Number(stringToBase(" !"))).toBe(97); - expect(Number(stringToBase("aa"))).toBe(6336); - expect(Number(stringToBase("cat"))).toBe(620055); - expect(Number(stringToBase("doggo"))).toBe(5689339845); - expect(Number(stringToBase("a", "abcdefghijklmnopqrstuvwxyz"))).toEqual(1); - expect(Number(stringToBase("a"))).toEqual(66); - expect(Number(stringToBase("c", "abcdefghijklmnopqrstuvwxyz"))).toEqual(3); - expect(Number(stringToBase("ab"))).toEqual(6337); - expect(Number(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz"))).toEqual(80); - expect(Number(stringToBase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))).toEqual(4.648312045971824e+78); - expect(Number(stringToBase("~".repeat(50)))).toEqual(7.776353884348688e+98); - expect(Number(stringToBase(" "))).toEqual(7820126496); - expect(Number(stringToBase(" "))).toEqual(96); - expect(Number(stringToBase(" !"))).toEqual(97); - expect(Number(stringToBase("S:J\\~"))).toEqual(4258975590); - expect(Number(stringToBase("!'Tu:}"))).toEqual(16173443434); + describe("midPointsBetweenStrings", () => { + it("should work", () => { + expect(averageBetweenStrings("!!", "##")).toBe('""'); + const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); + expect(midpoints[0]).toBe("a"); + expect(midpoints[4]).toBe("e"); + expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]); + }); + + it("should return empty array when the request is not possible", () => { + expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); + expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); + }); }); - it("baseToString", () => { - expect(baseToString(BigInt(10))).toBe(DEFAULT_ALPHABET[9]); - expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("j"); - expect(baseToString(BigInt(6241))).toEqual("`a"); - expect(baseToString(BigInt(53), "abcdefghijklmnopqrstuvwxyz")).toEqual("ba"); - expect(baseToString(BigInt(1234))).toBe("+}"); - expect(baseToString(BigInt(0))).toBe(""); - expect(baseToString(BigInt(1))).toBe(" "); - expect(baseToString(BigInt(95))).toBe("~"); - expect(baseToString(BigInt(96))).toBe(" "); - expect(baseToString(BigInt(97))).toBe(" !"); - expect(baseToString(BigInt(98))).toBe(' "'); - expect(baseToString(BigInt(1))).toBe(" "); - }); - - it("midPointsBetweenStrings", () => { - expect(averageBetweenStrings("!!", "##")).toBe('""'); - const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort(); - expect(midpoints[0]).toBe("a"); - expect(midpoints[4]).toBe("e"); - expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]); - expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]); - expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]); - expect(averageBetweenStrings(" ", "!!")).toBe(" P"); - expect(averageBetweenStrings("! ", "!!")).toBe("! "); - }); - - it("moveLexicographically left", () => { - moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1); - }); - - it("moveLexicographically right", () => { - moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1); - }); - - it("moveLexicographically all undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined, undefined, undefined], - 4, - 1, - 2, - ); - }); - - it("moveLexicographically all undefined to end", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined, undefined, undefined], - 1, - 4, - 5, - ); - }); - - it("moveLexicographically some undefined move left", () => { - moveLexicographicallyTest( - ["a", "c", "e", undefined, undefined, undefined], - 5, - 2, - 1, - ); - }); - - it("moveLexicographically some undefined move left close", () => { - moveLexicographicallyTest( - ["a", "a", "e", undefined, undefined, undefined], - 5, - 1, - 2, - ); - }); - - it("test moving to the start when all is undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined], - 2, - 0, - 1, - ); - }); - - it("test moving to the end when all is undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined], - 1, - 3, - 4, - ); - }); - - it("test moving left when all is undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined, undefined, undefined], - 4, - 1, - 2, - ); - }); - - it("test moving right when all is undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined], - 1, - 2, - 3, - ); - }); - - it("test moving more right when all is undefined", () => { - moveLexicographicallyTest( - [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined], - 1, - 4, - 5, - ); - }); - - it("test moving left when right is undefined", () => { - moveLexicographicallyTest( - ["20", undefined, undefined, undefined, undefined, undefined], - 4, - 2, - 2, - ); - }); - - it("test moving right when right is undefined", () => { - moveLexicographicallyTest( - ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined], - 1, - 4, - 4, - ); - }); - - it("test moving left when right is defined", () => { - moveLexicographicallyTest( - ["10", "20", "30", "40", undefined, undefined], - 3, - 1, - 1, - ); - }); - - it("test moving right when right is defined", () => { - moveLexicographicallyTest( - ["10", "20", "30", "40", "50", undefined], - 1, - 3, - 1, - ); - }); - - it("test moving left when all is defined", () => { - moveLexicographicallyTest( - ["11", "13", "15", "17", "19"], - 2, - 1, - 1, - ); - }); - - it("test moving right when all is defined", () => { - moveLexicographicallyTest( - ["11", "13", "15", "17", "19"], - 1, - 2, - 1, - ); - }); - - it("test moving left into no left space", () => { - moveLexicographicallyTest( - ["11", "12", "13", "14", "19"], - 3, - 1, - 2, - 2, - ); - - moveLexicographicallyTest( - [ - DEFAULT_ALPHABET.charAt(0), - // Target - DEFAULT_ALPHABET.charAt(1), - DEFAULT_ALPHABET.charAt(2), - DEFAULT_ALPHABET.charAt(3), - DEFAULT_ALPHABET.charAt(4), - DEFAULT_ALPHABET.charAt(5), - ], - 5, - 1, - 5, - 1, - ); - }); - - it("test moving right into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 1, - 3, - 3, - 2, - ); - - moveLexicographicallyTest( - [ - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), - ], - 1, - 3, - 3, - 1, - ); - }); - - it("test moving right into no left space", () => { - moveLexicographicallyTest( - ["11", "12", "13", "14", "15", "16", undefined], - 1, - 3, - 3, - ); - - moveLexicographicallyTest( - ["0", "1", "2", "3", "4", "5"], - 1, - 3, - 3, - 1, - ); - }); - - it("test moving left into no right space", () => { - moveLexicographicallyTest( - ["15", "16", "17", "18", "19"], - 4, - 3, - 4, - 2, - ); - - moveLexicographicallyTest( - [ - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), - DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), - ], - 4, - 3, - 4, - 1, - ); - }); - - const prev = (str: string) => baseToString(stringToBase(str) - BigInt(1)); - const next = (str: string) => baseToString(stringToBase(str) + BigInt(1)); - - it("baseN calculation is correctly consecutive", () => { - const str = "this-is-a-test"; - expect(next(prev(str))).toBe(str); - }); - - it("rolls over sanely", () => { - const maxSpaceValue = "~".repeat(50); - const fiftyFirstChar = " ".repeat(51); - expect(next(maxSpaceValue)).toBe(fiftyFirstChar); - expect(prev(fiftyFirstChar)).toBe(maxSpaceValue); - expect(Number(stringToBase(DEFAULT_ALPHABET[0]))).toEqual(1); - expect(Number(stringToBase(DEFAULT_ALPHABET[1]))).toEqual(2); - expect(DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1]).toBe("~"); - expect(DEFAULT_ALPHABET[0]).toBe(" "); + describe("reorderLexicographically", () => { + it("should work when moving left", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1); + }); + + it("should work when moving right", () => { + moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1); + }); + + it("should work when all orders are undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + 2, + ); + }); + + it("should work when moving to end and all orders are undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 1, + 4, + 5, + ); + }); + + it("should work when moving left and some orders are undefined", () => { + moveLexicographicallyTest( + ["a", "c", "e", undefined, undefined, undefined], + 5, + 2, + 1, + ); + + moveLexicographicallyTest( + ["a", "a", "e", undefined, undefined, undefined], + 5, + 1, + 2, + ); + }); + + it("should work moving to the start when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 2, + 0, + 1, + ); + }); + + it("should work moving to the end when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 3, + 4, + ); + }); + + it("should work moving left when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, undefined], + 4, + 1, + 2, + ); + }); + + it("should work moving right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined], + 1, + 2, + 3, + ); + }); + + it("should work moving more right when all is undefined", () => { + moveLexicographicallyTest( + [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 5, + ); + }); + + it("should work moving left when right is undefined", () => { + moveLexicographicallyTest( + ["20", undefined, undefined, undefined, undefined, undefined], + 4, + 2, + 2, + ); + }); + + it("should work moving right when right is undefined", () => { + moveLexicographicallyTest( + ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined], + 1, + 4, + 4, + ); + }); + + it("should work moving left when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", undefined, undefined], + 3, + 1, + 1, + ); + }); + + it("should work moving right when right is defined", () => { + moveLexicographicallyTest( + ["10", "20", "30", "40", "50", undefined], + 1, + 3, + 1, + ); + }); + + it("should work moving left when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 2, + 1, + 1, + ); + }); + + it("should work moving right when all is defined", () => { + moveLexicographicallyTest( + ["11", "13", "15", "17", "19"], + 1, + 2, + 1, + ); + }); + + it("should work moving left into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "19"], + 3, + 1, + 2, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(0), + // Target + DEFAULT_ALPHABET.charAt(1), + DEFAULT_ALPHABET.charAt(2), + DEFAULT_ALPHABET.charAt(3), + DEFAULT_ALPHABET.charAt(4), + DEFAULT_ALPHABET.charAt(5), + ], + 5, + 1, + 5, + 1, + ); + }); + + it("should work moving right into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 1, + 3, + 3, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), + ], + 1, + 3, + 3, + 1, + ); + }); + + it("should work moving right into no left space", () => { + moveLexicographicallyTest( + ["11", "12", "13", "14", "15", "16", undefined], + 1, + 3, + 3, + ); + + moveLexicographicallyTest( + ["0", "1", "2", "3", "4", "5"], + 1, + 3, + 3, + 1, + ); + }); + + it("should work moving left into no right space", () => { + moveLexicographicallyTest( + ["15", "16", "17", "18", "19"], + 4, + 3, + 4, + 2, + ); + + moveLexicographicallyTest( + [ + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2), + DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1), + ], + 4, + 3, + 4, + 1, + ); + }); }); }); From d4e376201f986223c16932229598c7e45a032c82 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 09:44:37 +0100 Subject: [PATCH 16/20] Break down the SpacePanel component --- src/components/views/spaces/SpacePanel.tsx | 134 ++++++++++++--------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 27f097e9d4d..2e3bfd157aa 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -14,18 +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 {_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, { UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, @@ -38,9 +38,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"; interface IButtonProps { space?: Room; @@ -121,11 +121,62 @@ const useSpaces = (): [Room[], Room[], Room | null] => { return [invites, spaces, activeSpace]; }; +interface IInnerSpacePanelProps { + children?: ReactNode; + isPanelCollapsed: boolean; + setPanelCollapsed: Dispatch>; +} + +// 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 }) => { + const [invites, spaces, activeSpace] = useSpaces(); + const activeSpaces = activeSpace ? [activeSpace] : []; + + return
    + SpaceStore.instance.setActiveSpace(null)} + selected={!activeSpace} + tooltip={_t("All rooms")} + notificationState={RoomNotificationStateStore.instance.globalState} + isNarrow={isPanelCollapsed} + /> + { invites.map(s => ( + setPanelCollapsed(false)} + /> + )) } + { spaces.map((s, i) => ( + + {(provided, snapshot) => ( + setPanelCollapsed(false)} + /> + )} + + )) } + { children } +
    ; +}); + 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(); - const [invites, spaces, activeSpace] = useSpaces(); const [isPanelCollapsed, setPanelCollapsed] = useState(true); useEffect(() => { @@ -134,10 +185,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 = ; @@ -204,7 +251,11 @@ const SpacePanel = () => { } }; - const activeSpaces = activeSpace ? [activeSpace] : []; + const onNewClick = menuDisplayed ? closeMenu : () => { + if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); + }; + return ( { if (!result.destination) return; // dropped outside the list @@ -226,59 +277,26 @@ const SpacePanel = () => { pointerEvents: "none", } : undefined} > -
    - SpaceStore.instance.setActiveSpace(null)} - selected={!activeSpace} - tooltip={_t("All rooms")} - notificationState={RoomNotificationStateStore.instance.globalState} - isNarrow={isPanelCollapsed} - /> - { invites.map(s => ( - setPanelCollapsed(false)} - /> - )) } - { spaces.map((s, i) => ( - - {(provided, snapshot) => ( - setPanelCollapsed(false)} - /> - )} - - )) } + { provided.placeholder } -
    + + { - if (!isPanelCollapsed) setPanelCollapsed(true); - openMenu(); - }} + onClick={onNewClick} isNarrow={isPanelCollapsed} /> )} setPanelCollapsed(!isPanelCollapsed)} title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")} /> From e7fde2686f43da4b9e680eb12ba8b4054f903d8d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 12:12:00 +0100 Subject: [PATCH 17/20] remove unused imports --- test/utils/stringOrderField-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts index ece3043d868..331627dfc0f 100644 --- a/test/utils/stringOrderField-test.ts +++ b/test/utils/stringOrderField-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { sortBy } from "lodash"; -import { stringToBase, baseToString, averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; +import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils"; import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField"; From 7948aa6181ec34cd2c55a3945721af747321bb40 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 21:10:29 +0100 Subject: [PATCH 18/20] Iterate PR, improve jsdoc and switch function style --- src/utils/arrays.ts | 8 ++++---- src/utils/stringOrderField.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 148861e5d35..6524debfb7a 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -225,10 +225,10 @@ export function arrayMerge(...a: T[][]): T[] { /** * Moves a single element from fromIndex to toIndex. - * @param list the list from which to construct the new list. - * @param fromIndex the index of the element to move. - * @param toIndex the index of where to put the element. - * @returns A new array with the requested value moved. + * @param {array} list the list from which to construct the new list. + * @param {number} fromIndex the index of the element to move. + * @param {number} toIndex the index of where to put the element. + * @returns {array} A new array with the requested value moved. */ export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] { const result = Array.from(list); diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts index b312b85b085..da840792ee8 100644 --- a/src/utils/stringOrderField.ts +++ b/src/utils/stringOrderField.ts @@ -18,13 +18,13 @@ import { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matri import { moveElement } from "./arrays"; -export const midPointsBetweenStrings = ( +export function midPointsBetweenStrings( a: string, b: string, count: number, maxLen: number, alphabet = DEFAULT_ALPHABET, -): string[] => { +): string[] { const padN = Math.min(Math.max(a.length, b.length), maxLen); const padA = alphabetPad(a, padN, alphabet); const padB = alphabetPad(b, padN, alphabet); @@ -48,7 +48,7 @@ export const midPointsBetweenStrings = ( const step = (baseB - baseA) / BigInt(count + 1); const start = BigInt(baseA + step); return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet)); -}; +} interface IEntry { index: number; From 99e3aea1e5eb0cc5f25742ab430fa7dcf18edc83 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 21:17:33 +0100 Subject: [PATCH 19/20] i18n and regen yarn lock --- src/i18n/strings/en_EN.json | 8 +- yarn.lock | 202 ++++++++++++++++-------------------- 2 files changed, 95 insertions(+), 115 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a94b608f2b9..4ca011f4048 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1025,10 +1025,10 @@ "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", - "Expand space panel": "Expand space panel", - "Collapse space panel": "Collapse space panel", "All rooms": "All rooms", "Home": "Home", + "Expand space panel": "Expand space panel", + "Collapse space panel": "Collapse space panel", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", @@ -2511,6 +2511,8 @@ "Update status": "Update status", "Set status": "Set status", "Set a new status...": "Set a new status...", + "Move up": "Move up", + "Move down": "Move down", "View Community": "View Community", "Unable to start audio streaming.": "Unable to start audio streaming.", "Failed to start livestream": "Failed to start livestream", @@ -2657,7 +2659,7 @@ "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", + "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", diff --git a/yarn.lock b/yarn.lock index b19a188014f..0d424cb93dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,13 +1017,20 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" + integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1504,6 +1511,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1620,6 +1635,13 @@ dependencies: "@types/node" "*" +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== + dependencies: + "@types/react" "*" + "@types/react-dom@^17.0.2": version "17.0.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc" @@ -1627,6 +1649,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -2122,14 +2154,6 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2648,11 +2672,6 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0: - version "2.6.12" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" - integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2712,6 +2731,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-select@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" @@ -4241,7 +4267,7 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4456,13 +4482,6 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5600,11 +5619,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.2.1: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== - lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" @@ -5625,7 +5639,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5794,10 +5808,10 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -memoize-one@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" - integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== meow@^9.0.0: version "9.0.0" @@ -6443,11 +6457,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -6657,7 +6666,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6734,12 +6743,12 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf-schd@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62" - integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== -raf@^3.1.0, raf@^3.4.1: +raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -6766,21 +6775,18 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" -react-beautiful-dnd@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" - integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA== - dependencies: - babel-runtime "^6.26.0" - invariant "^2.2.2" - memoize-one "^3.0.1" - prop-types "^15.6.0" - raf-schd "^2.1.0" - react-motion "^0.5.2" - react-redux "^5.0.6" - redux "^3.7.2" - redux-thunk "^2.2.0" - reselect "^3.0.1" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" react-clientside-effect@^1.2.2: version "1.2.3" @@ -6815,7 +6821,7 @@ react-focus-lock@^2.5.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6825,32 +6831,17 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - -react-redux@^5.0.6: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== +react-redux@^7.2.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" react-shallow-renderer@^16.13.1: version "16.14.1" @@ -6979,20 +6970,12 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-thunk@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" - integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== - -redux@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" - integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== +redux@^4.0.0, redux@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== dependencies: - lodash "^4.2.1" - lodash-es "^4.2.1" - loose-envify "^1.1.0" - symbol-observable "^1.0.3" + "@babel/runtime" "^7.9.2" regenerate-unicode-properties@^8.2.0: version "8.2.0" @@ -7006,11 +6989,6 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -7168,11 +7146,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -reselect@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= - resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -7895,11 +7868,6 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -symbol-observable@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -7962,6 +7930,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -8277,6 +8250,11 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-sidecar@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46" From 49d20d253034ad945708da53e8d7b3f19ca7f15d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 21:22:30 +0100 Subject: [PATCH 20/20] consolidate the two onRoomAccountData listeners --- src/stores/SpaceStore.tsx | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 8b4e3a8f3a8..e498574467f 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -527,17 +527,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; - private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { - if (!room.isSpaceRoom() || ev.getType() !== EventType.SpaceOrder) return; - - this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo - const order = ev.getContent()?.order; - const lastOrder = lastEv?.getContent()?.order; - if (order !== lastOrder) { - this.notifyIfOrderChanged(); - } - }; - private notifyIfOrderChanged(): void { const rootSpaces = this.sortRootSpaces(this.rootSpaces); if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) { @@ -577,10 +566,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; - private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { - if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => { + if (!room.isSpaceRoom()) return; + + if (ev.getType() === EventType.SpaceOrder) { + this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo + const order = ev.getContent()?.order; + const lastOrder = lastEv?.getContent()?.order; + if (order !== lastOrder) { + this.notifyIfOrderChanged(); + } + } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) { // If the room was in favourites and now isn't or the opposite then update its position in the trees - const oldTags = lastEvent?.getContent()?.tags || {}; + const oldTags = lastEv?.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {}; if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { this.onRoomUpdate(room); @@ -625,7 +623,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); if (!SettingsStore.getValue("feature_spaces.all_rooms")) { - this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("accountData", this.onAccountData); } } @@ -639,7 +636,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); if (!SettingsStore.getValue("feature_spaces.all_rooms")) { - this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("accountData", this.onAccountData); }