mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Fix unknown rooms in space lobby (#2224)
* add hook to fetch one level of space hierarchy * add enable param to level hierarchy hook * improve HierarchyItem types * fix type errors in lobby * load space hierarachy per level * fix menu item visibility * fix unknown spaces over federation * show inaccessible rooms only to admins * fix unknown room renders loading content twice * fix unknown room visible to normal user if space all room are unknown * show no rooms card if space does not have any room
This commit is contained in:
		
							parent
							
								
									f121cc0a24
								
							
						
					
					
						commit
						7c6ab366af
					
				
					 7 changed files with 564 additions and 330 deletions
				
			
		| 
						 | 
				
			
			@ -155,7 +155,7 @@ function SettingsMenuItem({
 | 
			
		|||
  disabled?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  const handleSettings = () => {
 | 
			
		||||
    if (item.space) {
 | 
			
		||||
    if ('space' in item) {
 | 
			
		||||
      openSpaceSettings(item.roomId);
 | 
			
		||||
    } else {
 | 
			
		||||
      toggleRoomSettings(item.roomId);
 | 
			
		||||
| 
						 | 
				
			
			@ -271,7 +271,7 @@ export function HierarchyItemMenu({
 | 
			
		|||
                            </Text>
 | 
			
		||||
                          </MenuItem>
 | 
			
		||||
                          {promptLeave &&
 | 
			
		||||
                            (item.space ? (
 | 
			
		||||
                            ('space' in item ? (
 | 
			
		||||
                              <LeaveSpacePrompt
 | 
			
		||||
                                roomId={item.roomId}
 | 
			
		||||
                                onDone={handleRequestClose}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,15 @@ import { useAtom, useAtomValue } from 'jotai';
 | 
			
		|||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
 | 
			
		||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import { useSpace } from '../../hooks/useSpace';
 | 
			
		||||
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
 | 
			
		||||
import { HierarchyItem, useSpaceHierarchy } from '../../hooks/useSpaceHierarchy';
 | 
			
		||||
import {
 | 
			
		||||
  HierarchyItem,
 | 
			
		||||
  HierarchyItemSpace,
 | 
			
		||||
  useSpaceHierarchy,
 | 
			
		||||
} from '../../hooks/useSpaceHierarchy';
 | 
			
		||||
import { VirtualTile } from '../../components/virtualizer';
 | 
			
		||||
import { spaceRoomsAtom } from '../../state/spaceRooms';
 | 
			
		||||
import { MembersDrawer } from '../room/MembersDrawer';
 | 
			
		||||
| 
						 | 
				
			
			@ -25,18 +31,15 @@ import {
 | 
			
		|||
  usePowerLevels,
 | 
			
		||||
  useRoomsPowerLevels,
 | 
			
		||||
} from '../../hooks/usePowerLevels';
 | 
			
		||||
import { RoomItemCard } from './RoomItem';
 | 
			
		||||
import { mDirectAtom } from '../../state/mDirectList';
 | 
			
		||||
import { SpaceItemCard } from './SpaceItem';
 | 
			
		||||
import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
 | 
			
		||||
import { useCategoryHandler } from '../../hooks/useCategoryHandler';
 | 
			
		||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
			
		||||
import { allRoomsAtom } from '../../state/room-list/roomList';
 | 
			
		||||
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
 | 
			
		||||
import { getSpaceRoomPath } from '../../pages/pathUtils';
 | 
			
		||||
import { HierarchyItemMenu } from './HierarchyItemMenu';
 | 
			
		||||
import { StateEvent } from '../../../types/matrix/room';
 | 
			
		||||
import { AfterItemDropTarget, CanDropCallback, useDnDMonitor } from './DnD';
 | 
			
		||||
import { CanDropCallback, useDnDMonitor } from './DnD';
 | 
			
		||||
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
 | 
			
		||||
import { getStateEvent } from '../../utils/room';
 | 
			
		||||
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +52,7 @@ import { useOrphanSpaces } from '../../state/hooks/roomList';
 | 
			
		|||
import { roomToParentsAtom } from '../../state/room/roomToParents';
 | 
			
		||||
import { AccountDataEvent } from '../../../types/matrix/accountData';
 | 
			
		||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
 | 
			
		||||
import { SpaceHierarchy } from './SpaceHierarchy';
 | 
			
		||||
 | 
			
		||||
export function Lobby() {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +85,8 @@ export function Lobby() {
 | 
			
		|||
    return new Set(sideSpaces);
 | 
			
		||||
  }, [sidebarItems]);
 | 
			
		||||
 | 
			
		||||
  const [spacesItems, setSpacesItem] = useState<Map<string, IHierarchyRoom>>(() => new Map());
 | 
			
		||||
 | 
			
		||||
  useElementSizeObserver(
 | 
			
		||||
    useCallback(() => heroSectionRef.current, []),
 | 
			
		||||
    useCallback((w, height) => setHeroSectionHeight(height), [])
 | 
			
		||||
| 
						 | 
				
			
			@ -107,19 +113,20 @@ export function Lobby() {
 | 
			
		|||
  );
 | 
			
		||||
 | 
			
		||||
  const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
 | 
			
		||||
  const flattenHierarchy = useSpaceHierarchy(
 | 
			
		||||
  const hierarchy = useSpaceHierarchy(
 | 
			
		||||
    space.roomId,
 | 
			
		||||
    spaceRooms,
 | 
			
		||||
    getRoom,
 | 
			
		||||
    useCallback(
 | 
			
		||||
      (childId) =>
 | 
			
		||||
        closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) || !!draggingItem?.space,
 | 
			
		||||
        closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
 | 
			
		||||
        (draggingItem ? 'space' in draggingItem : false),
 | 
			
		||||
      [closedCategories, space.roomId, draggingItem]
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const virtualizer = useVirtualizer({
 | 
			
		||||
    count: flattenHierarchy.length,
 | 
			
		||||
    count: hierarchy.length,
 | 
			
		||||
    getScrollElement: () => scrollRef.current,
 | 
			
		||||
    estimateSize: () => 1,
 | 
			
		||||
    overscan: 2,
 | 
			
		||||
| 
						 | 
				
			
			@ -129,8 +136,17 @@ export function Lobby() {
 | 
			
		|||
 | 
			
		||||
  const roomsPowerLevels = useRoomsPowerLevels(
 | 
			
		||||
    useMemo(
 | 
			
		||||
      () => flattenHierarchy.map((i) => mx.getRoom(i.roomId)).filter((r) => !!r) as Room[],
 | 
			
		||||
      [mx, flattenHierarchy]
 | 
			
		||||
      () =>
 | 
			
		||||
        hierarchy
 | 
			
		||||
          .flatMap((i) => {
 | 
			
		||||
            const childRooms = Array.isArray(i.rooms)
 | 
			
		||||
              ? i.rooms.map((r) => mx.getRoom(r.roomId))
 | 
			
		||||
              : [];
 | 
			
		||||
 | 
			
		||||
            return [mx.getRoom(i.space.roomId), ...childRooms];
 | 
			
		||||
          })
 | 
			
		||||
          .filter((r) => !!r) as Room[],
 | 
			
		||||
      [mx, hierarchy]
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -142,8 +158,8 @@ export function Lobby() {
 | 
			
		|||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (item.space) {
 | 
			
		||||
        if (!container.item.space) return false;
 | 
			
		||||
      if ('space' in item) {
 | 
			
		||||
        if (!('space' in container.item)) return false;
 | 
			
		||||
        const containerSpaceId = space.roomId;
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
| 
						 | 
				
			
			@ -156,9 +172,8 @@ export function Lobby() {
 | 
			
		|||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const containerSpaceId = container.item.space
 | 
			
		||||
        ? container.item.roomId
 | 
			
		||||
        : container.item.parentId;
 | 
			
		||||
      const containerSpaceId =
 | 
			
		||||
        'space' in container.item ? container.item.roomId : container.item.parentId;
 | 
			
		||||
 | 
			
		||||
      const dropOutsideSpace = item.parentId !== containerSpaceId;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -192,22 +207,22 @@ export function Lobby() {
 | 
			
		|||
  );
 | 
			
		||||
 | 
			
		||||
  const reorderSpace = useCallback(
 | 
			
		||||
    (item: HierarchyItem, containerItem: HierarchyItem) => {
 | 
			
		||||
    (item: HierarchyItemSpace, containerItem: HierarchyItem) => {
 | 
			
		||||
      if (!item.parentId) return;
 | 
			
		||||
 | 
			
		||||
      const childItems = flattenHierarchy
 | 
			
		||||
        .filter((i) => i.parentId && i.space)
 | 
			
		||||
      const itemSpaces: HierarchyItemSpace[] = hierarchy
 | 
			
		||||
        .map((i) => i.space)
 | 
			
		||||
        .filter((i) => i.roomId !== item.roomId);
 | 
			
		||||
 | 
			
		||||
      const beforeIndex = childItems.findIndex((i) => i.roomId === containerItem.roomId);
 | 
			
		||||
      const beforeIndex = itemSpaces.findIndex((i) => i.roomId === containerItem.roomId);
 | 
			
		||||
      const insertIndex = beforeIndex + 1;
 | 
			
		||||
 | 
			
		||||
      childItems.splice(insertIndex, 0, {
 | 
			
		||||
      itemSpaces.splice(insertIndex, 0, {
 | 
			
		||||
        ...item,
 | 
			
		||||
        content: { ...item.content, order: undefined },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const currentOrders = childItems.map((i) => {
 | 
			
		||||
      const currentOrders = itemSpaces.map((i) => {
 | 
			
		||||
        if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
 | 
			
		||||
          return i.content.order;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -217,21 +232,21 @@ export function Lobby() {
 | 
			
		|||
      const newOrders = orderKeys(lex, currentOrders);
 | 
			
		||||
 | 
			
		||||
      newOrders?.forEach((orderKey, index) => {
 | 
			
		||||
        const itm = childItems[index];
 | 
			
		||||
        const itm = itemSpaces[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,
 | 
			
		||||
            StateEvent.SpaceChild as any,
 | 
			
		||||
            { ...itm.content, order: orderKey },
 | 
			
		||||
            itm.roomId
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [mx, flattenHierarchy, lex, roomsPowerLevels, canEditSpaceChild]
 | 
			
		||||
    [mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const reorderRoom = useCallback(
 | 
			
		||||
| 
						 | 
				
			
			@ -240,13 +255,12 @@ export function Lobby() {
 | 
			
		|||
      if (!item.parentId) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const containerParentId: string = containerItem.space
 | 
			
		||||
        ? containerItem.roomId
 | 
			
		||||
        : containerItem.parentId;
 | 
			
		||||
      const containerParentId: string =
 | 
			
		||||
        'space' in containerItem ? containerItem.roomId : containerItem.parentId;
 | 
			
		||||
      const itemContent = item.content;
 | 
			
		||||
 | 
			
		||||
      if (item.parentId !== containerParentId) {
 | 
			
		||||
        mx.sendStateEvent(item.parentId, StateEvent.SpaceChild, {}, item.roomId);
 | 
			
		||||
        mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
| 
						 | 
				
			
			@ -265,28 +279,29 @@ export function Lobby() {
 | 
			
		|||
          const allow =
 | 
			
		||||
            joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? [];
 | 
			
		||||
          allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
 | 
			
		||||
          mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules, {
 | 
			
		||||
          mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
 | 
			
		||||
            ...joinRuleContent,
 | 
			
		||||
            allow,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const childItems = flattenHierarchy
 | 
			
		||||
        .filter((i) => i.parentId === containerParentId && !i.space)
 | 
			
		||||
        .filter((i) => i.roomId !== item.roomId);
 | 
			
		||||
      const itemSpaces = Array.from(
 | 
			
		||||
        hierarchy?.find((i) => i.space.roomId === containerParentId)?.rooms ?? []
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const beforeItem: HierarchyItem | undefined = containerItem.space ? undefined : containerItem;
 | 
			
		||||
      const beforeIndex = childItems.findIndex((i) => i.roomId === beforeItem?.roomId);
 | 
			
		||||
      const beforeItem: HierarchyItem | undefined =
 | 
			
		||||
        'space' in containerItem ? undefined : containerItem;
 | 
			
		||||
      const beforeIndex = itemSpaces.findIndex((i) => i.roomId === beforeItem?.roomId);
 | 
			
		||||
      const insertIndex = beforeIndex + 1;
 | 
			
		||||
 | 
			
		||||
      childItems.splice(insertIndex, 0, {
 | 
			
		||||
      itemSpaces.splice(insertIndex, 0, {
 | 
			
		||||
        ...item,
 | 
			
		||||
        parentId: containerParentId,
 | 
			
		||||
        content: { ...itemContent, order: undefined },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const currentOrders = childItems.map((i) => {
 | 
			
		||||
      const currentOrders = itemSpaces.map((i) => {
 | 
			
		||||
        if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
 | 
			
		||||
          return i.content.order;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -296,18 +311,18 @@ export function Lobby() {
 | 
			
		|||
      const newOrders = orderKeys(lex, currentOrders);
 | 
			
		||||
 | 
			
		||||
      newOrders?.forEach((orderKey, index) => {
 | 
			
		||||
        const itm = childItems[index];
 | 
			
		||||
        const itm = itemSpaces[index];
 | 
			
		||||
        if (itm && orderKey !== currentOrders[index]) {
 | 
			
		||||
          mx.sendStateEvent(
 | 
			
		||||
            containerParentId,
 | 
			
		||||
            StateEvent.SpaceChild,
 | 
			
		||||
            StateEvent.SpaceChild as any,
 | 
			
		||||
            { ...itm.content, order: orderKey },
 | 
			
		||||
            itm.roomId
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [mx, flattenHierarchy, lex]
 | 
			
		||||
    [mx, hierarchy, lex]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useDnDMonitor(
 | 
			
		||||
| 
						 | 
				
			
			@ -318,7 +333,7 @@ export function Lobby() {
 | 
			
		|||
        if (!canDrop(item, container)) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        if (item.space) {
 | 
			
		||||
        if ('space' in item) {
 | 
			
		||||
          reorderSpace(item, container.item);
 | 
			
		||||
        } else {
 | 
			
		||||
          reorderRoom(item, container.item);
 | 
			
		||||
| 
						 | 
				
			
			@ -328,8 +343,16 @@ export function Lobby() {
 | 
			
		|||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const addSpaceRoom = useCallback(
 | 
			
		||||
    (roomId: string) => setSpaceRooms({ type: 'PUT', roomId }),
 | 
			
		||||
  const handleSpacesFound = useCallback(
 | 
			
		||||
    (sItems: IHierarchyRoom[]) => {
 | 
			
		||||
      setSpaceRooms({ type: 'PUT', roomIds: sItems.map((i) => i.room_id) });
 | 
			
		||||
      setSpacesItem((current) => {
 | 
			
		||||
        const newItems = produce(current, (draft) => {
 | 
			
		||||
          sItems.forEach((item) => draft.set(item.room_id, item));
 | 
			
		||||
        });
 | 
			
		||||
        return current.size === newItems.size ? current : newItems;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [setSpaceRooms]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -394,121 +417,44 @@ export function Lobby() {
 | 
			
		|||
                      <LobbyHero />
 | 
			
		||||
                    </PageHeroSection>
 | 
			
		||||
                    {vItems.map((vItem) => {
 | 
			
		||||
                      const item = flattenHierarchy[vItem.index];
 | 
			
		||||
                      const item = hierarchy[vItem.index];
 | 
			
		||||
                      if (!item) return null;
 | 
			
		||||
                      const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
 | 
			
		||||
                      const userPLInItem = powerLevelAPI.getPowerLevel(
 | 
			
		||||
                        itemPowerLevel,
 | 
			
		||||
                        mx.getUserId() ?? undefined
 | 
			
		||||
                      );
 | 
			
		||||
                      const canInvite = powerLevelAPI.canDoAction(
 | 
			
		||||
                        itemPowerLevel,
 | 
			
		||||
                        'invite',
 | 
			
		||||
                        userPLInItem
 | 
			
		||||
                      );
 | 
			
		||||
                      const isJoined = allJoinedRooms.has(item.roomId);
 | 
			
		||||
                      const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId;
 | 
			
		||||
 | 
			
		||||
                      const nextRoomId: string | undefined =
 | 
			
		||||
                        flattenHierarchy[vItem.index + 1]?.roomId;
 | 
			
		||||
                      const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId);
 | 
			
		||||
 | 
			
		||||
                      const dragging =
 | 
			
		||||
                        draggingItem?.roomId === item.roomId &&
 | 
			
		||||
                        draggingItem.parentId === item.parentId;
 | 
			
		||||
 | 
			
		||||
                      if (item.space) {
 | 
			
		||||
                        const categoryId = makeLobbyCategoryId(space.roomId, item.roomId);
 | 
			
		||||
                        const { parentId } = item;
 | 
			
		||||
                        const parentPowerLevels = parentId
 | 
			
		||||
                          ? roomsPowerLevels.get(parentId) ?? {}
 | 
			
		||||
                          : undefined;
 | 
			
		||||
 | 
			
		||||
                        return (
 | 
			
		||||
                          <VirtualTile
 | 
			
		||||
                            virtualItem={vItem}
 | 
			
		||||
                            style={{
 | 
			
		||||
                              paddingTop: vItem.index === 0 ? 0 : config.space.S500,
 | 
			
		||||
                            }}
 | 
			
		||||
                            ref={virtualizer.measureElement}
 | 
			
		||||
                            key={vItem.index}
 | 
			
		||||
                          >
 | 
			
		||||
                            <SpaceItemCard
 | 
			
		||||
                              item={item}
 | 
			
		||||
                              joined={allJoinedRooms.has(item.roomId)}
 | 
			
		||||
                              categoryId={categoryId}
 | 
			
		||||
                              closed={closedCategories.has(categoryId) || !!draggingItem?.space}
 | 
			
		||||
                              handleClose={handleCategoryClick}
 | 
			
		||||
                              getRoom={getRoom}
 | 
			
		||||
                              canEditChild={canEditSpaceChild(
 | 
			
		||||
                                roomsPowerLevels.get(item.roomId) ?? {}
 | 
			
		||||
                              )}
 | 
			
		||||
                              canReorder={
 | 
			
		||||
                                parentPowerLevels ? canEditSpaceChild(parentPowerLevels) : false
 | 
			
		||||
                              }
 | 
			
		||||
                              options={
 | 
			
		||||
                                parentId &&
 | 
			
		||||
                                parentPowerLevels && (
 | 
			
		||||
                                  <HierarchyItemMenu
 | 
			
		||||
                                    item={{ ...item, parentId }}
 | 
			
		||||
                                    canInvite={canInvite}
 | 
			
		||||
                                    joined={isJoined}
 | 
			
		||||
                                    canEditChild={canEditSpaceChild(parentPowerLevels)}
 | 
			
		||||
                                    pinned={sidebarSpaces.has(item.roomId)}
 | 
			
		||||
                                    onTogglePin={togglePinToSidebar}
 | 
			
		||||
                                  />
 | 
			
		||||
                                )
 | 
			
		||||
                              }
 | 
			
		||||
                              before={item.parentId ? undefined : undefined}
 | 
			
		||||
                              after={
 | 
			
		||||
                                <AfterItemDropTarget
 | 
			
		||||
                                  item={item}
 | 
			
		||||
                                  nextRoomId={nextRoomId}
 | 
			
		||||
                                  afterSpace
 | 
			
		||||
                                  canDrop={canDrop}
 | 
			
		||||
                                />
 | 
			
		||||
                              }
 | 
			
		||||
                              onDragging={setDraggingItem}
 | 
			
		||||
                              data-dragging={dragging}
 | 
			
		||||
                            />
 | 
			
		||||
                          </VirtualTile>
 | 
			
		||||
                        );
 | 
			
		||||
                      }
 | 
			
		||||
 | 
			
		||||
                      const parentPowerLevels = roomsPowerLevels.get(item.parentId) ?? {};
 | 
			
		||||
                      const prevItem: HierarchyItem | undefined = flattenHierarchy[vItem.index - 1];
 | 
			
		||||
                      const nextItem: HierarchyItem | undefined = flattenHierarchy[vItem.index + 1];
 | 
			
		||||
                      return (
 | 
			
		||||
                        <VirtualTile
 | 
			
		||||
                          virtualItem={vItem}
 | 
			
		||||
                          style={{ paddingTop: config.space.S100 }}
 | 
			
		||||
                          style={{
 | 
			
		||||
                            paddingTop: vItem.index === 0 ? 0 : config.space.S500,
 | 
			
		||||
                          }}
 | 
			
		||||
                          ref={virtualizer.measureElement}
 | 
			
		||||
                          key={vItem.index}
 | 
			
		||||
                        >
 | 
			
		||||
                          <RoomItemCard
 | 
			
		||||
                            item={item}
 | 
			
		||||
                            onSpaceFound={addSpaceRoom}
 | 
			
		||||
                            dm={mDirects.has(item.roomId)}
 | 
			
		||||
                            firstChild={!prevItem || prevItem.space === true}
 | 
			
		||||
                            lastChild={!nextItem || nextItem.space === true}
 | 
			
		||||
                            onOpen={handleOpenRoom}
 | 
			
		||||
                            getRoom={getRoom}
 | 
			
		||||
                            canReorder={canEditSpaceChild(parentPowerLevels)}
 | 
			
		||||
                            options={
 | 
			
		||||
                              <HierarchyItemMenu
 | 
			
		||||
                                item={item}
 | 
			
		||||
                                canInvite={canInvite}
 | 
			
		||||
                                joined={isJoined}
 | 
			
		||||
                                canEditChild={canEditSpaceChild(parentPowerLevels)}
 | 
			
		||||
                              />
 | 
			
		||||
                          <SpaceHierarchy
 | 
			
		||||
                            spaceItem={item.space}
 | 
			
		||||
                            summary={spacesItems.get(item.space.roomId)}
 | 
			
		||||
                            roomItems={item.rooms}
 | 
			
		||||
                            allJoinedRooms={allJoinedRooms}
 | 
			
		||||
                            mDirects={mDirects}
 | 
			
		||||
                            roomsPowerLevels={roomsPowerLevels}
 | 
			
		||||
                            canEditSpaceChild={canEditSpaceChild}
 | 
			
		||||
                            categoryId={categoryId}
 | 
			
		||||
                            closed={
 | 
			
		||||
                              closedCategories.has(categoryId) ||
 | 
			
		||||
                              (draggingItem ? 'space' in draggingItem : false)
 | 
			
		||||
                            }
 | 
			
		||||
                            after={
 | 
			
		||||
                              <AfterItemDropTarget
 | 
			
		||||
                                item={item}
 | 
			
		||||
                                nextRoomId={nextRoomId}
 | 
			
		||||
                                canDrop={canDrop}
 | 
			
		||||
                              />
 | 
			
		||||
                            }
 | 
			
		||||
                            data-dragging={dragging}
 | 
			
		||||
                            handleClose={handleCategoryClick}
 | 
			
		||||
                            draggingItem={draggingItem}
 | 
			
		||||
                            onDragging={setDraggingItem}
 | 
			
		||||
                            canDrop={canDrop}
 | 
			
		||||
                            nextSpaceId={nextSpaceId}
 | 
			
		||||
                            getRoom={getRoom}
 | 
			
		||||
                            pinned={sidebarSpaces.has(item.space.roomId)}
 | 
			
		||||
                            togglePinToSidebar={togglePinToSidebar}
 | 
			
		||||
                            onSpacesFound={handleSpacesFound}
 | 
			
		||||
                            onOpenRoom={handleOpenRoom}
 | 
			
		||||
                          />
 | 
			
		||||
                        </VirtualTile>
 | 
			
		||||
                      );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef } from 'react';
 | 
			
		||||
import React, { MouseEventHandler, ReactNode, useCallback, useRef } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Badge,
 | 
			
		||||
| 
						 | 
				
			
			@ -20,23 +20,20 @@ import {
 | 
			
		|||
} from 'folds';
 | 
			
		||||
import FocusTrap from 'focus-trap-react';
 | 
			
		||||
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
 | 
			
		||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
 | 
			
		||||
import { SequenceCard } from '../../components/sequence-card';
 | 
			
		||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
			
		||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
 | 
			
		||||
import { millify } from '../../plugins/millify';
 | 
			
		||||
import {
 | 
			
		||||
  HierarchyRoomSummaryLoader,
 | 
			
		||||
  LocalRoomSummaryLoader,
 | 
			
		||||
} from '../../components/RoomSummaryLoader';
 | 
			
		||||
import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
 | 
			
		||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
			
		||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
 | 
			
		||||
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
 | 
			
		||||
import { Membership, RoomType } from '../../../types/matrix/room';
 | 
			
		||||
import { Membership } from '../../../types/matrix/room';
 | 
			
		||||
import * as css from './RoomItem.css';
 | 
			
		||||
import * as styleCss from './style.css';
 | 
			
		||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
			
		||||
import { ErrorCode } from '../../cs-errorcode';
 | 
			
		||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
 | 
			
		||||
import { ItemDraggableTarget, useDraggableItem } from './DnD';
 | 
			
		||||
import { mxcUrlToHttp } from '../../utils/matrix';
 | 
			
		||||
| 
						 | 
				
			
			@ -125,13 +122,11 @@ function RoomProfileLoading() {
 | 
			
		|||
 | 
			
		||||
type RoomProfileErrorProps = {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  error: Error;
 | 
			
		||||
  inaccessibleRoom: boolean;
 | 
			
		||||
  suggested?: boolean;
 | 
			
		||||
  via?: string[];
 | 
			
		||||
};
 | 
			
		||||
function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorProps) {
 | 
			
		||||
  const privateRoom = error.name === ErrorCode.M_FORBIDDEN;
 | 
			
		||||
 | 
			
		||||
function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProfileErrorProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Box grow="Yes" gap="300">
 | 
			
		||||
      <Avatar>
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +137,7 @@ function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorPro
 | 
			
		|||
          renderFallback={() => (
 | 
			
		||||
            <RoomIcon
 | 
			
		||||
              size="300"
 | 
			
		||||
              joinRule={privateRoom ? JoinRule.Invite : JoinRule.Restricted}
 | 
			
		||||
              joinRule={inaccessibleRoom ? JoinRule.Invite : JoinRule.Restricted}
 | 
			
		||||
              filled
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
| 
						 | 
				
			
			@ -162,25 +157,18 @@ function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorPro
 | 
			
		|||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Box gap="200" alignItems="Center">
 | 
			
		||||
          {privateRoom && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
 | 
			
		||||
                <Text size="L400">Private Room</Text>
 | 
			
		||||
              </Badge>
 | 
			
		||||
              <Line
 | 
			
		||||
                variant="SurfaceVariant"
 | 
			
		||||
                style={{ height: toRem(12) }}
 | 
			
		||||
                direction="Vertical"
 | 
			
		||||
                size="400"
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          {inaccessibleRoom ? (
 | 
			
		||||
            <Badge variant="Secondary" fill="Soft" radii="300" size="500">
 | 
			
		||||
              <Text size="L400">Inaccessible</Text>
 | 
			
		||||
            </Badge>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text size="T200" truncate>
 | 
			
		||||
              {roomId}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
          <Text size="T200" truncate>
 | 
			
		||||
            {roomId}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
      {!privateRoom && <RoomJoinButton roomId={roomId} via={via} />}
 | 
			
		||||
      {!inaccessibleRoom && <RoomJoinButton roomId={roomId} via={via} />}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -288,23 +276,11 @@ function RoomProfile({
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CallbackOnFoundSpace({
 | 
			
		||||
  roomId,
 | 
			
		||||
  onSpaceFound,
 | 
			
		||||
}: {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  onSpaceFound: (roomId: string) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    onSpaceFound(roomId);
 | 
			
		||||
  }, [roomId, onSpaceFound]);
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RoomItemCardProps = {
 | 
			
		||||
  item: HierarchyItem;
 | 
			
		||||
  onSpaceFound: (roomId: string) => void;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  error: Error | null;
 | 
			
		||||
  summary: IHierarchyRoom | undefined;
 | 
			
		||||
  dm?: boolean;
 | 
			
		||||
  firstChild?: boolean;
 | 
			
		||||
  lastChild?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -320,10 +296,10 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
 | 
			
		|||
  (
 | 
			
		||||
    {
 | 
			
		||||
      item,
 | 
			
		||||
      onSpaceFound,
 | 
			
		||||
      loading,
 | 
			
		||||
      error,
 | 
			
		||||
      summary,
 | 
			
		||||
      dm,
 | 
			
		||||
      firstChild,
 | 
			
		||||
      lastChild,
 | 
			
		||||
      onOpen,
 | 
			
		||||
      options,
 | 
			
		||||
      before,
 | 
			
		||||
| 
						 | 
				
			
			@ -348,8 +324,6 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
 | 
			
		|||
    return (
 | 
			
		||||
      <SequenceCard
 | 
			
		||||
        className={css.RoomItemCard}
 | 
			
		||||
        firstChild={firstChild}
 | 
			
		||||
        lastChild={lastChild}
 | 
			
		||||
        variant="SurfaceVariant"
 | 
			
		||||
        gap="300"
 | 
			
		||||
        alignItems="Center"
 | 
			
		||||
| 
						 | 
				
			
			@ -367,7 +341,9 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
 | 
			
		|||
                  name={localSummary.name}
 | 
			
		||||
                  topic={localSummary.topic}
 | 
			
		||||
                  avatarUrl={
 | 
			
		||||
                    dm ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
 | 
			
		||||
                    dm
 | 
			
		||||
                      ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
 | 
			
		||||
                      : getRoomAvatarUrl(mx, room, 96, useAuthentication)
 | 
			
		||||
                  }
 | 
			
		||||
                  memberCount={localSummary.memberCount}
 | 
			
		||||
                  suggested={content.suggested}
 | 
			
		||||
| 
						 | 
				
			
			@ -395,46 +371,46 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
 | 
			
		|||
              )}
 | 
			
		||||
            </LocalRoomSummaryLoader>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <HierarchyRoomSummaryLoader roomId={roomId}>
 | 
			
		||||
              {(summaryState) => (
 | 
			
		||||
                <>
 | 
			
		||||
                  {summaryState.status === AsyncStatus.Loading && <RoomProfileLoading />}
 | 
			
		||||
                  {summaryState.status === AsyncStatus.Error && (
 | 
			
		||||
                    <RoomProfileError
 | 
			
		||||
                      roomId={roomId}
 | 
			
		||||
                      error={summaryState.error}
 | 
			
		||||
                      suggested={content.suggested}
 | 
			
		||||
                      via={content.via}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {summaryState.status === AsyncStatus.Success && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {summaryState.data.room_type === RoomType.Space && (
 | 
			
		||||
                        <CallbackOnFoundSpace
 | 
			
		||||
                          roomId={summaryState.data.room_id}
 | 
			
		||||
                          onSpaceFound={onSpaceFound}
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                      <RoomProfile
 | 
			
		||||
            <>
 | 
			
		||||
              {!summary &&
 | 
			
		||||
                (error ? (
 | 
			
		||||
                  <RoomProfileError
 | 
			
		||||
                    roomId={roomId}
 | 
			
		||||
                    inaccessibleRoom={false}
 | 
			
		||||
                    suggested={content.suggested}
 | 
			
		||||
                    via={content.via}
 | 
			
		||||
                  />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {loading && <RoomProfileLoading />}
 | 
			
		||||
                    {!loading && (
 | 
			
		||||
                      <RoomProfileError
 | 
			
		||||
                        roomId={roomId}
 | 
			
		||||
                        name={summaryState.data.name || summaryState.data.canonical_alias || roomId}
 | 
			
		||||
                        topic={summaryState.data.topic}
 | 
			
		||||
                        avatarUrl={
 | 
			
		||||
                          summaryState.data?.avatar_url
 | 
			
		||||
                            ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
 | 
			
		||||
                            undefined
 | 
			
		||||
                            : undefined
 | 
			
		||||
                        }
 | 
			
		||||
                        memberCount={summaryState.data.num_joined_members}
 | 
			
		||||
                        inaccessibleRoom
 | 
			
		||||
                        suggested={content.suggested}
 | 
			
		||||
                        joinRule={summaryState.data.join_rule}
 | 
			
		||||
                        options={<RoomJoinButton roomId={roomId} via={content.via} />}
 | 
			
		||||
                        via={content.via}
 | 
			
		||||
                      />
 | 
			
		||||
                    </>
 | 
			
		||||
                  )}
 | 
			
		||||
                </>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </>
 | 
			
		||||
                ))}
 | 
			
		||||
              {summary && (
 | 
			
		||||
                <RoomProfile
 | 
			
		||||
                  roomId={roomId}
 | 
			
		||||
                  name={summary.name || summary.canonical_alias || roomId}
 | 
			
		||||
                  topic={summary.topic}
 | 
			
		||||
                  avatarUrl={
 | 
			
		||||
                    summary?.avatar_url
 | 
			
		||||
                      ? mxcUrlToHttp(mx, summary.avatar_url, useAuthentication, 96, 96, 'crop') ??
 | 
			
		||||
                        undefined
 | 
			
		||||
                      : undefined
 | 
			
		||||
                  }
 | 
			
		||||
                  memberCount={summary.num_joined_members}
 | 
			
		||||
                  suggested={content.suggested}
 | 
			
		||||
                  joinRule={summary.join_rule}
 | 
			
		||||
                  options={<RoomJoinButton roomId={roomId} via={content.via} />}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </HierarchyRoomSummaryLoader>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
        {options}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										225
									
								
								src/app/features/lobby/SpaceHierarchy.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/app/features/lobby/SpaceHierarchy.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,225 @@
 | 
			
		|||
import React, { forwardRef, MouseEventHandler, useEffect, useMemo } from 'react';
 | 
			
		||||
import { MatrixError, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
 | 
			
		||||
import { Box, config, Text } from 'folds';
 | 
			
		||||
import {
 | 
			
		||||
  HierarchyItem,
 | 
			
		||||
  HierarchyItemRoom,
 | 
			
		||||
  HierarchyItemSpace,
 | 
			
		||||
  useFetchSpaceHierarchyLevel,
 | 
			
		||||
} from '../../hooks/useSpaceHierarchy';
 | 
			
		||||
import { IPowerLevels, powerLevelAPI } from '../../hooks/usePowerLevels';
 | 
			
		||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
			
		||||
import { SpaceItemCard } from './SpaceItem';
 | 
			
		||||
import { AfterItemDropTarget, CanDropCallback } from './DnD';
 | 
			
		||||
import { HierarchyItemMenu } from './HierarchyItemMenu';
 | 
			
		||||
import { RoomItemCard } from './RoomItem';
 | 
			
		||||
import { RoomType } from '../../../types/matrix/room';
 | 
			
		||||
import { SequenceCard } from '../../components/sequence-card';
 | 
			
		||||
 | 
			
		||||
type SpaceHierarchyProps = {
 | 
			
		||||
  summary: IHierarchyRoom | undefined;
 | 
			
		||||
  spaceItem: HierarchyItemSpace;
 | 
			
		||||
  roomItems?: HierarchyItemRoom[];
 | 
			
		||||
  allJoinedRooms: Set<string>;
 | 
			
		||||
  mDirects: Set<string>;
 | 
			
		||||
  roomsPowerLevels: Map<string, IPowerLevels>;
 | 
			
		||||
  canEditSpaceChild: (powerLevels: IPowerLevels) => boolean;
 | 
			
		||||
  categoryId: string;
 | 
			
		||||
  closed: boolean;
 | 
			
		||||
  handleClose: MouseEventHandler<HTMLButtonElement>;
 | 
			
		||||
  draggingItem?: HierarchyItem;
 | 
			
		||||
  onDragging: (item?: HierarchyItem) => void;
 | 
			
		||||
  canDrop: CanDropCallback;
 | 
			
		||||
  nextSpaceId?: string;
 | 
			
		||||
  getRoom: (roomId: string) => Room | undefined;
 | 
			
		||||
  pinned: boolean;
 | 
			
		||||
  togglePinToSidebar: (roomId: string) => void;
 | 
			
		||||
  onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
 | 
			
		||||
  onOpenRoom: MouseEventHandler<HTMLButtonElement>;
 | 
			
		||||
};
 | 
			
		||||
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
 | 
			
		||||
  (
 | 
			
		||||
    {
 | 
			
		||||
      summary,
 | 
			
		||||
      spaceItem,
 | 
			
		||||
      roomItems,
 | 
			
		||||
      allJoinedRooms,
 | 
			
		||||
      mDirects,
 | 
			
		||||
      roomsPowerLevels,
 | 
			
		||||
      canEditSpaceChild,
 | 
			
		||||
      categoryId,
 | 
			
		||||
      closed,
 | 
			
		||||
      handleClose,
 | 
			
		||||
      draggingItem,
 | 
			
		||||
      onDragging,
 | 
			
		||||
      canDrop,
 | 
			
		||||
      nextSpaceId,
 | 
			
		||||
      getRoom,
 | 
			
		||||
      pinned,
 | 
			
		||||
      togglePinToSidebar,
 | 
			
		||||
      onOpenRoom,
 | 
			
		||||
      onSpacesFound,
 | 
			
		||||
    },
 | 
			
		||||
    ref
 | 
			
		||||
  ) => {
 | 
			
		||||
    const mx = useMatrixClient();
 | 
			
		||||
 | 
			
		||||
    const { fetching, error, rooms } = useFetchSpaceHierarchyLevel(spaceItem.roomId, true);
 | 
			
		||||
 | 
			
		||||
    const subspaces = useMemo(() => {
 | 
			
		||||
      const s: Map<string, IHierarchyRoom> = new Map();
 | 
			
		||||
      rooms.forEach((r) => {
 | 
			
		||||
        if (r.room_type === RoomType.Space) {
 | 
			
		||||
          s.set(r.room_id, r);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      return s;
 | 
			
		||||
    }, [rooms]);
 | 
			
		||||
 | 
			
		||||
    const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId) ?? {};
 | 
			
		||||
    const userPLInSpace = powerLevelAPI.getPowerLevel(
 | 
			
		||||
      spacePowerLevels,
 | 
			
		||||
      mx.getUserId() ?? undefined
 | 
			
		||||
    );
 | 
			
		||||
    const canInviteInSpace = powerLevelAPI.canDoAction(spacePowerLevels, 'invite', userPLInSpace);
 | 
			
		||||
 | 
			
		||||
    const draggingSpace =
 | 
			
		||||
      draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId;
 | 
			
		||||
 | 
			
		||||
    const { parentId } = spaceItem;
 | 
			
		||||
    const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) ?? {} : undefined;
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      onSpacesFound(Array.from(subspaces.values()));
 | 
			
		||||
    }, [subspaces, onSpacesFound]);
 | 
			
		||||
 | 
			
		||||
    let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
 | 
			
		||||
    if (!canEditSpaceChild(spacePowerLevels)) {
 | 
			
		||||
      // 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 (
 | 
			
		||||
      <Box direction="Column" gap="100" ref={ref}>
 | 
			
		||||
        <SpaceItemCard
 | 
			
		||||
          summary={rooms.get(spaceItem.roomId) ?? summary}
 | 
			
		||||
          loading={fetching}
 | 
			
		||||
          item={spaceItem}
 | 
			
		||||
          joined={allJoinedRooms.has(spaceItem.roomId)}
 | 
			
		||||
          categoryId={categoryId}
 | 
			
		||||
          closed={closed}
 | 
			
		||||
          handleClose={handleClose}
 | 
			
		||||
          getRoom={getRoom}
 | 
			
		||||
          canEditChild={canEditSpaceChild(spacePowerLevels)}
 | 
			
		||||
          canReorder={parentPowerLevels ? canEditSpaceChild(parentPowerLevels) : false}
 | 
			
		||||
          options={
 | 
			
		||||
            parentId &&
 | 
			
		||||
            parentPowerLevels && (
 | 
			
		||||
              <HierarchyItemMenu
 | 
			
		||||
                item={{ ...spaceItem, parentId }}
 | 
			
		||||
                canInvite={canInviteInSpace}
 | 
			
		||||
                joined={allJoinedRooms.has(spaceItem.roomId)}
 | 
			
		||||
                canEditChild={canEditSpaceChild(parentPowerLevels)}
 | 
			
		||||
                pinned={pinned}
 | 
			
		||||
                onTogglePin={togglePinToSidebar}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          after={
 | 
			
		||||
            <AfterItemDropTarget
 | 
			
		||||
              item={spaceItem}
 | 
			
		||||
              nextRoomId={closed ? nextSpaceId : childItems?.[0]?.roomId}
 | 
			
		||||
              afterSpace
 | 
			
		||||
              canDrop={canDrop}
 | 
			
		||||
            />
 | 
			
		||||
          }
 | 
			
		||||
          onDragging={onDragging}
 | 
			
		||||
          data-dragging={draggingSpace}
 | 
			
		||||
        />
 | 
			
		||||
        {childItems && childItems.length > 0 ? (
 | 
			
		||||
          <Box direction="Column" gap="100">
 | 
			
		||||
            {childItems.map((roomItem, index) => {
 | 
			
		||||
              const roomSummary = rooms.get(roomItem.roomId);
 | 
			
		||||
 | 
			
		||||
              const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
 | 
			
		||||
              const userPLInRoom = powerLevelAPI.getPowerLevel(
 | 
			
		||||
                roomPowerLevels,
 | 
			
		||||
                mx.getUserId() ?? undefined
 | 
			
		||||
              );
 | 
			
		||||
              const canInviteInRoom = powerLevelAPI.canDoAction(
 | 
			
		||||
                roomPowerLevels,
 | 
			
		||||
                'invite',
 | 
			
		||||
                userPLInRoom
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
              const lastItem = index === childItems.length;
 | 
			
		||||
              const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
 | 
			
		||||
 | 
			
		||||
              const roomDragging =
 | 
			
		||||
                draggingItem?.roomId === roomItem.roomId &&
 | 
			
		||||
                draggingItem.parentId === roomItem.parentId;
 | 
			
		||||
 | 
			
		||||
              return (
 | 
			
		||||
                <RoomItemCard
 | 
			
		||||
                  key={roomItem.roomId}
 | 
			
		||||
                  item={roomItem}
 | 
			
		||||
                  loading={fetching}
 | 
			
		||||
                  error={error}
 | 
			
		||||
                  summary={roomSummary}
 | 
			
		||||
                  dm={mDirects.has(roomItem.roomId)}
 | 
			
		||||
                  onOpen={onOpenRoom}
 | 
			
		||||
                  getRoom={getRoom}
 | 
			
		||||
                  canReorder={canEditSpaceChild(spacePowerLevels)}
 | 
			
		||||
                  options={
 | 
			
		||||
                    <HierarchyItemMenu
 | 
			
		||||
                      item={roomItem}
 | 
			
		||||
                      canInvite={canInviteInRoom}
 | 
			
		||||
                      joined={allJoinedRooms.has(roomItem.roomId)}
 | 
			
		||||
                      canEditChild={canEditSpaceChild(spacePowerLevels)}
 | 
			
		||||
                    />
 | 
			
		||||
                  }
 | 
			
		||||
                  after={
 | 
			
		||||
                    <AfterItemDropTarget
 | 
			
		||||
                      item={roomItem}
 | 
			
		||||
                      nextRoomId={nextRoomId}
 | 
			
		||||
                      canDrop={canDrop}
 | 
			
		||||
                    />
 | 
			
		||||
                  }
 | 
			
		||||
                  data-dragging={roomDragging}
 | 
			
		||||
                  onDragging={onDragging}
 | 
			
		||||
                />
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          childItems && (
 | 
			
		||||
            <SequenceCard variant="SurfaceVariant" gap="300" alignItems="Center">
 | 
			
		||||
              <Box
 | 
			
		||||
                grow="Yes"
 | 
			
		||||
                style={{
 | 
			
		||||
                  padding: config.space.S700,
 | 
			
		||||
                }}
 | 
			
		||||
                direction="Column"
 | 
			
		||||
                alignItems="Center"
 | 
			
		||||
                justifyContent="Center"
 | 
			
		||||
                gap="100"
 | 
			
		||||
              >
 | 
			
		||||
                <Text size="H5" align="Center">
 | 
			
		||||
                  No Rooms
 | 
			
		||||
                </Text>
 | 
			
		||||
                <Text align="Center" size="T300" priority="300">
 | 
			
		||||
                  This space does not contains rooms yet.
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Box>
 | 
			
		||||
            </SequenceCard>
 | 
			
		||||
          )
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -19,19 +19,16 @@ import {
 | 
			
		|||
import FocusTrap from 'focus-trap-react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { MatrixError, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
 | 
			
		||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
 | 
			
		||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
			
		||||
import { RoomAvatar } from '../../components/room-avatar';
 | 
			
		||||
import { nameInitials } from '../../utils/common';
 | 
			
		||||
import {
 | 
			
		||||
  HierarchyRoomSummaryLoader,
 | 
			
		||||
  LocalRoomSummaryLoader,
 | 
			
		||||
} from '../../components/RoomSummaryLoader';
 | 
			
		||||
import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
 | 
			
		||||
import { getRoomAvatarUrl } from '../../utils/room';
 | 
			
		||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
			
		||||
import * as css from './SpaceItem.css';
 | 
			
		||||
import * as styleCss from './style.css';
 | 
			
		||||
import { ErrorCode } from '../../cs-errorcode';
 | 
			
		||||
import { useDraggableItem } from './DnD';
 | 
			
		||||
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
 | 
			
		||||
import { stopPropagation } from '../../utils/keyboard';
 | 
			
		||||
| 
						 | 
				
			
			@ -53,18 +50,11 @@ function SpaceProfileLoading() {
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UnknownPrivateSpaceProfileProps = {
 | 
			
		||||
type InaccessibleSpaceProfileProps = {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  name?: string;
 | 
			
		||||
  avatarUrl?: string;
 | 
			
		||||
  suggested?: boolean;
 | 
			
		||||
};
 | 
			
		||||
function UnknownPrivateSpaceProfile({
 | 
			
		||||
  roomId,
 | 
			
		||||
  name,
 | 
			
		||||
  avatarUrl,
 | 
			
		||||
  suggested,
 | 
			
		||||
}: UnknownPrivateSpaceProfileProps) {
 | 
			
		||||
function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Chip
 | 
			
		||||
      as="span"
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +65,9 @@ function UnknownPrivateSpaceProfile({
 | 
			
		|||
        <Avatar size="200" radii="300">
 | 
			
		||||
          <RoomAvatar
 | 
			
		||||
            roomId={roomId}
 | 
			
		||||
            src={avatarUrl}
 | 
			
		||||
            alt={name}
 | 
			
		||||
            renderFallback={() => (
 | 
			
		||||
              <Text as="span" size="H6">
 | 
			
		||||
                {nameInitials(name)}
 | 
			
		||||
                U
 | 
			
		||||
              </Text>
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
| 
						 | 
				
			
			@ -88,11 +76,11 @@ function UnknownPrivateSpaceProfile({
 | 
			
		|||
    >
 | 
			
		||||
      <Box alignItems="Center" gap="200">
 | 
			
		||||
        <Text size="H4" truncate>
 | 
			
		||||
          {name || 'Unknown'}
 | 
			
		||||
          Unknown
 | 
			
		||||
        </Text>
 | 
			
		||||
 | 
			
		||||
        <Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
 | 
			
		||||
          <Text size="L400">Private Space</Text>
 | 
			
		||||
          <Text size="L400">Inaccessible</Text>
 | 
			
		||||
        </Badge>
 | 
			
		||||
        {suggested && (
 | 
			
		||||
          <Badge variant="Success" fill="Soft" radii="Pill" outlined>
 | 
			
		||||
| 
						 | 
				
			
			@ -104,20 +92,20 @@ function UnknownPrivateSpaceProfile({
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UnknownSpaceProfileProps = {
 | 
			
		||||
type UnjoinedSpaceProfileProps = {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  via?: string[];
 | 
			
		||||
  name?: string;
 | 
			
		||||
  avatarUrl?: string;
 | 
			
		||||
  suggested?: boolean;
 | 
			
		||||
};
 | 
			
		||||
function UnknownSpaceProfile({
 | 
			
		||||
function UnjoinedSpaceProfile({
 | 
			
		||||
  roomId,
 | 
			
		||||
  via,
 | 
			
		||||
  name,
 | 
			
		||||
  avatarUrl,
 | 
			
		||||
  suggested,
 | 
			
		||||
}: UnknownSpaceProfileProps) {
 | 
			
		||||
}: UnjoinedSpaceProfileProps) {
 | 
			
		||||
  const mx = useMatrixClient();
 | 
			
		||||
 | 
			
		||||
  const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
 | 
			
		||||
| 
						 | 
				
			
			@ -376,6 +364,8 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
type SpaceItemCardProps = {
 | 
			
		||||
  summary: IHierarchyRoom | undefined;
 | 
			
		||||
  loading?: boolean;
 | 
			
		||||
  item: HierarchyItem;
 | 
			
		||||
  joined?: boolean;
 | 
			
		||||
  categoryId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -393,6 +383,8 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
 | 
			
		|||
  (
 | 
			
		||||
    {
 | 
			
		||||
      className,
 | 
			
		||||
      summary,
 | 
			
		||||
      loading,
 | 
			
		||||
      joined,
 | 
			
		||||
      closed,
 | 
			
		||||
      categoryId,
 | 
			
		||||
| 
						 | 
				
			
			@ -451,37 +443,31 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
 | 
			
		|||
                }
 | 
			
		||||
              </LocalRoomSummaryLoader>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <HierarchyRoomSummaryLoader roomId={roomId}>
 | 
			
		||||
                {(summaryState) => (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {summaryState.status === AsyncStatus.Loading && <SpaceProfileLoading />}
 | 
			
		||||
                    {summaryState.status === AsyncStatus.Error &&
 | 
			
		||||
                      (summaryState.error.name === ErrorCode.M_FORBIDDEN ? (
 | 
			
		||||
                        <UnknownPrivateSpaceProfile roomId={roomId} suggested={content.suggested} />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <UnknownSpaceProfile
 | 
			
		||||
                          roomId={roomId}
 | 
			
		||||
                          via={item.content.via}
 | 
			
		||||
                          suggested={content.suggested}
 | 
			
		||||
                        />
 | 
			
		||||
                      ))}
 | 
			
		||||
                    {summaryState.status === AsyncStatus.Success && (
 | 
			
		||||
                      <UnknownSpaceProfile
 | 
			
		||||
                        roomId={roomId}
 | 
			
		||||
                        via={item.content.via}
 | 
			
		||||
                        name={summaryState.data.name || summaryState.data.canonical_alias || roomId}
 | 
			
		||||
                        avatarUrl={
 | 
			
		||||
                          summaryState.data?.avatar_url
 | 
			
		||||
                            ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
 | 
			
		||||
                            undefined
 | 
			
		||||
                            : undefined
 | 
			
		||||
                        }
 | 
			
		||||
                        suggested={content.suggested}
 | 
			
		||||
                      />
 | 
			
		||||
                    )}
 | 
			
		||||
                  </>
 | 
			
		||||
              <>
 | 
			
		||||
                {!summary &&
 | 
			
		||||
                  (loading ? (
 | 
			
		||||
                    <SpaceProfileLoading />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <InaccessibleSpaceProfile
 | 
			
		||||
                      roomId={item.roomId}
 | 
			
		||||
                      suggested={item.content.suggested}
 | 
			
		||||
                    />
 | 
			
		||||
                  ))}
 | 
			
		||||
                {summary && (
 | 
			
		||||
                  <UnjoinedSpaceProfile
 | 
			
		||||
                    roomId={roomId}
 | 
			
		||||
                    via={item.content.via}
 | 
			
		||||
                    name={summary.name || summary.canonical_alias || roomId}
 | 
			
		||||
                    avatarUrl={
 | 
			
		||||
                      summary?.avatar_url
 | 
			
		||||
                        ? mxcUrlToHttp(mx, summary.avatar_url, useAuthentication, 96, 96, 'crop') ??
 | 
			
		||||
                          undefined
 | 
			
		||||
                        : undefined
 | 
			
		||||
                    }
 | 
			
		||||
                    suggested={content.suggested}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
              </HierarchyRoomSummaryLoader>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
          {canEditChild && (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
import { atom, useAtom, useAtomValue } from 'jotai';
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Room } from 'matrix-js-sdk';
 | 
			
		||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { MatrixError, Room } from 'matrix-js-sdk';
 | 
			
		||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
 | 
			
		||||
import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query';
 | 
			
		||||
import { useMatrixClient } from './useMatrixClient';
 | 
			
		||||
import { roomToParentsAtom } from '../state/room/roomToParents';
 | 
			
		||||
import { MSpaceChildContent, StateEvent } from '../../types/matrix/room';
 | 
			
		||||
| 
						 | 
				
			
			@ -8,22 +10,24 @@ import { getAllParents, getStateEvents, isValidChild } from '../utils/room';
 | 
			
		|||
import { isRoomId } from '../utils/matrix';
 | 
			
		||||
import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
 | 
			
		||||
import { useStateEventCallback } from './useStateEventCallback';
 | 
			
		||||
import { ErrorCode } from '../cs-errorcode';
 | 
			
		||||
 | 
			
		||||
export type HierarchyItem =
 | 
			
		||||
  | {
 | 
			
		||||
      roomId: string;
 | 
			
		||||
      content: MSpaceChildContent;
 | 
			
		||||
      ts: number;
 | 
			
		||||
      space: true;
 | 
			
		||||
      parentId?: string;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      roomId: string;
 | 
			
		||||
      content: MSpaceChildContent;
 | 
			
		||||
      ts: number;
 | 
			
		||||
      space?: false;
 | 
			
		||||
      parentId: string;
 | 
			
		||||
    };
 | 
			
		||||
export type HierarchyItemSpace = {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  content: MSpaceChildContent;
 | 
			
		||||
  ts: number;
 | 
			
		||||
  space: true;
 | 
			
		||||
  parentId?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type HierarchyItemRoom = {
 | 
			
		||||
  roomId: string;
 | 
			
		||||
  content: MSpaceChildContent;
 | 
			
		||||
  ts: number;
 | 
			
		||||
  parentId: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type HierarchyItem = HierarchyItemSpace | HierarchyItemRoom;
 | 
			
		||||
 | 
			
		||||
type GetRoomCallback = (roomId: string) => Room | undefined;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -35,16 +39,16 @@ const getHierarchySpaces = (
 | 
			
		|||
  rootSpaceId: string,
 | 
			
		||||
  getRoom: GetRoomCallback,
 | 
			
		||||
  spaceRooms: Set<string>
 | 
			
		||||
): HierarchyItem[] => {
 | 
			
		||||
  const rootSpaceItem: HierarchyItem = {
 | 
			
		||||
): HierarchyItemSpace[] => {
 | 
			
		||||
  const rootSpaceItem: HierarchyItemSpace = {
 | 
			
		||||
    roomId: rootSpaceId,
 | 
			
		||||
    content: { via: [] },
 | 
			
		||||
    ts: 0,
 | 
			
		||||
    space: true,
 | 
			
		||||
  };
 | 
			
		||||
  let spaceItems: HierarchyItem[] = [];
 | 
			
		||||
  let spaceItems: HierarchyItemSpace[] = [];
 | 
			
		||||
 | 
			
		||||
  const findAndCollectHierarchySpaces = (spaceItem: HierarchyItem) => {
 | 
			
		||||
  const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => {
 | 
			
		||||
    if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
 | 
			
		||||
    const space = getRoom(spaceItem.roomId);
 | 
			
		||||
    spaceItems.push(spaceItem);
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +65,7 @@ const getHierarchySpaces = (
 | 
			
		|||
      // 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: HierarchyItem = {
 | 
			
		||||
        const childItem: HierarchyItemSpace = {
 | 
			
		||||
          roomId: childId,
 | 
			
		||||
          content: childEvent.getContent<MSpaceChildContent>(),
 | 
			
		||||
          ts: childEvent.getTs(),
 | 
			
		||||
| 
						 | 
				
			
			@ -85,28 +89,34 @@ const getHierarchySpaces = (
 | 
			
		|||
  return spaceItems;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SpaceHierarchy = {
 | 
			
		||||
  space: HierarchyItemSpace;
 | 
			
		||||
  rooms?: HierarchyItemRoom[];
 | 
			
		||||
};
 | 
			
		||||
const getSpaceHierarchy = (
 | 
			
		||||
  rootSpaceId: string,
 | 
			
		||||
  spaceRooms: Set<string>,
 | 
			
		||||
  getRoom: (roomId: string) => Room | undefined,
 | 
			
		||||
  closedCategory: (spaceId: string) => boolean
 | 
			
		||||
): HierarchyItem[] => {
 | 
			
		||||
  const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
 | 
			
		||||
): SpaceHierarchy[] => {
 | 
			
		||||
  const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
 | 
			
		||||
 | 
			
		||||
  const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
 | 
			
		||||
  const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => {
 | 
			
		||||
    const space = getRoom(spaceItem.roomId);
 | 
			
		||||
    if (!space || closedCategory(spaceItem.roomId)) {
 | 
			
		||||
      return [spaceItem];
 | 
			
		||||
      return {
 | 
			
		||||
        space: spaceItem,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    const childEvents = getStateEvents(space, StateEvent.SpaceChild);
 | 
			
		||||
    const childItems: HierarchyItem[] = [];
 | 
			
		||||
    const childItems: HierarchyItemRoom[] = [];
 | 
			
		||||
    childEvents.forEach((childEvent) => {
 | 
			
		||||
      if (!isValidChild(childEvent)) return;
 | 
			
		||||
      const childId = childEvent.getStateKey();
 | 
			
		||||
      if (!childId || !isRoomId(childId)) return;
 | 
			
		||||
      if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) return;
 | 
			
		||||
 | 
			
		||||
      const childItem: HierarchyItem = {
 | 
			
		||||
      const childItem: HierarchyItemRoom = {
 | 
			
		||||
        roomId: childId,
 | 
			
		||||
        content: childEvent.getContent<MSpaceChildContent>(),
 | 
			
		||||
        ts: childEvent.getTs(),
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +124,11 @@ const getSpaceHierarchy = (
 | 
			
		|||
      };
 | 
			
		||||
      childItems.push(childItem);
 | 
			
		||||
    });
 | 
			
		||||
    return [spaceItem, ...childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder)];
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      space: spaceItem,
 | 
			
		||||
      rooms: childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return hierarchy;
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +139,7 @@ export const useSpaceHierarchy = (
 | 
			
		|||
  spaceRooms: Set<string>,
 | 
			
		||||
  getRoom: (roomId: string) => Room | undefined,
 | 
			
		||||
  closedCategory: (spaceId: string) => boolean
 | 
			
		||||
): HierarchyItem[] => {
 | 
			
		||||
): SpaceHierarchy[] => {
 | 
			
		||||
  const mx = useMatrixClient();
 | 
			
		||||
  const roomToParents = useAtomValue(roomToParentsAtom);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +177,7 @@ const getSpaceJoinedHierarchy = (
 | 
			
		|||
  excludeRoom: (parentId: string, roomId: string) => boolean,
 | 
			
		||||
  sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
 | 
			
		||||
): HierarchyItem[] => {
 | 
			
		||||
  const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
 | 
			
		||||
  const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
 | 
			
		||||
 | 
			
		||||
  const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
 | 
			
		||||
    const space = getRoom(spaceItem.roomId);
 | 
			
		||||
| 
						 | 
				
			
			@ -182,14 +196,14 @@ const getSpaceJoinedHierarchy = (
 | 
			
		|||
 | 
			
		||||
    if (joinedRoomEvents.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
    const childItems: HierarchyItem[] = [];
 | 
			
		||||
    const childItems: HierarchyItemRoom[] = [];
 | 
			
		||||
    joinedRoomEvents.forEach((childEvent) => {
 | 
			
		||||
      const childId = childEvent.getStateKey();
 | 
			
		||||
      if (!childId) return;
 | 
			
		||||
 | 
			
		||||
      if (excludeRoom(space.roomId, childId)) return;
 | 
			
		||||
 | 
			
		||||
      const childItem: HierarchyItem = {
 | 
			
		||||
      const childItem: HierarchyItemRoom = {
 | 
			
		||||
        roomId: childId,
 | 
			
		||||
        content: childEvent.getContent<MSpaceChildContent>(),
 | 
			
		||||
        ts: childEvent.getTs(),
 | 
			
		||||
| 
						 | 
				
			
			@ -251,3 +265,85 @@ export const useSpaceJoinedHierarchy = (
 | 
			
		|||
 | 
			
		||||
  return hierarchy;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// we will paginate until 5000 items
 | 
			
		||||
const PER_PAGE_COUNT = 100;
 | 
			
		||||
const MAX_AUTO_PAGE_COUNT = 50;
 | 
			
		||||
export type FetchSpaceHierarchyLevelData = {
 | 
			
		||||
  fetching: boolean;
 | 
			
		||||
  error: Error | null;
 | 
			
		||||
  rooms: Map<string, IHierarchyRoom>;
 | 
			
		||||
};
 | 
			
		||||
export const useFetchSpaceHierarchyLevel = (
 | 
			
		||||
  roomId: string,
 | 
			
		||||
  enable: boolean
 | 
			
		||||
): FetchSpaceHierarchyLevelData => {
 | 
			
		||||
  const mx = useMatrixClient();
 | 
			
		||||
  const pageNoRef = useRef(0);
 | 
			
		||||
 | 
			
		||||
  const fetchLevel: QueryFunction<
 | 
			
		||||
    Awaited<ReturnType<typeof mx.getRoomHierarchy>>,
 | 
			
		||||
    string[],
 | 
			
		||||
    string | undefined
 | 
			
		||||
  > = useCallback(
 | 
			
		||||
    ({ pageParam }) => mx.getRoomHierarchy(roomId, PER_PAGE_COUNT, 1, false, pageParam),
 | 
			
		||||
    [roomId, mx]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const queryResponse = useInfiniteQuery({
 | 
			
		||||
    refetchOnMount: enable,
 | 
			
		||||
    queryKey: [roomId, 'hierarchy_level'],
 | 
			
		||||
    initialPageParam: undefined,
 | 
			
		||||
    queryFn: fetchLevel,
 | 
			
		||||
    getNextPageParam: (result) => {
 | 
			
		||||
      if (result.next_batch) return result.next_batch;
 | 
			
		||||
      return undefined;
 | 
			
		||||
    },
 | 
			
		||||
    retry: 5,
 | 
			
		||||
    retryDelay: (failureCount, error) => {
 | 
			
		||||
      if (error instanceof MatrixError && error.errcode === ErrorCode.M_LIMIT_EXCEEDED) {
 | 
			
		||||
        const { retry_after_ms: delay } = error.data;
 | 
			
		||||
        if (typeof delay === 'number') {
 | 
			
		||||
          return delay;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return 500 * failureCount;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const { data, isLoading, isFetchingNextPage, error, fetchNextPage, hasNextPage } = queryResponse;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      hasNextPage &&
 | 
			
		||||
      pageNoRef.current <= MAX_AUTO_PAGE_COUNT &&
 | 
			
		||||
      !error &&
 | 
			
		||||
      data &&
 | 
			
		||||
      data.pages.length > 0
 | 
			
		||||
    ) {
 | 
			
		||||
      pageNoRef.current += 1;
 | 
			
		||||
      fetchNextPage();
 | 
			
		||||
    }
 | 
			
		||||
  }, [fetchNextPage, hasNextPage, data, error]);
 | 
			
		||||
 | 
			
		||||
  const rooms: Map<string, IHierarchyRoom> = useMemo(() => {
 | 
			
		||||
    const roomsMap: Map<string, IHierarchyRoom> = new Map();
 | 
			
		||||
    if (!data) return roomsMap;
 | 
			
		||||
 | 
			
		||||
    const rms = data.pages.flatMap((result) => result.rooms);
 | 
			
		||||
    rms.forEach((r) => {
 | 
			
		||||
      roomsMap.set(r.room_id, r);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return roomsMap;
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  const fetching = isLoading || isFetchingNextPage;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    fetching,
 | 
			
		||||
    error,
 | 
			
		||||
    rooms,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,32 +23,37 @@ const baseSpaceRoomsAtom = atomWithLocalStorage<Set<string>>(
 | 
			
		|||
type SpaceRoomsAction =
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'PUT';
 | 
			
		||||
      roomId: string;
 | 
			
		||||
      roomIds: string[];
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'DELETE';
 | 
			
		||||
      roomId: string;
 | 
			
		||||
      roomIds: string[];
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
export const spaceRoomsAtom = atom<Set<string>, [SpaceRoomsAction], undefined>(
 | 
			
		||||
  (get) => get(baseSpaceRoomsAtom),
 | 
			
		||||
  (get, set, action) => {
 | 
			
		||||
    if (action.type === 'DELETE') {
 | 
			
		||||
    const current = get(baseSpaceRoomsAtom);
 | 
			
		||||
    const { type, roomIds } = action;
 | 
			
		||||
 | 
			
		||||
    if (type === 'DELETE' && roomIds.find((roomId) => current.has(roomId))) {
 | 
			
		||||
      set(
 | 
			
		||||
        baseSpaceRoomsAtom,
 | 
			
		||||
        produce(get(baseSpaceRoomsAtom), (draft) => {
 | 
			
		||||
          draft.delete(action.roomId);
 | 
			
		||||
        produce(current, (draft) => {
 | 
			
		||||
          roomIds.forEach((roomId) => draft.delete(roomId));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (action.type === 'PUT') {
 | 
			
		||||
      set(
 | 
			
		||||
        baseSpaceRoomsAtom,
 | 
			
		||||
        produce(get(baseSpaceRoomsAtom), (draft) => {
 | 
			
		||||
          draft.add(action.roomId);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    if (type === 'PUT') {
 | 
			
		||||
      const newEntries = roomIds.filter((roomId) => !current.has(roomId));
 | 
			
		||||
      if (newEntries.length > 0)
 | 
			
		||||
        set(
 | 
			
		||||
          baseSpaceRoomsAtom,
 | 
			
		||||
          produce(current, (draft) => {
 | 
			
		||||
            newEntries.forEach((roomId) => draft.add(roomId));
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue