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
src/app
|
@ -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