diff --git a/.changeset/implements_improved_subspace_rendering.md b/.changeset/implements_improved_subspace_rendering.md deleted file mode 100644 index b45aa6aff..000000000 --- a/.changeset/implements_improved_subspace_rendering.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -default: minor ---- - -# Implemented improved subspace rendering - -Navbar and Lobby view now represent the hierarchy of subspaces via guide lines. -Depth of subspace limit is customizable with a setting. -Also changes the navbar collapse/extpanded arrows to be on the right of the categories. diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx index 50cfc6717..80ace407b 100644 --- a/src/app/features/add-existing/AddExisting.tsx +++ b/src/app/features/add-existing/AddExisting.tsx @@ -83,39 +83,13 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM const allRoomsSet = useAllJoinedRoomsSet(); const getRoom = useGetRoom(allRoomsSet); - /** - * Recursively checks if a given sourceId room is an ancestor to the targetId space. - * - * @param sourceId - The room to check. - * @param targetId - The space ID to check against. - * @param visited - Set used to prevent recursion errors. - * @returns True if rId is an ancestor of targetId. - */ - const isAncestor = useCallback( - (sourceId: string, targetId: string, visited: Set = new Set()): boolean => { - // Prevent infinite recursion - if (visited.has(targetId)) return false; - visited.add(targetId); - - const parentIds = roomIdToParents.get(targetId); - if (!parentIds) return false; - - if (parentIds.has(sourceId)) { - return true; - } - - return Array.from(parentIds).some((id) => isAncestor(sourceId, id, visited)); - }, - [roomIdToParents] - ); - const allItems: string[] = useMemo(() => { const rIds = space ? [...spaces] : [...rooms, ...directs]; return rIds - .filter((rId) => rId !== parentId && !isAncestor(rId, parentId)) + .filter((rId) => rId !== parentId && !roomIdToParents.get(rId)?.has(parentId)) .sort(factoryRoomIdByAtoZ(mx)); - }, [space, spaces, rooms, directs, mx, parentId, isAncestor]); + }, [spaces, rooms, directs, space, parentId, roomIdToParents, mx]); const getRoomNameStr: SearchItemStrGetter = useCallback( (rId) => getRoom(rId)?.name ?? rId, diff --git a/src/app/features/lobby/DnD.css.ts b/src/app/features/lobby/DnD.css.ts index 2b7e5e363..347382568 100644 --- a/src/app/features/lobby/DnD.css.ts +++ b/src/app/features/lobby/DnD.css.ts @@ -11,7 +11,7 @@ export const ItemDraggableTarget = style([ top: 0, zIndex: 1, cursor: 'grab', - borderRadius: 0, + borderRadius: config.radii.R400, opacity: config.opacity.P300, ':active': { diff --git a/src/app/features/lobby/HierarchyItemMenu.tsx b/src/app/features/lobby/HierarchyItemMenu.tsx index 1377f33ff..bbaf3885f 100644 --- a/src/app/features/lobby/HierarchyItemMenu.tsx +++ b/src/app/features/lobby/HierarchyItemMenu.tsx @@ -30,9 +30,6 @@ import { IPowerLevels } from '$hooks/usePowerLevels'; import { getRoomCreatorsForRoomId } from '$hooks/useRoomCreators'; import { getRoomPermissionsAPI } from '$hooks/useRoomPermissions'; import { InviteUserPrompt } from '$components/invite-user-prompt'; -import { getCanonicalAliasOrRoomId } from '$utils/matrix'; -import { useNavigate } from 'react-router-dom'; -import { getSpaceLobbyPath } from '$pages/pathUtils'; type HierarchyItemWithParent = HierarchyItem & { parentId: string; @@ -230,7 +227,6 @@ export function HierarchyItemMenu({ }; const handleRequestClose = useCallback(() => setMenuAnchor(undefined), []); - const navigate = useNavigate(); if (!joined && !canEditChild) { return null; @@ -282,17 +278,6 @@ export function HierarchyItemMenu({ )} - { - navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, item.roomId))); - }} - > - - Open Lobby - - { - closedCategoriesCache.current.clear(); - }, [closedCategories, roomToParents, getRoom]); - - /** - * Recursively checks if a given parentId (or all its ancestors) is in a closed category. - * - * @param spaceId - The root space ID. - * @param parentId - The parent space ID to start the check from. - * @param previousId - The last ID checked, only used to ignore root collapse state. - * @param visited - Set used to prevent recursion errors. - * @returns True if parentId or all ancestors is in a closed category. - */ - const getInClosedCategories = useCallback( - ( - spaceId: string, - parentId: string, - previousId?: string, - visited: Set = new Set() - ): boolean => { - // Ignore root space being collapsed if in a subspace, - // this is due to many spaces dumping all rooms in the top-level space. - if (parentId === spaceId && previousId) { - if (spaceRooms.has(previousId) || getRoom(previousId)?.isSpaceRoom()) { - return false; - } - } - - const categoryId = makeLobbyCategoryId(spaceId, parentId); - - // Prevent infinite recursion - if (visited.has(categoryId)) return false; - visited.add(categoryId); - - if (closedCategoriesCache.current.has(categoryId)) { - return closedCategoriesCache.current.get(categoryId); - } - - if (closedCategories.has(categoryId)) { - closedCategoriesCache.current.set(categoryId, true); - return true; - } - - const parentParentIds = roomToParents.get(parentId); - if (!parentParentIds || parentParentIds.size === 0) { - closedCategoriesCache.current.set(categoryId, false); - return false; - } - - // As a subspace can be in multiple spaces, - // only return true if all parent spaces are closed. - const allClosed = !Array.from(parentParentIds).some( - (id) => !getInClosedCategories(spaceId, id, parentId, visited) - ); - visited.delete(categoryId); - closedCategoriesCache.current.set(categoryId, allClosed); - return allClosed; - }, - [closedCategories, getRoom, roomToParents, spaceRooms] - ); - - /** - * Determines whether all parent categories are collapsed. - * - * @param spaceId - The root space ID. - * @param roomId - The room ID to start the check from. - * @returns True if every parent category is collapsed; false otherwise. - */ - const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { - const parentIds = roomToParents.get(roomId); - - if (!parentIds || parentIds.size === 0) { - return false; - } - - return !Array.from(parentIds).some((id) => !getInClosedCategories(spaceId, id, roomId)); - }; - - const [subspaceHierarchyLimit] = useSetting(settingsAtom, 'subspaceHierarchyLimit'); const [draggingItem, setDraggingItem] = useState(); const hierarchy = useSpaceHierarchy( space.roomId, spaceRooms, getRoom, - useCallback( - (_childId, _spaceId, depth) => depth >= subspaceHierarchyLimit, - [subspaceHierarchyLimit] - ), useCallback( (childId) => - getInClosedCategories(space.roomId, childId) || + closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) || (draggingItem ? 'space' in draggingItem : false), - [draggingItem, getInClosedCategories, space.roomId] + [closedCategories, space.roomId, draggingItem] ) ); @@ -409,7 +298,7 @@ export function Lobby() { // remove from current space if (item.parentId !== containerParentId) { - await mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId); + mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId); } if ( @@ -428,7 +317,7 @@ export function Lobby() { joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? []; allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId }); - await mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, { + mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, { ...joinRuleContent, allow, }); @@ -514,18 +403,9 @@ export function Lobby() { [setSpaceRooms] ); - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { - const collapsed = closedCategories.has(categoryId); - const [spaceId, roomId] = getLobbyCategoryIdParts(categoryId); - - // Prevent collapsing if all parents are collapsed - const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); - - if (toggleable) { - return collapsed; - } - return !collapsed; - }); + const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => + closedCategories.has(categoryId) + ); const handleOpenRoom: MouseEventHandler = (evt) => { const rId = evt.currentTarget.getAttribute('data-room-id'); @@ -546,77 +426,6 @@ export function Lobby() { [mx, sidebarItems, sidebarSpaces] ); - const getPaddingTop = (vItem: VirtualItem) => { - if (vItem.index === 0) return 0; - const prevDepth = hierarchy[vItem.index - 1]?.space.depth ?? 0; - const { depth } = hierarchy[vItem.index].space; - if (depth !== 1 && depth >= prevDepth) return config.space.S200; - return config.space.S500; - }; - - const getConnectorSVG = useCallback( - (virtualizedItems: VirtualItem[]): ReactElement => { - const PADDING_LEFT_DEPTH_OFFSET = 15.75; - const PADDING_LEFT_DEPTH_OFFSET_START = -15; - - let aY = 0; - // Holder for the paths - const pathHolder: ReactElement[] = []; - virtualizedItems.forEach((vItem) => { - const { depth } = hierarchy[vItem.index].space ?? {}; - - // We will render spaces at a level above their normal depth, since we want their children to be "under" them - // for the root items, we are not doing anything with it. - if (depth < 1) { - return; - } - // for the sub-root items, we will not draw any arcs from root to it. - // however, we should capture the aX and aY to draw starter arcs for next depths. - if (depth === 1) { - aY = vItem.end; - return; - } - - const pathStrings: string[] = []; - - for (let iDepth = 0; iDepth < depth; iDepth += 1) { - const X = iDepth * PADDING_LEFT_DEPTH_OFFSET + PADDING_LEFT_DEPTH_OFFSET_START; - - const bY = vItem.end; - - pathStrings.push(`M ${X} ${aY} L ${X} ${bY}`); - } - - pathHolder.push( - - ); - - aY = vItem.end; - }); - - return ( - - {pathHolder} - - ); - }, - [hierarchy] - ); - return ( @@ -658,69 +467,45 @@ export function Lobby() { const item = hierarchy[vItem.index]; if (!item) return null; const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId; - const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId); - const inClosedCategory = getInClosedCategories( - space.roomId, - item.space.roomId - ); - const paddingLeft = `calc((${item.space.depth} - 1) * ${config.space.S400})`; + const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId); return ( - {item.space.depth !== subspaceHierarchyLimit ? ( - - ) : ( - - )} + ); })} - {getConnectorSVG(vItems)} {reordering && ( void; onOpenRoom: MouseEventHandler; }; -export const SpaceHierarchyItem = forwardRef( +export const SpaceHierarchy = forwardRef( ( { summary, @@ -98,22 +98,18 @@ export const SpaceHierarchyItem = forwardRef { - let childItemsMut = roomItems?.filter((i) => !subspaces.has(i.roomId)); - if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) { - // hide unknown rooms for normal user - childItemsMut = childItems?.filter((i) => { - const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false; - const inaccessibleRoom = !rooms.get(i.roomId) && !fetching && (error ? forbidden : true); - return !inaccessibleRoom; - }); - } - setChildItems(childItemsMut); - }, [childItems, spacePermissions, mx, roomItems, subspaces, fetching, error, rooms]); + let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId)); + if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) { + // hide unknown rooms for normal user + childItems = childItems?.filter((i) => { + const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false; + const inaccessibleRoom = !rooms.get(i.roomId) && !fetching && (error ? forbidden : true); + return !inaccessibleRoom; + }); + } return ( - + {childItems && childItems.length > 0 ? ( - + {childItems.map((roomItem, index) => { const roomSummary = rooms.get(roomItem.roomId); @@ -208,19 +204,22 @@ export const SpaceHierarchyItem = forwardRef ) : ( childItems && ( - + + + No Rooms + - This space does not contain any rooms. + This space does not contains rooms yet. diff --git a/src/app/features/lobby/SpaceHierarchyNavItem.tsx b/src/app/features/lobby/SpaceHierarchyNavItem.tsx deleted file mode 100644 index def804f62..000000000 --- a/src/app/features/lobby/SpaceHierarchyNavItem.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { forwardRef } from 'react'; -import { Room, IHierarchyRoom } from '$types/matrix-sdk'; -import { Box } from 'folds'; -import { HierarchyItem, HierarchyItemSpace } from '$hooks/useSpaceHierarchy'; -import { IPowerLevels } from '$hooks/usePowerLevels'; -import { useMatrixClient } from '$hooks/useMatrixClient'; -import { StateEvent } from '$types/matrix/room'; -import { getRoomCreatorsForRoomId } from '$hooks/useRoomCreators'; -import { getRoomPermissionsAPI } from '$hooks/useRoomPermissions'; -import { AfterItemDropTarget, CanDropCallback } from './DnD'; -import { HierarchyItemMenu } from './HierarchyItemMenu'; -import { SpaceNavItemCard } from './SpaceNavItem'; - -type SpaceHierarchyNavItemProps = { - summary: IHierarchyRoom | undefined; - spaceItem: HierarchyItemSpace; - allJoinedRooms: Set; - roomsPowerLevels: Map; - categoryId: string; - draggingItem?: HierarchyItem; - onDragging: (item?: HierarchyItem) => void; - canDrop: CanDropCallback; - disabledReorder?: boolean; - nextSpaceId?: string; - pinned: boolean; - togglePinToSidebar: (roomId: string) => void; - getRoom: (roomId: string) => Room | undefined; -}; -export const SpaceHierarchyNavItem = forwardRef( - ( - { - summary, - spaceItem, - allJoinedRooms, - roomsPowerLevels, - categoryId, - draggingItem, - onDragging, - canDrop, - disabledReorder, - nextSpaceId, - pinned, - togglePinToSidebar, - getRoom, - }, - ref - ) => { - const mx = useMatrixClient(); - - const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId); - - const draggingSpace = - draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId; - - const { parentId } = spaceItem; - const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined; - const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined; - const parentPermissions = - parentCreators && - parentPowerLevels && - getRoomPermissionsAPI(parentCreators, parentPowerLevels); - - return ( - - - ) - } - getRoom={getRoom} - after={ - - } - onDragging={onDragging} - data-dragging={draggingSpace} - /> - - ); - } -); diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index 3a95049d7..c8ec05f22 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -11,11 +11,12 @@ import { toRem, Spinner, PopOut, - config, Menu, MenuItem, RectCords, + config, } from 'folds'; +import FocusTrap from 'focus-trap-react'; import classNames from 'classnames'; import { MatrixError, Room, IHierarchyRoom } from '$types/matrix-sdk'; import { HierarchyItem } from '$hooks/useSpaceHierarchy'; @@ -25,18 +26,17 @@ import { nameInitials } from '$utils/common'; import { LocalRoomSummaryLoader } from '$components/RoomSummaryLoader'; import { getRoomAvatarUrl } from '$utils/room'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; +import { stopPropagation } from '$utils/keyboard'; import { mxcUrlToHttp } from '$utils/matrix'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; -import { BetaNoticeBadge } from '$components/BetaNoticeBadge'; -import { CreateRoomType } from '$components/create-room'; -import { AddExistingModal } from '$features/add-existing'; import { useOpenCreateRoomModal } from '$state/hooks/createRoomModal'; import { useOpenCreateSpaceModal } from '$state/hooks/createSpaceModal'; -import { stopPropagation } from '$utils/keyboard'; -import FocusTrap from 'focus-trap-react'; -import * as css from './SpaceItem.css'; -import * as styleCss from './style.css'; +import { CreateRoomType } from '$components/create-room/types'; +import { AddExistingModal } from '$features/add-existing'; +import { BetaNoticeBadge } from '$components/BetaNoticeBadge'; import { useDraggableItem } from './DnD'; +import * as styleCss from './style.css'; +import * as css from './SpaceItem.css'; function SpaceProfileLoading() { return ( @@ -57,7 +57,7 @@ type InaccessibleSpaceProfileProps = { roomId: string; suggested?: boolean; }; -export function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) { +function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) { return ( } > - {item.parentId === undefined && ( - } - onClick={handleAddRoom} - aria-pressed={!!cords} - > - Add Room - - )} + } + onClick={handleAddRoom} + aria-pressed={!!cords} + > + Add Room + {addExisting && ( setAddExisting(false)} /> )} @@ -372,17 +370,15 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { } > - {item.parentId === undefined && ( - } - onClick={handleAddSpace} - aria-pressed={!!cords} - > - Add Space - - )} + } + onClick={handleAddSpace} + aria-pressed={!!cords} + > + Add Space + {addExisting && ( setAddExisting(false)} /> )} @@ -506,7 +502,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>( {space && canEditChild && ( - + {item.parentId === undefined && } )} diff --git a/src/app/features/lobby/SpaceNavItem.css.ts b/src/app/features/lobby/SpaceNavItem.css.ts deleted file mode 100644 index c9d1b2c7a..000000000 --- a/src/app/features/lobby/SpaceNavItem.css.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { config, toRem } from 'folds'; -import { recipe } from '@vanilla-extract/recipes'; - -export const SpaceItemCard = recipe({ - base: { - paddingBottom: config.space.S100, - borderBottom: `${config.borderWidth.B300} solid transparent`, - position: 'relative', - selectors: { - '&[data-dragging=true]': { - opacity: config.opacity.Disabled, - }, - }, - }, -}); -export const HeaderChip = style({ - paddingLeft: config.space.S200, - selectors: { - [`&[data-ui-before="true"]`]: { - paddingLeft: config.space.S100, - }, - }, -}); -export const HeaderChipPlaceholder = style([ - { - borderRadius: config.radii.R400, - paddingLeft: config.space.S100, - paddingRight: config.space.S300, - height: toRem(32), - }, -]); diff --git a/src/app/features/lobby/SpaceNavItem.tsx b/src/app/features/lobby/SpaceNavItem.tsx deleted file mode 100644 index c07ce6cbc..000000000 --- a/src/app/features/lobby/SpaceNavItem.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { ReactNode, useRef } from 'react'; -import { Avatar, Badge, Box, Chip, Icon, Icons, as, Text } from 'folds'; -import classNames from 'classnames'; -import { IHierarchyRoom, MatrixClient, Room } from '$types/matrix-sdk'; -import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getCanonicalAliasOrRoomId, mxcUrlToHttp } from '$utils/matrix'; -import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; -import { HierarchyItem } from '$hooks/useSpaceHierarchy'; -import { LocalRoomSummaryLoader } from '$components/RoomSummaryLoader'; -import { getRoomAvatarUrl } from '$utils/room'; -import { RoomAvatar } from '$components/room-avatar'; -import { nameInitials } from '$utils/common'; -import { useNavigate } from 'react-router-dom'; -import { getSpaceLobbyPath } from '$pages/pathUtils'; -import { InaccessibleSpaceProfile, UnjoinedSpaceProfile } from './SpaceItem'; -import * as css from './SpaceNavItem.css'; -import { useDraggableItem } from './DnD'; - -type SpaceProfileProps = { - roomId: string; - name: string; - avatarUrl?: string; - suggested?: boolean; - categoryId: string; - mx: MatrixClient; -}; -function SpaceNavProfile({ - roomId, - name, - avatarUrl, - suggested, - categoryId, - mx, -}: SpaceProfileProps) { - const navigate = useNavigate(); - return ( - navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, roomId)))} - before={ - - ( - - {nameInitials(name)} - - )} - /> - - } - after={} - > - - - {name} - - {suggested && ( - - Suggested - - )} - - - ); -} - -type SpaceNavItemCardProps = { - summary: IHierarchyRoom | undefined; - item: HierarchyItem; - joined?: boolean; - categoryId: string; - options?: ReactNode; - before?: ReactNode; - after?: ReactNode; - canReorder: boolean; - getRoom: (roomId: string) => Room | undefined; - onDragging: (item?: HierarchyItem) => void; -}; -export const SpaceNavItemCard = as<'div', SpaceNavItemCardProps>( - ( - { - className, - summary, - joined, - categoryId, - item, - options, - before, - after, - canReorder, - onDragging, - getRoom, - ...props - }, - ref - ) => { - const mx = useMatrixClient(); - const useAuthentication = useMediaAuthentication(); - const { roomId, content } = item; - // const spaceNav = getRoom(roomId); - const targetRef = useRef(null); - useDraggableItem(item, targetRef, onDragging); - const space = getRoom(item.roomId); - - return ( - - {before} - - - {space ? ( - - {(localSummary) => ( - - )} - - ) : ( - <> - {!summary && ( - - )} - {summary && ( - - )} - - )} - - - {options} - {after} - - ); - } -); diff --git a/src/app/features/room-nav/RoomNavCategoryButton.tsx b/src/app/features/room-nav/RoomNavCategoryButton.tsx index 3df48aa7d..7adc6dcb9 100644 --- a/src/app/features/room-nav/RoomNavCategoryButton.tsx +++ b/src/app/features/room-nav/RoomNavCategoryButton.tsx @@ -7,8 +7,8 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>( ( {...props} ref={ref} > - + {children} diff --git a/src/app/features/settings/cosmetics/Themes.tsx b/src/app/features/settings/cosmetics/Themes.tsx index a9d8e020c..8d0f75e7e 100644 --- a/src/app/features/settings/cosmetics/Themes.tsx +++ b/src/app/features/settings/cosmetics/Themes.tsx @@ -343,52 +343,6 @@ function ThemeSettings() { ); } -function SubnestedSpaceLinkDepthInput() { - const [subspaceHierarchyLimit, setSubspaceHierarchyLimit] = useSetting( - settingsAtom, - 'subspaceHierarchyLimit' - ); - const [inputValue, setInputValue] = useState(subspaceHierarchyLimit.toString()); - - const handleChange: ChangeEventHandler = (evt) => { - const val = evt.target.value; - setInputValue(val); - - const parsed = parseInt(val, 10); - if (!Number.isNaN(parsed) && parsed >= 2 && parsed <= 10) { - setSubspaceHierarchyLimit(parsed); - } - }; - - const handleKeyDown: KeyboardEventHandler = (evt) => { - if (isKeyHotkey('escape', evt)) { - evt.stopPropagation(); - setInputValue(subspaceHierarchyLimit.toString()); - (evt.target as HTMLInputElement).blur(); - } - - if (isKeyHotkey('enter', evt)) { - (evt.target as HTMLInputElement).blur(); - } - }; - - return ( - - ); -} - function PageZoomInput() { const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom'); const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`); @@ -453,14 +407,6 @@ export function Appearance() { } /> - - - } - /> - ); diff --git a/src/app/features/space-nav/SpaceNavItem.tsx b/src/app/features/space-nav/SpaceNavItem.tsx deleted file mode 100644 index f319a9da6..000000000 --- a/src/app/features/space-nav/SpaceNavItem.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { MouseEventHandler, useState } from 'react'; -import { Room } from '$types/matrix-sdk'; -import { Box, Icon, Icons, Text, config, RectCords, Avatar } from 'folds'; -import { useNavigate } from 'react-router-dom'; -import { NavButton, NavItem, NavItemContent } from '$components/nav'; -import { useRoomName } from '$hooks/useRoomMeta'; - -type SpaceNavItemProps = { - room: Room; - selected: boolean; - linkPath: string; -}; - -export function SpaceNavItem({ room, selected, linkPath }: SpaceNavItemProps) { - const [menuAnchor, setMenuAnchor] = useState(); - - const matrixRoomName = useRoomName(room); - const roomName = matrixRoomName; - - const navigate = useNavigate(); - - const handleContextMenu: MouseEventHandler = (evt) => { - evt.preventDefault(); - setMenuAnchor({ - x: evt.clientX, - y: evt.clientY, - width: 0, - height: 0, - }); - }; - - const handleNavItemClick: MouseEventHandler = () => { - navigate(linkPath); - }; - - const ariaLabel = [roomName, 'Space'].flat().filter(Boolean).join(', '); - - return ( - - - - - - - - - - - {roomName} - - - - - - - - ); -} diff --git a/src/app/features/space-nav/index.ts b/src/app/features/space-nav/index.ts deleted file mode 100644 index 507f8fc17..000000000 --- a/src/app/features/space-nav/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SpaceNavItem'; diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index b5c088750..af8c80196 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -1,6 +1,6 @@ import { atom, useAtom, useAtomValue } from 'jotai'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { MatrixError, MatrixEvent, Room, IHierarchyRoom } from '$types/matrix-sdk'; +import { MatrixError, Room, IHierarchyRoom } from '$types/matrix-sdk'; import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query'; import { MSpaceChildContent, StateEvent } from '$types/matrix/room'; import { roomToParentsAtom } from '$state/room/roomToParents'; @@ -8,7 +8,6 @@ import { getAllParents, getStateEvents, isValidChild } from '$utils/room'; import { isRoomId } from '$utils/matrix'; import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '$utils/sort'; import { useMatrixClient } from './useMatrixClient'; -import { makeLobbyCategoryId } from '../state/closedLobbyCategories'; import { useStateEventCallback } from './useStateEventCallback'; import { ErrorCode } from '../cs-errorcode'; @@ -18,7 +17,6 @@ export type HierarchyItemSpace = { ts: number; space: true; parentId?: string; - depth: number; }; export type HierarchyItemRoom = { @@ -26,7 +24,6 @@ export type HierarchyItemRoom = { content: MSpaceChildContent; ts: number; parentId: string; - depth: number; }; export type HierarchyItem = HierarchyItemSpace | HierarchyItemRoom; @@ -37,14 +34,9 @@ const hierarchyItemTs: SortFunc = (a, b) => byTsOldToNew(a.ts, b. const hierarchyItemByOrder: SortFunc = (a, b) => byOrderKey(a.content.order, b.content.order); -const childEventTs: SortFunc = (a, b) => byTsOldToNew(a.getTs(), b.getTs()); -const childEventByOrder: SortFunc = (a, b) => - byOrderKey(a.getContent().order, b.getContent().order); - const getHierarchySpaces = ( rootSpaceId: string, getRoom: GetRoomCallback, - excludeRoom: (parentId: string, roomId: string, depth: number) => boolean, spaceRooms: Set ): HierarchyItemSpace[] => { const rootSpaceItem: HierarchyItemSpace = { @@ -52,56 +44,46 @@ const getHierarchySpaces = ( content: { via: [] }, ts: 0, space: true, - depth: 0, }; - const spaceItems: HierarchyItemSpace[] = []; - - const findAndCollectHierarchySpaces = ( - spaceItem: HierarchyItemSpace, - parentSpaceId: string, - visited: Set = new Set() - ) => { - const spaceItemId = makeLobbyCategoryId(parentSpaceId, spaceItem.roomId); - - // Prevent infinite recursion - if (visited.has(spaceItemId)) return; - visited.add(spaceItemId); + let spaceItems: HierarchyItemSpace[] = []; + const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => { + if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return; const space = getRoom(spaceItem.roomId); spaceItems.push(spaceItem); if (!space) return; - const childEvents = getStateEvents(space, StateEvent.SpaceChild) - .filter((childEvent) => { - if (!isValidChild(childEvent)) return false; - const childId = childEvent.getStateKey(); - if (!childId || !isRoomId(childId)) return false; - if (excludeRoom(spaceItem.roomId, childId, spaceItem.depth)) return false; - - // because we can not find if a childId is space without joining - // or requesting room summary, we will look it into spaceRooms local - // cache which we maintain as we load summary in UI. - return getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId); - }) - .sort(childEventTs) - .sort(childEventByOrder); + const childEvents = getStateEvents(space, StateEvent.SpaceChild); childEvents.forEach((childEvent) => { + if (!isValidChild(childEvent)) return; const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return; - const childItem: HierarchyItemSpace = { - roomId: childId, - content: childEvent.getContent(), - ts: childEvent.getTs(), - space: true, - parentId: spaceItem.roomId, - depth: spaceItem.depth + 1, - }; - findAndCollectHierarchySpaces(childItem, spaceItem.roomId, visited); + // because we can not find if a childId is space without joining + // or requesting room summary, we will look it into spaceRooms local + // cache which we maintain as we load summary in UI. + if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) { + const childItem: HierarchyItemSpace = { + roomId: childId, + content: childEvent.getContent(), + ts: childEvent.getTs(), + space: true, + parentId: spaceItem.roomId, + }; + findAndCollectHierarchySpaces(childItem); + } }); }; - findAndCollectHierarchySpaces(rootSpaceItem, rootSpaceId); + findAndCollectHierarchySpaces(rootSpaceItem); + + spaceItems = [ + rootSpaceItem, + ...spaceItems + .filter((item) => item.roomId !== rootSpaceId) + .sort(hierarchyItemTs) + .sort(hierarchyItemByOrder), + ]; return spaceItems; }; @@ -114,15 +96,9 @@ const getSpaceHierarchy = ( rootSpaceId: string, spaceRooms: Set, getRoom: (roomId: string) => Room | undefined, - excludeRoom: (parentId: string, roomId: string, depth: number) => boolean, closedCategory: (spaceId: string) => boolean ): SpaceHierarchy[] => { - const spaceItems: HierarchyItemSpace[] = getHierarchySpaces( - rootSpaceId, - getRoom, - excludeRoom, - spaceRooms - ); + const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms); const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => { const space = getRoom(spaceItem.roomId); @@ -144,7 +120,6 @@ const getSpaceHierarchy = ( content: childEvent.getContent(), ts: childEvent.getTs(), parentId: spaceItem.roomId, - depth: spaceItem.depth, }; childItems.push(childItem); }); @@ -162,20 +137,19 @@ export const useSpaceHierarchy = ( spaceId: string, spaceRooms: Set, getRoom: (roomId: string) => Room | undefined, - excludeRoom: (parentId: string, roomId: string, depth: number) => boolean, closedCategory: (spaceId: string) => boolean ): SpaceHierarchy[] => { const mx = useMatrixClient(); const roomToParents = useAtomValue(roomToParentsAtom); const [hierarchyAtom] = useState(() => - atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, excludeRoom, closedCategory)) + atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory)) ); const [hierarchy, setHierarchy] = useAtom(hierarchyAtom); useEffect(() => { - setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, excludeRoom, closedCategory)); - }, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory, excludeRoom]); + setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory)); + }, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]); useStateEventCallback( mx, @@ -186,12 +160,10 @@ export const useSpaceHierarchy = ( if (!eventRoomId) return; if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) { - setHierarchy( - getSpaceHierarchy(spaceId, spaceRooms, getRoom, excludeRoom, closedCategory) - ); + setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory)); } }, - [spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory, excludeRoom] + [spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory] ) ); @@ -201,44 +173,10 @@ export const useSpaceHierarchy = ( const getSpaceJoinedHierarchy = ( rootSpaceId: string, getRoom: GetRoomCallback, - excludeRoom: (parentId: string, roomId: string, depth: number) => boolean, + excludeRoom: (parentId: string, roomId: string) => boolean, sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[] ): HierarchyItem[] => { - const spaceItems: HierarchyItemSpace[] = getHierarchySpaces( - rootSpaceId, - getRoom, - excludeRoom, - new Set() - ); - - /** - * Recursively checks if the given space or any of its descendants contain non-space rooms. - * - * @param spaceId - The space ID to check. - * @param visited - Set used to prevent recursion errors. - * @returns True if the space or any descendant contains non-space rooms. - */ - const getContainsRoom = (spaceId: string, visited: Set = new Set()) => { - // Prevent infinite recursion - if (visited.has(spaceId)) return false; - visited.add(spaceId); - - const space = getRoom(spaceId); - if (!space) return false; - - const childEvents = getStateEvents(space, StateEvent.SpaceChild); - - return childEvents.some((childEvent): boolean => { - if (!isValidChild(childEvent)) return false; - const childId = childEvent.getStateKey(); - if (!childId || !isRoomId(childId)) return false; - const room = getRoom(childId); - if (!room) return false; - - if (!room.isSpaceRoom()) return true; - return getContainsRoom(childId, visited); - }); - }; + const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, new Set()); const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => { const space = getRoom(spaceItem.roomId); @@ -255,21 +193,20 @@ const getSpaceJoinedHierarchy = ( return true; }); - if (!getContainsRoom(spaceItem.roomId)) return []; + if (joinedRoomEvents.length === 0) return []; const childItems: HierarchyItemRoom[] = []; joinedRoomEvents.forEach((childEvent) => { const childId = childEvent.getStateKey(); if (!childId) return; - if (excludeRoom(space.roomId, childId, spaceItem.depth)) return; + if (excludeRoom(space.roomId, childId)) return; const childItem: HierarchyItemRoom = { roomId: childId, content: childEvent.getContent(), ts: childEvent.getTs(), parentId: spaceItem.roomId, - depth: spaceItem.depth, }; childItems.push(childItem); }); @@ -282,7 +219,7 @@ const getSpaceJoinedHierarchy = ( export const useSpaceJoinedHierarchy = ( spaceId: string, getRoom: GetRoomCallback, - excludeRoom: (parentId: string, roomId: string, depth: number) => boolean, + excludeRoom: (parentId: string, roomId: string) => boolean, sortByActivity: (spaceId: string) => boolean ): HierarchyItem[] => { const mx = useMatrixClient(); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 4eb7b117c..aae2f2d7b 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -1,13 +1,4 @@ -import { - MouseEventHandler, - ReactElement, - forwardRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { MouseEventHandler, forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { Avatar, @@ -27,7 +18,7 @@ import { config, toRem, } from 'folds'; -import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'; +import { useVirtualizer } from '@tanstack/react-virtual'; import FocusTrap from 'focus-trap-react'; import { useNavigate } from 'react-router-dom'; import { JoinRule, Room, RoomJoinRulesEventContent } from '$types/matrix-sdk'; @@ -40,21 +31,18 @@ import { useSelectedRoom } from '$hooks/router/useSelectedRoom'; import { useSpaceLobbySelected, useSpaceSearchSelected } from '$hooks/router/useSelectedSpace'; import { useSpace } from '$hooks/useSpace'; import { VirtualTile } from '$components/virtualizer'; -import { spaceRoomsAtom } from '$state/spaceRooms'; import { RoomNavCategoryButton, RoomNavItem } from '$features/room-nav'; -import { SpaceNavItem } from '$features/space-nav'; -import { makeNavCategoryId, getNavCategoryIdParts } from '$state/closedNavCategories'; +import { makeNavCategoryId } from '$state/closedNavCategories'; import { roomToUnreadAtom } from '$state/room/roomToUnread'; import { useCategoryHandler } from '$hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '$hooks/useNavToActivePathMapper'; import { useRoomName } from '$hooks/useRoomMeta'; -import { HierarchyItem, useSpaceJoinedHierarchy } from '$hooks/useSpaceHierarchy'; +import { useSpaceJoinedHierarchy } from '$hooks/useSpaceHierarchy'; import { allRoomsAtom } from '$state/room-list/roomList'; import { PageNav, PageNavContent, PageNavHeader } from '$components/page'; import { usePowerLevels } from '$hooks/usePowerLevels'; import { useRecursiveChildScopeFactory, useSpaceChildren } from '$state/hooks/roomList'; import { roomToParentsAtom } from '$state/room/roomToParents'; -import { roomToChildrenAtom } from '$state/room/roomToChildren'; import { markAsRead } from '$utils/notifications'; import { useRoomsUnread } from '$state/hooks/unread'; import { UseStateProvider } from '$components/UseStateProvider'; @@ -387,10 +375,7 @@ export function Space() { const scrollRef = useRef(null); const mDirects = useAtomValue(mDirectAtom); const roomToUnread = useAtomValue(roomToUnreadAtom); - const roomToParents = useAtomValue(roomToParentsAtom); - const roomToChildren = useAtomValue(roomToChildrenAtom); const allRooms = useAtomValue(allRoomsAtom); - const [spaceRooms] = useAtom(spaceRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); @@ -412,261 +397,25 @@ export function Space() { [mx, allJoinedRooms] ); - const closedCategoriesCache = useRef(new Map()); - const ancestorsCollapsedCache = useRef(new Map()); - useEffect(() => { - closedCategoriesCache.current.clear(); - ancestorsCollapsedCache.current.clear(); - }, [closedCategories, roomToParents, getRoom]); - - /** - * Recursively checks if a given parentId (or all its ancestors) is in a closed category. - * - * @param spaceId - The root space ID. - * @param parentId - The parent space ID to start the check from. - * @param previousId - The last ID checked, only used to ignore root collapse state. - * @param visited - Set used to prevent recursion errors. - * @returns True if parentId or all ancestors is in a closed category. - */ - const getInClosedCategories = useCallback( - ( - spaceId: string, - parentId: string, - previousId?: string, - visited: Set = new Set() - ): boolean => { - // Ignore root space being collapsed if in a subspace, - // this is due to many spaces dumping all rooms in the top-level space. - if (parentId === spaceId && previousId) { - if (spaceRooms.has(previousId) || getRoom(previousId)?.isSpaceRoom()) { - return false; - } - } - - const categoryId = makeNavCategoryId(spaceId, parentId); - - // Prevent infinite recursion - if (visited.has(categoryId)) return false; - visited.add(categoryId); - - if (closedCategoriesCache.current.has(categoryId)) { - return closedCategoriesCache.current.get(categoryId); - } - - if (closedCategories.has(categoryId)) { - closedCategoriesCache.current.set(categoryId, true); - return true; - } - - const parentParentIds = roomToParents.get(parentId); - if (!parentParentIds || parentParentIds.size === 0) { - closedCategoriesCache.current.set(categoryId, false); - return false; - } - - // As a subspace can be in multiple spaces, - // only return true if all parent spaces are closed. - const allClosed = !Array.from(parentParentIds).some( - (id) => !getInClosedCategories(spaceId, id, parentId, visited) - ); - visited.delete(categoryId); - closedCategoriesCache.current.set(categoryId, allClosed); - return allClosed; - }, - [closedCategories, getRoom, roomToParents, spaceRooms] - ); - - /** - * Recursively checks if the given room or any of its descendants should be visible. - * - * @param roomId - The room ID to check. - * @param visited - Set used to prevent recursion errors. - * @returns True if the room or any descendant should be visible. - */ - const getContainsShowRoom = useCallback( - (roomId: string, visited: Set = new Set()): boolean => { - if (roomToUnread.has(roomId) || roomId === selectedRoomId) { - return true; - } - - // Prevent infinite recursion - if (visited.has(roomId)) return false; - visited.add(roomId); - - const childIds = roomToChildren.get(roomId); - if (!childIds || childIds.size === 0) { - return false; - } - - return Array.from(childIds).some((id) => getContainsShowRoom(id, visited)); - }, - [roomToUnread, selectedRoomId, roomToChildren] - ); - - /** - * Determines whether all parent categories are collapsed. - * - * @param spaceId - The root space ID. - * @param roomId - The room ID to start the check from. - * @returns True if every parent category is collapsed; false otherwise. - */ - const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { - const categoryId = makeNavCategoryId(spaceId, roomId); - if (ancestorsCollapsedCache.current.has(categoryId)) { - return ancestorsCollapsedCache.current.get(categoryId); - } - - const parentIds = roomToParents.get(roomId); - if (!parentIds || parentIds.size === 0) { - ancestorsCollapsedCache.current.set(categoryId, false); - return false; - } - - const allCollapsed = !Array.from(parentIds).some( - (id) => !getInClosedCategories(spaceId, id, roomId) - ); - ancestorsCollapsedCache.current.set(categoryId, allCollapsed); - return allCollapsed; - }; - - /** - * Determines the depth limit for the joined space hierarchy and the SpaceNavItems to start appearing - */ - const [subspaceHierarchyLimit] = useSetting(settingsAtom, 'subspaceHierarchyLimit'); - /** - * Creates an SVG used for connecting spaces to their subrooms. - * @param virtualizedItems - The virtualized item list that will be used to render elements in the nav - * @returns React SVG Element that can be overlayed on top of the nav category for rooms. - */ - const getConnectorSVG = ( - hierarchy: HierarchyItem[], - virtualizedItems: VirtualItem[] - ): ReactElement => { - const DEPTH_START = 2; - const PADDING_LEFT_DEPTH_OFFSET = 15.75; - const PADDING_LEFT_DEPTH_OFFSET_START = -15.75; - const RADIUS = 5; - - let connectorStack: { aX: number; aY: number }[] = []; - // Holder for the paths - const pathHolder: ReactElement[] = []; - virtualizedItems.forEach((vItem) => { - const { roomId, depth } = hierarchy[vItem.index] ?? {}; - const room = getRoom(roomId); - // We will render spaces at a level above their normal depth, since we want their children to be "under" them - const renderDepth = room?.isSpaceRoom() ? depth : depth + 1; - // for the root items, we are not doing anything with it. - if (renderDepth < DEPTH_START) { - return; - } - // for nearly root level text/call rooms, we will not be drawing any arcs. - if (renderDepth === DEPTH_START - 1 && !room?.isSpaceRoom() && connectorStack.length === 0) { - return; - } - - // for the sub-root items, we will not draw any arcs from root to it. - // however, we should capture the aX and aY to draw starter arcs for next depths. - if (renderDepth === DEPTH_START) { - connectorStack = [ - { - aX: PADDING_LEFT_DEPTH_OFFSET * DEPTH_START + PADDING_LEFT_DEPTH_OFFSET_START, - aY: vItem.end, - }, - ]; - return; - } - // adjust the stack to be at the correct depth, which is the "parent" of the current item. - while (connectorStack.length + DEPTH_START > renderDepth && connectorStack.length !== 0) { - connectorStack.pop(); - } - - // Fixes crash in case the top level virtual item is unrendered. - if (connectorStack.length === 0) { - connectorStack = [{ aX: Math.round(renderDepth * PADDING_LEFT_DEPTH_OFFSET), aY: 0 }]; - } - - const lastConnector = connectorStack[connectorStack.length - 1]; - - // aX: numeric x where the vertical connector starts - // aY: end of parent (already numeric) - const { aX, aY } = lastConnector; - - // bX: point where the vertical connector ends - const bX = Math.round( - (renderDepth - 0.5) * PADDING_LEFT_DEPTH_OFFSET + PADDING_LEFT_DEPTH_OFFSET_START - ); - // bY: center of current item - const bY = vItem.end - vItem.size / 2; - - const pathString = - `M ${aX} ${aY} ` + - `L ${aX} ${bY - RADIUS} ` + - `A ${RADIUS} ${RADIUS} 0 0 0 ${aX + RADIUS} ${bY} ` + - `L ${bX} ${bY}`; - - pathHolder.push( - - ); - - // add this item to the connector stack, in case the next item's depth is higher. - connectorStack.push({ - aX: Math.round(renderDepth * PADDING_LEFT_DEPTH_OFFSET) + PADDING_LEFT_DEPTH_OFFSET_START, - aY: vItem.end, - }); - }); - return ( - - {pathHolder} - - ); - }; - const hierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, useCallback( - (parentId, roomId, depth) => { - if (depth >= subspaceHierarchyLimit) { - // we will exclude items above this depth - return true; - } - if (!getInClosedCategories(space.roomId, parentId, roomId)) { + (parentId, roomId) => { + if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { return false; } const unread = roomToUnread.get(roomId); - const containsShowRoom = getContainsShowRoom(roomId); const hasUnread = !!unread && (unread.total > 0 || unread.highlight > 0); const showRoomAnyway = hasUnread || roomId === selectedRoomId || callEmbed?.roomId === roomId; - return containsShowRoom || !showRoomAnyway; + return !showRoomAnyway; }, - [ - getContainsShowRoom, - getInClosedCategories, - space.roomId, - callEmbed, - subspaceHierarchyLimit, - roomToUnread, - selectedRoomId, - ] + [space.roomId, closedCategories, roomToUnread, selectedRoomId, callEmbed] ), useCallback( - (sId) => getInClosedCategories(space.roomId, sId), - [getInClosedCategories, space.roomId] + (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)), + [closedCategories, space.roomId] ) ); @@ -677,30 +426,13 @@ export function Space() { overscan: 10, }); - const virtualizedItems = virtualizer.getVirtualItems(); - - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { - const collapsed = closedCategories.has(categoryId); - const [spaceId, roomId] = getNavCategoryIdParts(categoryId); - - // Only prevent collapsing if all parents are collapsed - const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); - - if (toggleable) { - return collapsed; - } - return !collapsed; - }); + const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => + closedCategories.has(categoryId) + ); const getToLink = (roomId: string) => getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId)); - const getCategoryPadding = (depth: number): string | undefined => { - if (depth === 0) return undefined; - if (depth === 1) return config.space.S400; - return config.space.S0; - }; - const navigate = useNavigate(); const lastRoomId = useAtomValue(lastVisitedRoomIdAtom); @@ -763,35 +495,13 @@ export function Space() { position: 'relative', }} > - {virtualizedItems.map((vItem) => { - const { roomId, depth } = hierarchy[vItem.index] ?? {}; + {virtualizer.getVirtualItems().map((vItem) => { + const { roomId } = hierarchy[vItem.index] ?? {}; const room = mx.getRoom(roomId); - const renderDepth = room?.isSpaceRoom() ? depth - 2 : depth - 1; if (!room) return null; - if (depth === subspaceHierarchyLimit && room.isSpaceRoom()) { - return ( - -
- -
-
- ); - } - - const paddingTop = getCategoryPadding(depth); - const paddingLeft = `calc(${renderDepth} * ${config.space.S400})`; if (room.isSpaceRoom()) { const categoryId = makeNavCategoryId(space.roomId, roomId); - const closedViaCategory = getInClosedCategories(space.roomId, roomId); return ( -
+
{roomId === space.roomId ? 'Rooms' : room?.name} @@ -820,23 +532,20 @@ export function Space() { key={vItem.index} ref={virtualizer.measureElement} > -
- -
+ ); })} - {getConnectorSVG(hierarchy, virtualizedItems)} diff --git a/src/app/state/closedLobbyCategories.ts b/src/app/state/closedLobbyCategories.ts index 3c5c99e16..9d4d5d175 100644 --- a/src/app/state/closedLobbyCategories.ts +++ b/src/app/state/closedLobbyCategories.ts @@ -66,5 +66,3 @@ export const makeClosedLobbyCategoriesAtom = (userId: string): ClosedLobbyCatego }; export const makeLobbyCategoryId = (...args: string[]): string => args.join('|'); - -export const getLobbyCategoryIdParts = (categoryId: string): string[] => categoryId.split('|'); diff --git a/src/app/state/closedNavCategories.ts b/src/app/state/closedNavCategories.ts index 8c2348902..d21187f9a 100644 --- a/src/app/state/closedNavCategories.ts +++ b/src/app/state/closedNavCategories.ts @@ -66,5 +66,3 @@ export const makeClosedNavCategoriesAtom = (userId: string): ClosedNavCategories }; export const makeNavCategoryId = (...args: string[]): string => args.join('|'); - -export const getNavCategoryIdParts = (categoryId: string): string[] => categoryId.split('|'); diff --git a/src/app/state/room/roomToChildren.ts b/src/app/state/room/roomToChildren.ts deleted file mode 100644 index ae0f4f24f..000000000 --- a/src/app/state/room/roomToChildren.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { atom } from 'jotai'; -import { roomToParentsAtom } from './roomToParents'; - -export const roomToChildrenAtom = atom((get) => { - const roomToParents = get(roomToParentsAtom); - const map = new Map>(); - - roomToParents.forEach((parentSet, childId) => { - parentSet.forEach((parentId) => { - if (!map.has(parentId)) map.set(parentId, new Set()); - map.get(parentId)?.add(childId); - }); - }); - - return map; -}); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 93ad93c45..d3c4f721a 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -96,7 +96,6 @@ export interface Settings { autoplayStickers: boolean; autoplayEmojis: boolean; saveStickerEmojiBandwidth: boolean; - subspaceHierarchyLimit: number; alwaysShowCallButton: boolean; faviconForMentionsOnly: boolean; highlightMentions: boolean; @@ -181,7 +180,6 @@ const defaultSettings: Settings = { autoplayStickers: true, autoplayEmojis: true, saveStickerEmojiBandwidth: false, - subspaceHierarchyLimit: 3, alwaysShowCallButton: false, faviconForMentionsOnly: false, highlightMentions: true, diff --git a/src/app/utils/colorMXID.ts b/src/app/utils/colorMXID.ts index 3c512d99f..c94a19d48 100644 --- a/src/app/utils/colorMXID.ts +++ b/src/app/utils/colorMXID.ts @@ -9,7 +9,7 @@ function hashCode(str?: string): number { const chr = str.codePointAt(i) ?? 0; // eslint-disable-next-line no-bitwise hash = (hash << 5) - hash + chr; - + // eslint-disable-next-line no-bitwise hash = Math.trunc(hash); } return Math.abs(hash);