{
+ ev.stopPropagation();
+ toggleShowChildren();
+ }}
+ />;
+ if (showChildren) {
+ childSection =
+ { children }
+
;
+ }
+ }
+
+ return <>
+
+ { content }
+ { childToggle }
+
+ { childSection }
+ >;
};
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
@@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
interface IHierarchyLevelProps {
spaceId: string;
rooms: Map
;
- editing?: boolean;
- relations: EnhancedMap;
+ relations: EnhancedMap>;
parents: Set;
- queueAction?(action: IAction): void;
- onPreviewClick(roomId: string): void;
- onRemoveFromSpaceClick?(roomId: string): void;
- onJoinClick?(roomId: string): void;
+ selectedMap?: Map>;
+ onViewRoomClick(roomId: string, autoJoin: boolean): void;
+ onToggleClick?(parentId: string, childId: string): void;
}
export const HierarchyLevel = ({
spaceId,
rooms,
- editing,
relations,
parents,
- onPreviewClick,
- onJoinClick,
- queueAction,
+ selectedMap,
+ onViewRoomClick,
+ onToggleClick,
}: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get();
const space = cli.getRoom(spaceId);
- // TODO respect order
- const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
- if (!rooms.has(roomId)) return result; // TODO wat
+ const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())
+
+ const sortedChildren = sortBy([...relations.get(spaceId)?.values()], ev => ev.content.order || null);
+ const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
+ const roomId = ev.state_key;
+ if (!rooms.has(roomId)) return result;
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
return result;
}, [[], []]) || [[], []];
- // Don't render this subspace if it has no rooms we can show
- // TODO this is broken - as a space may have subspaces we still need to show
- // if (!childRooms.length) return null;
-
- const userId = cli.getUserId();
-
const newParents = new Set(parents).add(spaceId);
return
{
childRooms.map(roomId => (
- {
- onPreviewClick(roomId);
+ suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
+ selected={selectedMap?.get(spaceId)?.has(roomId)}
+ onViewRoomClick={(autoJoin) => {
+ onViewRoomClick(roomId, autoJoin);
}}
- onJoinClick={onJoinClick ? () => {
- onJoinClick(roomId);
- } : undefined}
+ hasPermissions={hasPermissions}
+ onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
/>
))
}
{
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
- {
- onPreviewClick(roomId);
- }}
- onJoinClick={() => {
- onJoinClick(roomId);
+ room={rooms.get(roomId)}
+ numChildRooms={Array.from(relations.get(roomId)?.values() || [])
+ .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
+ suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
+ selected={selectedMap?.get(spaceId)?.has(roomId)}
+ onViewRoomClick={(autoJoin) => {
+ onViewRoomClick(roomId, autoJoin);
}}
+ hasPermissions={hasPermissions}
+ onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined}
>
-
+
))
}
@@ -415,8 +319,8 @@ export const HierarchyLevel = ({
const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => {
// TODO pagination
const cli = MatrixClientPeg.get();
+ const userId = cli.getUserId();
const [query, setQuery] = useState(initialText);
- const [isEditing, setIsEditing] = useState(false);
const onCreateRoomClick = () => {
dis.dispatch({
@@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis
onFinished();
};
- // stored within a ref as we don't need to re-render when it changes
- const pendingActions = useRef(new Map());
+ const [selected, setSelected] = useState(new Map>()); // Map>
- let adminButton;
- if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
- const onManageButtonClicked = () => {
- setIsEditing(true);
- };
-
- const onSaveButtonClicked = () => {
- // TODO setBusy
- pendingActions.current.forEach(({event, suggested, removed}) => {
- const content = {
- ...event.getContent(),
- suggested,
- };
-
- if (removed) {
- delete content["via"];
- }
-
- cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
- });
- setIsEditing(false);
- };
-
- if (isEditing) {
- adminButton =
-
- { _t("Promoted to users") }
- ;
- } else {
- adminButton = ;
- }
- }
-
- const [rooms, relations, viaMap] = useAsyncMemo(async () => {
+ const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
try {
const data = await cli.getSpaceSummary(space.roomId);
- const parentChildRelations = new EnhancedMap();
+ const parentChildRelations = new EnhancedMap>();
+ const childParentRelations = new EnhancedMap>();
const viaMap = new EnhancedMap>();
data.events.map((ev: ISpaceSummaryEvent) => {
if (ev.type === EventType.SpaceChild) {
- parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
+ parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
+ childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id);
}
if (Array.isArray(ev.content["via"])) {
const set = viaMap.getOrCreate(ev.state_key, new Set());
@@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis
}
});
- return [data.rooms, parentChildRelations, viaMap];
+ return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap];
} catch (e) {
console.error(e); // TODO
}
@@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis
const roomsMap = useMemo(() => {
if (!rooms) return null;
- const lcQuery = query.toLowerCase();
+ const lcQuery = query.toLowerCase().trim();
- const filteredRooms = rooms.filter(r => {
- return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
- || r.name?.toLowerCase().includes(lcQuery)
- || r.topic?.toLowerCase().includes(lcQuery);
+ const roomsMap = new Map(rooms.map(r => [r.room_id, r]));
+ if (!lcQuery) return roomsMap;
+
+ const directMatches = rooms.filter(r => {
+ return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery);
});
- return new Map(filteredRooms.map(r => [r.room_id, r]));
- // const root = rooms.get(space.roomId);
- }, [rooms, query]);
+ // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy
+ const visited = new Set();
+ const queue = [...directMatches.map(r => r.room_id)];
+ while (queue.length) {
+ const roomId = queue.pop();
+ visited.add(roomId);
+ childParentMap.get(roomId)?.forEach(parentId => {
+ if (!visited.has(parentId)) {
+ queue.push(parentId);
+ }
+ });
+ }
+
+ // Remove any mappings for rooms which were not visited in the walk
+ Array.from(roomsMap.keys()).forEach(roomId => {
+ if (!visited.has(roomId)) {
+ roomsMap.delete(roomId);
+ }
+ });
+ return roomsMap;
+ }, [rooms, childParentMap, query]);
const title =
-
+
;
+
const explanation =
- _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null,
+ _t("If you can't find the room you're looking for, ask for an invite or create a new room.", null,
{a: sub => {
return {sub};
}},
);
+ const [error, setError] = useState("");
+ const [removing, setRemoving] = useState(false);
+ const [saving, setSaving] = useState(false);
+
let content;
if (roomsMap) {
- content =
- {
- pendingActions.current.set(action.event.room_id, action);
- }}
- onPreviewClick={roomId => {
- showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
- onFinished();
- }}
- onJoinClick={(roomId) => {
- showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
- onFinished();
- }}
- />
- ;
+ const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
+ const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
+
+ let countsStr;
+ if (numSpaces > 1) {
+ countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
+ } else if (numSpaces > 0) {
+ countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
+ } else {
+ countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
+ }
+
+ let editSection;
+ if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
+ const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
+ return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
+ });
+
+ let buttons;
+ if (selectedRelations.length) {
+ const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
+ return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
+ });
+
+ const disabled = removing || saving;
+
+ buttons = <>
+ {
+ setRemoving(true);
+ try {
+ for (const [parentId, childId] of selectedRelations) {
+ await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
+ parentChildMap.get(parentId).get(childId).content = {};
+ parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
+ }
+ } catch (e) {
+ setError(_t("Failed to remove some rooms. Try again later"));
+ }
+ setRemoving(false);
+ }}
+ kind="danger_outline"
+ disabled={disabled}
+ >
+ { removing ? _t("Removing...") : _t("Remove") }
+
+ {
+ setSaving(true);
+ try {
+ for (const [parentId, childId] of selectedRelations) {
+ const suggested = !selectionAllSuggested;
+ const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
+ if (!existingContent || existingContent.suggested === suggested) continue;
+
+ const content = {
+ ...existingContent,
+ suggested: !selectionAllSuggested,
+ };
+
+ await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
+
+ parentChildMap.get(parentId).get(childId).content = content;
+ parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
+ }
+ } catch (e) {
+ setError("Failed to update some suggestions. Try again later");
+ }
+ setSaving(false);
+ }}
+ kind="primary_outline"
+ disabled={disabled}
+ >
+ { saving
+ ? _t("Saving...")
+ : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
+ }
+
+ >;
+ }
+
+ editSection =
+ { buttons }
+ ;
+ }
+
+ let results;
+ if (roomsMap.size) {
+ results = <>
+ {
+ setError("");
+ if (!selected.has(parentId)) {
+ setSelected(new Map(selected.set(parentId, new Set([childId]))));
+ return;
+ }
+
+ const parentSet = selected.get(parentId);
+ if (!parentSet.has(childId)) {
+ setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
+ return;
+ }
+
+ parentSet.delete(childId);
+ setSelected(new Map(selected.set(parentId, new Set(parentSet))));
+ }}
+ onViewRoomClick={(roomId, autoJoin) => {
+ showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
+ onFinished();
+ }}
+ />
+
+ >;
+ } else {
+ results =
+
{ _t("No results found") }
+
{ _t("You may want to try a different search or check for typos.") }
+
;
+ }
+
+ content = <>
+
+ { countsStr }
+ { editSection }
+
+ { error &&
+ { error }
+
}
+
+ { results }
+
+ { _t("Create room") }
+
+
+ >;
+ } else {
+ content = ;
}
// TODO loading state/error state
@@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis
-
- { adminButton }
-
{ content }
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 0b0f2a2ac9..2b4168a983 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -230,10 +230,10 @@ const SpaceLanding = ({ space }) => {
try {
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
- const parentChildRelations = new EnhancedMap