Files
cinny/src/app/hooks/useSpaceHierarchy.ts
T

254 lines
8.1 KiB
TypeScript
Raw Normal View History

import { atom, useAtom, useAtomValue } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { Room } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { roomToParentsAtom } from '../state/room/roomToParents';
import { MSpaceChildContent, StateEvent } from '../../types/matrix/room';
import { getAllParents, getStateEvents, isValidChild } from '../utils/room';
import { isRoomId } from '../utils/matrix';
import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
import { useStateEventCallback } from './useStateEventCallback';
export type HierarchyItem =
| {
roomId: string;
content: MSpaceChildContent;
ts: number;
space: true;
parentId?: string;
}
| {
roomId: string;
content: MSpaceChildContent;
ts: number;
space?: false;
parentId: string;
};
type GetRoomCallback = (roomId: string) => Room | undefined;
const hierarchyItemTs: SortFunc<HierarchyItem> = (a, b) => byTsOldToNew(a.ts, b.ts);
const hierarchyItemByOrder: SortFunc<HierarchyItem> = (a, b) =>
byOrderKey(a.content.order, b.content.order);
const getHierarchySpaces = (
rootSpaceId: string,
getRoom: GetRoomCallback,
spaceRooms: Set<string>
): HierarchyItem[] => {
const rootSpaceItem: HierarchyItem = {
roomId: rootSpaceId,
content: { via: [] },
ts: 0,
space: true,
};
let spaceItems: HierarchyItem[] = [];
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItem) => {
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
const space = getRoom(spaceItem.roomId);
spaceItems.push(spaceItem);
if (!space) return;
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
childEvents.forEach((childEvent) => {
if (!isValidChild(childEvent)) return;
const childId = childEvent.getStateKey();
if (!childId || !isRoomId(childId)) return;
// because we can not find if a childId is space without joining
// or requesting room summary, we will look it into spaceRooms local
// cache which we maintain as we load summary in UI.
if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) {
const childItem: HierarchyItem = {
roomId: childId,
content: childEvent.getContent<MSpaceChildContent>(),
ts: childEvent.getTs(),
space: true,
parentId: spaceItem.roomId,
};
findAndCollectHierarchySpaces(childItem);
}
});
};
findAndCollectHierarchySpaces(rootSpaceItem);
spaceItems = [
rootSpaceItem,
...spaceItems
.filter((item) => item.roomId !== rootSpaceId)
.sort(hierarchyItemTs)
.sort(hierarchyItemByOrder),
];
return spaceItems;
};
const getSpaceHierarchy = (
rootSpaceId: string,
spaceRooms: Set<string>,
getRoom: (roomId: string) => Room | undefined,
closedCategory: (spaceId: string) => boolean
): HierarchyItem[] => {
const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
const space = getRoom(spaceItem.roomId);
if (!space || closedCategory(spaceItem.roomId)) {
return [spaceItem];
}
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
const childItems: HierarchyItem[] = [];
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 = {
roomId: childId,
content: childEvent.getContent<MSpaceChildContent>(),
ts: childEvent.getTs(),
parentId: spaceItem.roomId,
};
childItems.push(childItem);
});
return [spaceItem, ...childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder)];
});
return hierarchy;
};
export const useSpaceHierarchy = (
spaceId: string,
spaceRooms: Set<string>,
getRoom: (roomId: string) => Room | undefined,
closedCategory: (spaceId: string) => boolean
): HierarchyItem[] => {
const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom);
const [hierarchyAtom] = useState(() =>
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory))
);
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
useEffect(() => {
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]);
useStateEventCallback(
mx,
useCallback(
(mEvent) => {
if (mEvent.getType() !== StateEvent.SpaceChild) return;
const eventRoomId = mEvent.getRoomId();
if (!eventRoomId) return;
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
}
},
[spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory]
)
);
return hierarchy;
};
const getSpaceJoinedHierarchy = (
rootSpaceId: string,
getRoom: GetRoomCallback,
excludeRoom: (parentId: string, roomId: string) => boolean,
sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
): HierarchyItem[] => {
const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
const space = getRoom(spaceItem.roomId);
if (!space) {
return [];
}
const joinedRoomEvents = getStateEvents(space, StateEvent.SpaceChild).filter((childEvent) => {
if (!isValidChild(childEvent)) return false;
const childId = childEvent.getStateKey();
if (!childId || !isRoomId(childId)) return false;
const room = getRoom(childId);
if (!room || room.isSpaceRoom()) return false;
return true;
});
if (joinedRoomEvents.length === 0) return [];
const childItems: HierarchyItem[] = [];
joinedRoomEvents.forEach((childEvent) => {
const childId = childEvent.getStateKey();
if (!childId) return;
if (excludeRoom(space.roomId, childId)) return;
const childItem: HierarchyItem = {
roomId: childId,
content: childEvent.getContent<MSpaceChildContent>(),
ts: childEvent.getTs(),
parentId: spaceItem.roomId,
};
childItems.push(childItem);
});
return [spaceItem, ...sortRoomItems(spaceItem.roomId, childItems)];
});
return hierarchy;
};
export const useSpaceJoinedHierarchy = (
spaceId: string,
getRoom: GetRoomCallback,
excludeRoom: (parentId: string, roomId: string) => boolean,
sortByActivity: (spaceId: string) => boolean
): HierarchyItem[] => {
const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom);
const sortRoomItems = useCallback(
(sId: string, items: HierarchyItem[]) => {
if (sortByActivity(sId)) {
items.sort((a, b) => factoryRoomIdByActivity(mx)(a.roomId, b.roomId));
return items;
}
items.sort(hierarchyItemTs).sort(hierarchyItemByOrder);
return items;
},
[mx, sortByActivity]
);
const [hierarchyAtom] = useState(() =>
atom(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems))
);
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
useEffect(() => {
setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
}, [mx, spaceId, setHierarchy, getRoom, excludeRoom, sortRoomItems]);
useStateEventCallback(
mx,
useCallback(
(mEvent) => {
if (mEvent.getType() !== StateEvent.SpaceChild) return;
const eventRoomId = mEvent.getRoomId();
if (!eventRoomId) return;
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
}
},
[spaceId, roomToParents, setHierarchy, getRoom, excludeRoom, sortRoomItems]
)
);
return hierarchy;
};