diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 7aa20ab3..727fe5e3 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -36,6 +36,7 @@ import { getSpaceRoomPath } from '../../pages/pathUtils'; import { HierarchyItemMenu } from './HierarchyItemMenu'; import { StateEvent } from '../../../types/matrix/room'; import { AfterItemDropTarget, CanDropCallback, useDnDMonitor } from './DnD'; +import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable'; export function Lobby() { const navigate = useNavigate(); @@ -45,6 +46,7 @@ export function Lobby() { const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const space = useSpace(); const spacePowerLevels = usePowerLevels(space); + const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []); const scrollRef = useRef(null); const heroSectionRef = useRef(null); @@ -144,13 +146,118 @@ export function Lobby() { [getRoom, space.roomId, roomsPowerLevels, canEditSpaceChild] ); + const reorderSpace = useCallback( + (item: HierarchyItem, containerItem: HierarchyItem) => { + if (!item.parentId) return; + + const childItems = flattenHierarchy + .filter((i) => i.parentId && i.space) + .filter((i) => i.roomId !== item.roomId); + + const beforeIndex = childItems.findIndex((i) => i.roomId === containerItem.roomId); + const insertIndex = beforeIndex + 1; + + childItems.splice(insertIndex, 0, { + ...item, + content: { ...item.content, order: undefined }, + }); + + const currentOrders = childItems.map((i) => { + if (typeof i.content.order === 'string' && lex.has(i.content.order)) { + return i.content.order; + } + return undefined; + }); + + const newOrders = orderKeys(lex, currentOrders); + + newOrders?.forEach((orderKey, index) => { + const itm = childItems[index]; + if (!itm || !itm.parentId) return; + const parentPL = roomsPowerLevels.get(itm.parentId); + const canEdit = parentPL && canEditSpaceChild(parentPL); + if (canEdit && orderKey !== currentOrders[index]) { + mx.sendStateEvent( + itm.parentId, + StateEvent.SpaceChild, + { ...itm.content, order: orderKey }, + itm.roomId + ); + } + }); + }, + [mx, flattenHierarchy, lex, roomsPowerLevels, canEditSpaceChild] + ); + + const reorderRoom = useCallback( + (item: HierarchyItem, containerItem: HierarchyItem): void => { + if (!item.parentId) { + return; + } + const containerParentId: string = containerItem.space + ? containerItem.roomId + : containerItem.parentId; + const itemContent = item.content; + + if (item.parentId !== containerParentId) { + mx.sendStateEvent(item.parentId, StateEvent.SpaceChild, {}, item.roomId); + } + + const childItems = flattenHierarchy + .filter((i) => i.parentId === containerParentId && !item.space) + .filter((i) => i.roomId !== item.roomId); + + const beforeItem: HierarchyItem | undefined = containerItem.space ? undefined : containerItem; + const beforeIndex = childItems.findIndex((i) => i.roomId === beforeItem?.roomId); + const insertIndex = beforeIndex + 1; + + childItems.splice(insertIndex, 0, { + ...item, + parentId: containerParentId, + content: { ...itemContent, order: undefined }, + }); + + const currentOrders = childItems.map((i) => { + if (typeof i.content.order === 'string' && lex.has(i.content.order)) { + return i.content.order; + } + return undefined; + }); + + const newOrders = orderKeys(lex, currentOrders); + + newOrders?.forEach((orderKey, index) => { + const itm = childItems[index]; + if (itm && orderKey !== currentOrders[index]) { + mx.sendStateEvent( + containerParentId, + StateEvent.SpaceChild, + { ...itm.content, order: orderKey }, + itm.roomId + ); + } + }); + }, + [mx, flattenHierarchy, lex] + ); + useDnDMonitor( scrollRef, setDraggingItem, - useCallback((item, container) => { - console.log(item, container); - // TODO: prompt when dragging restricted room of private space to public space and etc. - }, []) + useCallback( + (item, container) => { + if (!canDrop(item, container)) { + return; + } + if (item.space) { + reorderSpace(item, container.item); + } else { + reorderRoom(item, container.item); + // TODO: prompt when dragging restricted room of private space to public space and etc. + } + }, + [reorderRoom, reorderSpace, canDrop] + ) ); const addSpaceRoom = (roomId: string) => setSpaceRooms({ type: 'PUT', roomId }); diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 6a336ad1..b0c52598 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -6,6 +6,7 @@ import { roomToParentsAtom } from '../state/room/roomToParents'; import { MSpaceChildContent, StateEvent } from '../../types/matrix/room'; import { getAllParents, getStateEvents, isValidChild } from '../utils/room'; import { isRoomId } from '../utils/matrix'; +import { SortFunc } from '../utils/sort'; export type HierarchyItem = | { @@ -22,6 +23,26 @@ export type HierarchyItem = space?: false; parentId: string; }; + +const hierarchyItemByOrder: SortFunc = (a, b) => { + const aOrder = a.content.order; + const bOrder = b.content.order; + const aTs = a.ts; + const bTs = b.ts; + + if (!aOrder && !bOrder) { + return aTs - bTs; + } + + if (!bOrder) return -1; + if (!aOrder) return 1; + + if (aOrder > bOrder) { + return 1; + } + return 0; +}; + const getFlattenSpaceHierarchy = ( rootSpaceId: string, spaceRooms: Set, @@ -66,10 +87,9 @@ const getFlattenSpaceHierarchy = ( }; findAndCollectHierarchySpaces(rootSpaceItem); - // TODO: sort by order + added ts spaceItems = [ rootSpaceItem, - ...spaceItems.filter((item) => item.roomId !== rootSpaceId).sort((a, b) => a.ts - b.ts), + ...spaceItems.filter((item) => item.roomId !== rootSpaceId).sort(hierarchyItemByOrder), ]; const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => { @@ -93,7 +113,7 @@ const getFlattenSpaceHierarchy = ( }; childItems.push(childItem); }); - return [spaceItem, ...childItems.sort((a, b) => a.ts - b.ts)]; + return [spaceItem, ...childItems.sort(hierarchyItemByOrder)]; }); return hierarchy; diff --git a/src/app/utils/ASCIILexicalTable.ts b/src/app/utils/ASCIILexicalTable.ts index c9001876..61b2481a 100644 --- a/src/app/utils/ASCIILexicalTable.ts +++ b/src/app/utils/ASCIILexicalTable.ts @@ -185,16 +185,53 @@ export class ASCIILexicalTable { return undefined; } - between(i: string, j: string): string | undefined { - if (!this.has(i) || !this.has(j)) { + between(a: string, b: string): string | undefined { + if (!this.has(a) || !this.has(b)) { return undefined; } - const centerIndex = Math.floor((this.index(i) + this.index(j)) / 2); + const centerIndex = Math.floor((this.index(a) + this.index(b)) / 2); - const b = this.get(centerIndex); - if (b === i || b === j) return undefined; - return b; + const str = this.get(centerIndex); + if (str === a || str === b) return undefined; + return str; + } + + nBetween(n: number, a: string, b: string): string[] | undefined { + if (n <= 0 || !this.has(a) || !this.has(b)) { + return undefined; + } + + const indexA = this.index(a); + const indexB = this.index(b); + + const nBetween = Math.max(indexA, indexB) - Math.min(indexA, indexB); + if (nBetween < n) { + return undefined; + } + const segmentSize = Math.floor(nBetween / (n + 1)); + if (segmentSize === 0) return undefined; + + const items: string[] = []; + + for ( + let segmentIndex = indexA + segmentSize; + segmentIndex < indexB; + segmentIndex += segmentSize + ) { + if (items.length === n) break; + + const str = this.get(segmentIndex); + + if (!str) break; + items.push(str); + } + + if (items.length < n) { + return undefined; + } + + return items; } } @@ -234,10 +271,12 @@ export class ASCIILexicalTable { // console.log('\n'); -// const lex = new ASCIILexicalTable('a'.charCodeAt(0), 'c'.charCodeAt(0), 5); +// const lex = new ASCIILexicalTable('a'.charCodeAt(0), 'c'.charCodeAt(0), 3); // printLex(lex); -// console.log(lex.between('a', 'aacb')); -// console.log(lex.get(123) === 'baa'); +// console.log(lex.size()); +// console.log(lex.nBetween(8, ' ', '~~~~~')); +// console.log(lex.between('a', 'ccc')); +// console.log(lex.get(11)); // console.log(lex.get(11) === 'aaac'); // const lex4 = new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 5); @@ -291,3 +330,64 @@ export class ASCIILexicalTable { // }; // perf(); + +const findNextFilledKey = ( + fromIndex: number, + keys: Array +): [number, string] | [-1, undefined] => { + for (let j = fromIndex; j < keys.length; j += 1) { + const key = keys[j]; + if (typeof key === 'string') { + return [j, key]; + } + } + + return [-1, undefined]; +}; + +export const orderKeys = ( + lex: ASCIILexicalTable, + keys: Array +): Array | undefined => { + const newKeys: string[] = []; + + for (let i = 0; i < keys.length; ) { + const key = keys[i]; + const collectedKeys: string[] = []; + const [nextKeyIndex, nextKey] = findNextFilledKey(i + 1, keys); + const isKey = typeof key === 'string'; + + if (isKey) { + collectedKeys.push(key); + } + + const keyToGenerateCount = + (nextKeyIndex === -1 ? keys.length : nextKeyIndex) - (key ? i + 1 : i + 0); + + if (keyToGenerateCount > 0) { + const generatedKeys = lex.nBetween( + keyToGenerateCount, + key ?? lex.first(), + nextKey ?? lex.last() + ); + if (generatedKeys) { + collectedKeys.push(...generatedKeys); + } else { + return lex.nBetween(keys?.length, lex.first(), lex.last()); + } + } + + newKeys.push(...collectedKeys); + i += collectedKeys.length; + } + + if (newKeys.length !== keys.length) { + return undefined; + } + + return newKeys; +}; + +// const lex = new ASCIILexicalTable('a'.charCodeAt(0), 'b'.charCodeAt(0), 2); +// const keys = [undefined, undefined]; +// console.log(orderKeys(lex, keys));