From 6f37e8b223cb99fa7a36fab883b5f411e184e2f4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Jul 2021 12:16:59 +0100 Subject: [PATCH 01/11] Use getChildren instead of getSpaceSummary as MSC2946 has moved on --- .../structures/SpaceRoomDirectory.tsx | 51 ++++++++++--------- src/stores/SpaceStore.tsx | 22 ++++---- test/test-utils.js | 4 +- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 038c1df5148..195c31e959a 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -18,7 +18,7 @@ import React, { ReactNode, useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; -import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; +import { IRoomChild, IRoomChildState } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; import { sortBy } from "lodash"; @@ -50,11 +50,11 @@ interface IHierarchyProps { initialText?: string; refreshToken?: any; additionalButtons?: ReactNode; - showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; + showRoom(room: IRoomChild, viaServers?: string[], autoJoin?: boolean): void; } interface ITileProps { - room: ISpaceSummaryRoom; + room: IRoomChild; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -205,7 +205,7 @@ const Tile: React.FC = ({ ; }; -export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { +export const showRoom = (room: IRoomChild, viaServers?: string[], autoJoin = false) => { // Don't let the user view a room they won't be able to either peek or join: // fail earlier so they don't have to click back to the directory. if (MatrixClientPeg.get().isGuest()) { @@ -234,8 +234,8 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi interface IHierarchyLevelProps { spaceId: string; - rooms: Map; - relations: Map>; + rooms: Map; + relations: Map>; parents: Set; selectedMap?: Map>; onViewRoomClick(roomId: string, autoJoin: boolean): void; @@ -260,7 +260,7 @@ export const HierarchyLevel = ({ // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting return getChildOrder(ev.content.order, null, ev.state_key); }); - const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { + const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IRoomChildState) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); @@ -318,31 +318,34 @@ export const HierarchyLevel = ({ // mutate argument refreshToken to force a reload export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ null, - ISpaceSummaryRoom[], - Map>?, + IRoomChild[], + Map>?, Map>?, Map>?, ] | [Error] => { // TODO pagination return useAsyncMemo(async () => { try { - const data = await cli.getSpaceSummary(space.roomId); + const { rooms } = await cli.getRoomChildren(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, 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()); - ev.content.via.forEach(via => set.add(via)); - } + + rooms.forEach(room => { + room.children_state.forEach((ev: IRoomChildState) => { + if (ev.type === EventType.SpaceChild) { + 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()); + ev.content.via.forEach(via => set.add(via)); + } + }); }); - return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; + return [null, rooms, parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO return [e]; @@ -370,7 +373,7 @@ export const SpaceHierarchy: React.FC = ({ if (!rooms) return null; const lcQuery = query.toLowerCase().trim(); - const roomsMap = new Map(rooms.map(r => [r.room_id, r])); + const roomsMap = new Map(rooms.map(r => [r.room_id, r])); if (!lcQuery) return roomsMap; const directMatches = rooms.filter(r => { @@ -614,7 +617,7 @@ const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText } { + showRoom={(room: IRoomChild, viaServers?: string[], autoJoin = false) => { showRoom(room, viaServers, autoJoin); onFinished(); }} @@ -637,6 +640,6 @@ export default SpaceRoomDirectory; // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // but works with the objects we get from the public room list -function getDisplayAliasForRoom(room: ISpaceSummaryRoom) { +function getDisplayAliasForRoom(room: IRoomChild) { return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); } diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d064b01257a..e25ca7e32db 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -19,7 +19,7 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { ISpaceSummaryRoom } from "matrix-js-sdk/src/@types/spaces"; +import { IRoomChild } from "matrix-js-sdk/src/@types/spaces"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { IRoomCapability } from "matrix-js-sdk/src/client"; @@ -63,7 +63,7 @@ export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change -export interface ISuggestedRoom extends ISpaceSummaryRoom { +export interface ISuggestedRoom extends IRoomChild { viaServers: string[]; } @@ -297,18 +297,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => { try { - const data = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit); + const { rooms } = await this.matrixClient.getRoomChildren(space.roomId, limit, 1, true); const viaMap = new EnhancedMap>(); - data.events.forEach(ev => { - if (ev.type === EventType.SpaceChild && ev.content.via?.length) { - ev.content.via.forEach(via => { - viaMap.getOrCreate(ev.state_key, new Set()).add(via); - }); - } + rooms.forEach(room => { + room.children_state.forEach(ev => { + if (ev.type === EventType.SpaceChild && ev.content.via?.length) { + ev.content.via.forEach(via => { + viaMap.getOrCreate(ev.state_key, new Set()).add(via); + }); + } + }); }); - return data.rooms.filter(roomInfo => { + return rooms.filter(roomInfo => { return roomInfo.room_type !== RoomType.Space && this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join"; }).map(roomInfo => ({ diff --git a/test/test-utils.js b/test/test-utils.js index 5e29fd242e1..8bc357c7b37 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -86,7 +86,9 @@ export function createTestClient() { isCryptoEnabled: () => false, getSpaceSummary: jest.fn().mockReturnValue({ rooms: [], - events: [], + }), + getRoomChildren: jest.fn().mockReturnValue({ + rooms: [], }), // Used by various internal bits we aren't concerned with (yet) From d459dbe7009567299c61d6765dd3f714d60ec0b0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Jul 2021 17:35:15 +0100 Subject: [PATCH 02/11] Refactor Space Hierarchy stuff in preparation for pagination --- src/components/structures/RoomDirectory.tsx | 2 +- .../structures/SpaceRoomDirectory.tsx | 242 +++++++++--------- src/stores/SpaceStore.tsx | 6 +- test/test-utils.js | 2 +- 4 files changed, 121 insertions(+), 131 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 84e8de82214..8d8609d1cfd 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -833,6 +833,6 @@ export default class RoomDirectory extends React.Component { // Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom // but works with the objects we get from the public room list -function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) { +export function getDisplayAliasForRoom(room: IPublicRoomsChunkRoom) { return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 7d74d7d5a70..05ba61973fa 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, useMemo, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; -import { IRoomChild, IRoomChildState } from "matrix-js-sdk/src/@types/spaces"; +import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; import { sortBy } from "lodash"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; +import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import BaseDialog from "../views/dialogs/BaseDialog"; @@ -30,8 +32,6 @@ import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomName from "../views/elements/RoomName"; -import { useAsyncMemo } from "../../hooks/useAsyncMemo"; -import { EnhancedMap } from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; @@ -42,20 +42,19 @@ import { useStateToggle } from "../../hooks/useStateToggle"; import { getChildOrder } from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { linkifyElement } from "../../HtmlUtils"; -import { getDisplayAliasForAliasSet } from "../../Rooms"; import { useDispatcher } from "../../hooks/useDispatcher"; -import defaultDispatcher from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; +import { getDisplayAliasForRoom } from "./RoomDirectory"; interface IHierarchyProps { space: Room; initialText?: string; additionalButtons?: ReactNode; - showRoom(room: IRoomChild, viaServers?: string[], autoJoin?: boolean): void; + showRoom(hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void; } interface ITileProps { - room: IRoomChild; + room: IHierarchyRoom; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -206,7 +205,9 @@ const Tile: React.FC = ({ ; }; -export const showRoom = (room: IRoomChild, viaServers?: string[], autoJoin = false) => { +export const showRoom = (hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => { + const room = hierarchy.roomMap.get(roomId); + // Don't let the user view a room they won't be able to either peek or join: // fail earlier so they don't have to click back to the directory. if (MatrixClientPeg.get().isGuest()) { @@ -224,7 +225,7 @@ export const showRoom = (room: IRoomChild, viaServers?: string[], autoJoin = fal _type: "room_directory", // instrumentation room_alias: roomAlias, room_id: room.room_id, - via_servers: viaServers, + via_servers: Array.from(hierarchy.viaMap.get(roomId) || []), oob_data: { avatarUrl: room.avatar_url, // XXX: This logic is duplicated from the JS SDK which would normally decide what the name is. @@ -234,9 +235,9 @@ export const showRoom = (room: IRoomChild, viaServers?: string[], autoJoin = fal }; interface IHierarchyLevelProps { - spaceId: string; - rooms: Map; - relations: Map>; + root: IHierarchyRoom; + roomSet: Set; + hierarchy: RoomHierarchy; parents: Set; selectedMap?: Map>; onViewRoomClick(roomId: string, autoJoin: boolean): void; @@ -244,67 +245,69 @@ interface IHierarchyLevelProps { } export const HierarchyLevel = ({ - spaceId, - rooms, - relations, + root, + roomSet, + hierarchy, parents, selectedMap, onViewRoomClick, onToggleClick, }: IHierarchyLevelProps) => { const cli = MatrixClientPeg.get(); - const space = cli.getRoom(spaceId); + const space = cli.getRoom(root.room_id); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - const children = Array.from(relations.get(spaceId)?.values() || []); - const sortedChildren = sortBy(children, ev => { - // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting - return getChildOrder(ev.content.order, null, ev.state_key); + const sortedChildren = sortBy(root.children_state, ev => { + return getChildOrder(ev.content.order, ev.origin_server_ts, ev.state_key); }); - const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IRoomChildState) => { - const roomId = ev.state_key; - if (!rooms.has(roomId)) return result; - result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); + + const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => { + const room = hierarchy.roomMap.get(ev.state_key); + if (room && roomSet.has(room)) { + result[room.room_type === RoomType.Space ? 0 : 1].push(room); + } return result; - }, [[], []]) || [[], []]; + }, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]); - const newParents = new Set(parents).add(spaceId); + const newParents = new Set(parents).add(root.room_id); return { - childRooms.map(roomId => ( + childRooms.map(room => ( { - onViewRoomClick(roomId, autoJoin); + onViewRoomClick(room.room_id, autoJoin); }} hasPermissions={hasPermissions} - onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} + onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined} /> )) } { - subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( + subspaces.filter(room => !newParents.has(room.room_id)).map(space => ( rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length} - suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} - selected={selectedMap?.get(spaceId)?.has(roomId)} + key={space.room_id} + room={space} + numChildRooms={space.children_state.filter(ev => { + const room = hierarchy.roomMap.get(ev.state_key); + return room && roomSet.has(room) && !room.room_type; + }).length} + suggested={hierarchy.isSuggested(root.room_id, space.room_id)} + selected={selectedMap?.get(root.room_id)?.has(space.room_id)} onViewRoomClick={(autoJoin) => { - onViewRoomClick(roomId, autoJoin); + onViewRoomClick(space.room_id, autoJoin); }} hasPermissions={hasPermissions} - onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} + onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined} > ; }; -export const useSpaceSummary = (space: Room): [ - null, - IRoomChild[], - Map>?, - Map>?, - Map>?, -] | [Error] => { - // crude temporary refresh token approach until we have pagination and rework the data flow here - const [refreshToken, setRefreshToken] = useState(0); +export const useSpaceSummary = (space: Room): { + loading: boolean; + rooms: IHierarchyRoom[]; + hierarchy: RoomHierarchy; + loadMore(pageSize?: number): Promise ; +} => { + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + + const INITIAL_PAGE_SIZE = 20; + const [hierarchy, setHierarchy] = useState(); + + const resetHierarchy = useCallback(() => { + const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE); + setHierarchy(hierarchy); + + let discard = false; + hierarchy.load().then(() => { + if (discard) return; + setRooms(hierarchy.rooms); + setLoading(false); + }); + + return () => { + discard = true; + }; + }, [space]); + useEffect(resetHierarchy, [resetHierarchy]); + useDispatcher(defaultDispatcher, (payload => { if (payload.action === Action.UpdateSpaceHierarchy) { - setRefreshToken(t => t + 1); + setLoading(true); + setRooms([]); // TODO + resetHierarchy(); } })); - // TODO pagination - return useAsyncMemo(async () => { - try { - const { rooms } = await space.client.getRoomChildren(space.roomId); - - const parentChildRelations = new EnhancedMap>(); - const childParentRelations = new EnhancedMap>(); - const viaMap = new EnhancedMap>(); + const loadMore = useCallback(async (pageSize?: number) => { + if (!hierarchy.canLoadMore || hierarchy.noSupport) return; - rooms.forEach(room => { - room.children_state.forEach((ev: IRoomChildState) => { - if (ev.type === EventType.SpaceChild) { - 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()); - ev.content.via.forEach(via => set.add(via)); - } - }); - }); + setLoading(true); + await hierarchy.load(pageSize); + setRooms(hierarchy.rooms); + setLoading(false); + }, [hierarchy]); - return [null, rooms, parentChildRelations, viaMap, childParentRelations]; - } catch (e) { - console.error(e); // TODO - return [e]; - } - }, [space, refreshToken], [undefined]); + return { loading, rooms, hierarchy, loadMore }; }; export const SpaceHierarchy: React.FC = ({ @@ -374,14 +381,12 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space); + const { loading, rooms, hierarchy } = useSpaceSummary(space); - const roomsMap = useMemo(() => { - if (!rooms) return null; + const filteredRoomSet = useMemo>(() => { + if (!rooms.length) return new Set(); const lcQuery = query.toLowerCase().trim(); - - const roomsMap = new Map(rooms.map(r => [r.room_id, r])); - if (!lcQuery) return roomsMap; + if (!lcQuery) return new Set(rooms); const directMatches = rooms.filter(r => { return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery); @@ -393,34 +398,30 @@ export const SpaceHierarchy: React.FC = ({ while (queue.length) { const roomId = queue.pop(); visited.add(roomId); - childParentMap.get(roomId)?.forEach(parentId => { + hierarchy.backRefs.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]); + return new Set(rooms.filter(r => visited.has(r.room_id))); + }, [rooms, hierarchy, query]); const [error, setError] = useState(""); const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); - if (summaryError) { + if (!loading && hierarchy.noSupport) { return

{ _t("Your server does not support showing space hierarchies.") }

; } let content; - if (roomsMap) { - const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; - const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at + if (loading) { + content = ; + } else { + const numRooms = Array.from(filteredRoomSet).filter(r => !r.room_type).length; + const numSpaces = filteredRoomSet.size - numRooms - 1; // -1 at the end to exclude the space we are looking at let countsStr; if (numSpaces > 1) { @@ -438,7 +439,7 @@ export const SpaceHierarchy: React.FC = ({ }); const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + return hierarchy.isSuggested(parentId, childId); }); const disabled = !selectedRelations.length || removing || saving; @@ -461,17 +462,14 @@ export const SpaceHierarchy: React.FC = ({ try { for (const [parentId, childId] of selectedRelations) { await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); - parentChildMap.get(parentId).delete(childId); - if (parentChildMap.get(parentId).size > 0) { - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); - } else { - parentChildMap.delete(parentId); - } + + hierarchy.removeRelation(parentId, childId); } } catch (e) { setError(_t("Failed to remove some rooms. Try again later")); } setRemoving(false); + setSelected(new Map()); }} kind="danger_outline" disabled={disabled} @@ -485,7 +483,7 @@ export const SpaceHierarchy: React.FC = ({ try { for (const [parentId, childId] of selectedRelations) { const suggested = !selectionAllSuggested; - const existingContent = parentChildMap.get(parentId)?.get(childId)?.content; + const existingContent = hierarchy.getRelation(parentId, childId)?.content; if (!existingContent || existingContent.suggested === suggested) continue; const content = { @@ -495,8 +493,8 @@ export const SpaceHierarchy: React.FC = ({ await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId); - parentChildMap.get(parentId).get(childId).content = content; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + // mutate the local state to save us having to refetch the world + existingContent.suggested = content.suggested; } } catch (e) { setError("Failed to update some suggestions. Try again later"); @@ -516,14 +514,14 @@ export const SpaceHierarchy: React.FC = ({ } let results; - if (roomsMap.size) { + if (filteredRoomSet.size) { const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); results = <> { @@ -543,7 +541,7 @@ export const SpaceHierarchy: React.FC = ({ setSelected(new Map(selected.set(parentId, new Set(parentSet)))); } : undefined} onViewRoomClick={(roomId, autoJoin) => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); + showRoom(hierarchy, roomId, autoJoin); }} /> { children &&
} @@ -571,8 +569,6 @@ export const SpaceHierarchy: React.FC = ({ { children } ; - } else { - content = ; } // TODO loading state/error state @@ -624,8 +620,8 @@ const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText } { - showRoom(room, viaServers, autoJoin); + showRoom={(hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => { + showRoom(hierarchy, roomId, autoJoin); onFinished(); }} initialText={initialText} @@ -644,9 +640,3 @@ const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText } }; export default SpaceRoomDirectory; - -// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom -// but works with the objects we get from the public room list -function getDisplayAliasForRoom(room: IRoomChild) { - return getDisplayAliasForAliasSet(room.canonical_alias, room.aliases); -} diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e25ca7e32db..bf8c1ecb44d 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -19,7 +19,7 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { IRoomChild } from "matrix-js-sdk/src/@types/spaces"; +import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { IRoomCapability } from "matrix-js-sdk/src/client"; @@ -63,7 +63,7 @@ export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change -export interface ISuggestedRoom extends IRoomChild { +export interface ISuggestedRoom extends IHierarchyRoom { viaServers: string[]; } @@ -297,7 +297,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS): Promise => { try { - const { rooms } = await this.matrixClient.getRoomChildren(space.roomId, limit, 1, true); + const { rooms } = await this.matrixClient.getRoomHierarchy(space.roomId, limit, 1, true); const viaMap = new EnhancedMap>(); rooms.forEach(room => { diff --git a/test/test-utils.js b/test/test-utils.js index 8bc357c7b37..b337828b685 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -87,7 +87,7 @@ export function createTestClient() { getSpaceSummary: jest.fn().mockReturnValue({ rooms: [], }), - getRoomChildren: jest.fn().mockReturnValue({ + getRoomHierarchy: jest.fn().mockReturnValue({ rooms: [], }), From 259627fba2f9a83e6f9a2e3cbf39b04c225aaf1d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Jul 2021 11:01:49 +0100 Subject: [PATCH 03/11] Clean up SpaceHierarchy --- res/css/_components.scss | 2 +- ...oomDirectory.scss => _SpaceHierarchy.scss} | 54 +++------ res/css/structures/_SpaceRoomView.scss | 5 - ...ceRoomDirectory.tsx => SpaceHierarchy.tsx} | 108 ++++-------------- src/components/structures/SpaceRoomView.tsx | 2 +- 5 files changed, 43 insertions(+), 128 deletions(-) rename res/css/structures/{_SpaceRoomDirectory.scss => _SpaceHierarchy.scss} (86%) rename src/components/structures/{SpaceRoomDirectory.tsx => SpaceHierarchy.tsx} (86%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 1feea1d26fb..15cb20d9635 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,8 +27,8 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpaceHierarchy.scss"; @import "./structures/_SpacePanel.scss"; -@import "./structures/_SpaceRoomDirectory.scss"; @import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceHierarchy.scss similarity index 86% rename from res/css/structures/_SpaceRoomDirectory.scss rename to res/css/structures/_SpaceHierarchy.scss index bc343f535c1..afb01d7c9a8 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -14,21 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SpaceRoomDirectory_dialogWrapper > .mx_Dialog { - max-width: 960px; - height: 100%; -} - -.mx_SpaceRoomDirectory { - height: 100%; - margin-bottom: 12px; - color: $primary-fg-color; - word-break: break-word; - display: flex; - flex-direction: column; -} - -.mx_SpaceRoomDirectory, .mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -67,7 +52,7 @@ limitations under the License. margin: 24px 0 16px; } - .mx_SpaceRoomDirectory_noResults { + .mx_SpaceHierarchy_noResults { text-align: center; > div { @@ -77,7 +62,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_listHeader { + .mx_SpaceHierarchy_listHeader { display: flex; min-height: 32px; align-items: center; @@ -104,7 +89,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_error { + .mx_SpaceHierarchy_error { position: relative; font-weight: $font-semi-bold; color: $notice-primary-color; @@ -123,13 +108,8 @@ limitations under the License. background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } -} - -.mx_SpaceRoomDirectory_list { - margin-top: 16px; - padding-bottom: 40px; - .mx_SpaceRoomDirectory_roomCount { + .mx_SpaceHierarchy_roomCount { > h3 { display: inline; font-weight: $font-semi-bold; @@ -146,13 +126,13 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_subspace { + .mx_SpaceHierarchy_subspace { .mx_BaseAvatar_image { border-radius: 8px; } } - .mx_SpaceRoomDirectory_subspace_toggle { + .mx_SpaceHierarchy_subspace_toggle { position: absolute; left: -1px; top: 10px; @@ -176,17 +156,17 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } - &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + &.mx_SpaceHierarchy_subspace_toggle_shown::before { transform: rotate(0deg); } } - .mx_SpaceRoomDirectory_subspace_children { + .mx_SpaceHierarchy_subspace_children { position: relative; padding-left: 12px; } - .mx_SpaceRoomDirectory_roomTile { + .mx_SpaceHierarchy_roomTile { position: relative; padding: 8px 16px; border-radius: 8px; @@ -203,7 +183,7 @@ limitations under the License. grid-column: 1; } - .mx_SpaceRoomDirectory_roomTile_name { + .mx_SpaceHierarchy_roomTile_name { font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-18px; @@ -231,7 +211,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_roomTile_info { + .mx_SpaceHierarchy_roomTile_info { font-size: $font-14px; line-height: $font-18px; color: $secondary-fg-color; @@ -243,7 +223,7 @@ limitations under the License. overflow: hidden; } - .mx_SpaceRoomDirectory_actions { + .mx_SpaceHierarchy_actions { text-align: right; margin-left: 20px; grid-column: 3; @@ -277,8 +257,8 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_roomTile, - .mx_SpaceRoomDirectory_subspace_children { + .mx_SpaceHierarchy_roomTile, + .mx_SpaceHierarchy_subspace_children { &::before { content: ""; position: absolute; @@ -290,8 +270,8 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_actions { - .mx_SpaceRoomDirectory_actionsText { + .mx_SpaceHierarchy_actions { + .mx_SpaceHierarchy_actionsText { font-weight: normal; font-size: $font-12px; line-height: $font-15px; @@ -306,7 +286,7 @@ limitations under the License. margin: 20px 0; } - .mx_SpaceRoomDirectory_createRoom { + .mx_SpaceHierarchy_createRoom { display: block; margin: 16px auto 0; width: max-content; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index e4832d94308..3119d2fe6e3 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -354,11 +354,6 @@ $SpaceRoomViewInnerWidth: 428px; display: none; } } - - .mx_SpaceRoomDirectory_list { - // we don't want this container to get forced into the flexbox layout - display: contents; - } } .mx_SpaceRoomView_privateScope { diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceHierarchy.tsx similarity index 86% rename from src/components/structures/SpaceRoomDirectory.tsx rename to src/components/structures/SpaceHierarchy.tsx index 05ba61973fa..d17d17f31e4 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; @@ -27,13 +27,10 @@ import dis from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; -import BaseDialog from "../views/dialogs/BaseDialog"; import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; import RoomAvatar from "../views/avatars/RoomAvatar"; -import RoomName from "../views/elements/RoomName"; import StyledCheckbox from "../views/elements/StyledCheckbox"; -import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; import { mediaFromMxc } from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; @@ -46,7 +43,7 @@ import { useDispatcher } from "../../hooks/useDispatcher"; import { Action } from "../../dispatcher/actions"; import { getDisplayAliasForRoom } from "./RoomDirectory"; -interface IHierarchyProps { +interface IProps { space: Room; initialText?: string; additionalButtons?: ReactNode; @@ -148,13 +145,13 @@ const Tile: React.FC = ({ const content = { avatar } -
+
{ name } { suggestedSection }
e && linkifyElement(e)} onClick={ev => { // prevent clicks on links from bubbling up to the room tile @@ -165,7 +162,7 @@ const Tile: React.FC = ({ > { description }
-
+
{ button } { checkbox }
@@ -176,8 +173,8 @@ const Tile: React.FC = ({ if (children) { // the chevron is purposefully a div rather than a button as it should be ignored for a11y childToggle =
{ ev.stopPropagation(); @@ -185,7 +182,7 @@ const Tile: React.FC = ({ }} />; if (showChildren) { - childSection =
+ childSection =
{ children }
; } @@ -193,8 +190,8 @@ const Tile: React.FC = ({ return <> @@ -319,6 +316,8 @@ export const HierarchyLevel = ({ ; }; +const INITIAL_PAGE_SIZE = 20; + export const useSpaceSummary = (space: Room): { loading: boolean; rooms: IHierarchyRoom[]; @@ -327,8 +326,6 @@ export const useSpaceSummary = (space: Room): { } => { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); - - const INITIAL_PAGE_SIZE = 20; const [hierarchy, setHierarchy] = useState(); const resetHierarchy = useCallback(() => { @@ -368,13 +365,12 @@ export const useSpaceSummary = (space: Room): { return { loading, rooms, hierarchy, loadMore }; }; -export const SpaceHierarchy: React.FC = ({ +const SpaceHierarchy = ({ space, initialText = "", showRoom, additionalButtons, - children, -}) => { +}: IProps) => { const cli = MatrixClientPeg.get(); const userId = cli.getUserId(); const [query, setQuery] = useState(initialText); @@ -416,7 +412,8 @@ export const SpaceHierarchy: React.FC = ({ return

{ _t("Your server does not support showing space hierarchies.") }

; } - let content; + let content: JSX.Element; + let loader: JSX.Element; if (loading) { content = ; } else { @@ -513,7 +510,7 @@ export const SpaceHierarchy: React.FC = ({ ; } - let results; + let results: JSX.Element; if (filteredRoomSet.size) { const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); @@ -544,34 +541,31 @@ export const SpaceHierarchy: React.FC = ({ showRoom(hierarchy, roomId, autoJoin); }} /> - { children &&
} ; } else { - results =
+ results =

{ _t("No results found") }

{ _t("You may want to try a different search or check for typos.") }
; } content = <> -
+
{ countsStr } { additionalButtons } { manageButtons }
- { error &&
+ { error &&
{ error }
} - - { results } - { children } - + + { results } + { loader } ; } - // TODO loading state/error state return <> = ({ ; }; -interface IProps { - space: Room; - initialText?: string; - onFinished(): void; -} - -const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText }) => { - const onCreateRoomClick = () => { - dis.dispatch({ - action: 'view_create_room', - public: true, - }); - onFinished(); - }; - - const title = - -
-

{ _t("Explore rooms") }

-
-
-
; - - return ( - -
- { _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 }; - } }, - ) } - - { - showRoom(hierarchy, roomId, autoJoin); - onFinished(); - }} - initialText={initialText} - > - - { _t("Create room") } - - -
-
- ); -}; - -export default SpaceRoomDirectory; +export default SpaceHierarchy; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 5829578cd23..f2052f1613a 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -54,7 +54,7 @@ import { showCreateNewSubspace, showSpaceSettings, } from "../../utils/space"; -import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory"; +import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import MemberAvatar from "../views/avatars/MemberAvatar"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; From f4ed9aeef11040953e14433b8571ce119a09d2ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Jul 2021 11:02:05 +0100 Subject: [PATCH 04/11] Add pagination mechanism to SpaceHierarchy based on IntersectionObserver --- src/components/structures/SpaceHierarchy.tsx | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index d17d17f31e4..f1f5d70fa6c 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -377,7 +377,7 @@ const SpaceHierarchy = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const { loading, rooms, hierarchy } = useSpaceSummary(space); + const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space); const filteredRoomSet = useMemo>(() => { if (!rooms.length) return new Set(); @@ -408,6 +408,29 @@ const SpaceHierarchy = ({ const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); + const handleObserver = (entries: IntersectionObserverEntry[]) => { + const target = entries[0]; + if (target.isIntersecting) { + loadMore(); + } + }; + + const observerRef = useRef(); + const loaderRef = (element: HTMLDivElement) => { + if (observerRef.current) { + observerRef.current.disconnect(); + } else if (element) { + observerRef.current = new IntersectionObserver(handleObserver, { + root: element.parentElement, + rootMargin: "0px 0px 600px 0px", + }); + } + + if (observerRef.current && element) { + observerRef.current.observe(element); + } + }; + if (!loading && hierarchy.noSupport) { return

{ _t("Your server does not support showing space hierarchies.") }

; } @@ -542,6 +565,10 @@ const SpaceHierarchy = ({ }} /> ; + + loader =
+ +
; } else { results =

{ _t("No results found") }

From a3ca2abae386eb921c54f0f528219c8584bbb59d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Jul 2021 11:11:21 +0100 Subject: [PATCH 05/11] i18n --- src/i18n/strings/en_EN.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4537bddf1..a960af9f792 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2812,8 +2812,6 @@ "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "Search names and descriptions": "Search names and descriptions", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", - "Create room": "Create room", "Spaces are a beta feature.": "Spaces are a beta feature.", "Public space": "Public space", "Private space": "Private space", From 1c2dc13fa330fe2de05d6bfabaaf1028428e8c66 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Jul 2021 12:12:49 +0100 Subject: [PATCH 06/11] factor our observer hook --- src/components/structures/SpaceHierarchy.tsx | 48 +++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index f1f5d70fa6c..f13fc7f208e 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -365,6 +365,31 @@ export const useSpaceSummary = (space: Room): { return { loading, rooms, hierarchy, loadMore }; }; +const useIntersectionObserver = (callback: () => void) => { + const handleObserver = (entries: IntersectionObserverEntry[]) => { + const target = entries[0]; + if (target.isIntersecting) { + callback(); + } + }; + + const observerRef = useRef(); + return (element: HTMLDivElement) => { + if (observerRef.current) { + observerRef.current.disconnect(); + } else if (element) { + observerRef.current = new IntersectionObserver(handleObserver, { + root: element.parentElement, + rootMargin: "0px 0px 600px 0px", + }); + } + + if (observerRef.current && element) { + observerRef.current.observe(element); + } + }; +}; + const SpaceHierarchy = ({ space, initialText = "", @@ -408,28 +433,7 @@ const SpaceHierarchy = ({ const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); - const handleObserver = (entries: IntersectionObserverEntry[]) => { - const target = entries[0]; - if (target.isIntersecting) { - loadMore(); - } - }; - - const observerRef = useRef(); - const loaderRef = (element: HTMLDivElement) => { - if (observerRef.current) { - observerRef.current.disconnect(); - } else if (element) { - observerRef.current = new IntersectionObserver(handleObserver, { - root: element.parentElement, - rootMargin: "0px 0px 600px 0px", - }); - } - - if (observerRef.current && element) { - observerRef.current.observe(element); - } - }; + const loaderRef = useIntersectionObserver(loadMore); if (!loading && hierarchy.noSupport) { return

{ _t("Your server does not support showing space hierarchies.") }

; From d74e9c4f905327210e61ecf82982ac52a0ee49a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Aug 2021 16:47:14 +0100 Subject: [PATCH 07/11] Remove impossible space hierarchy size string --- res/css/structures/_SpaceHierarchy.scss | 6 ++++++ src/components/structures/SpaceHierarchy.tsx | 14 +------------- src/i18n/strings/en_EN.json | 6 ++---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/res/css/structures/_SpaceHierarchy.scss b/res/css/structures/_SpaceHierarchy.scss index d79eed1a645..f5810b8dfe6 100644 --- a/res/css/structures/_SpaceHierarchy.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -70,6 +70,12 @@ limitations under the License. font-size: $font-15px; line-height: $font-24px; color: $primary-fg-color; + margin-bottom: 12px; + + > h4 { + font-weight: $font-semi-bold; + margin: 0; + } .mx_AccessibleButton { padding: 4px 12px; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index f13fc7f208e..c55902cf702 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -444,18 +444,6 @@ const SpaceHierarchy = ({ if (loading) { content = ; } else { - const numRooms = Array.from(filteredRoomSet).filter(r => !r.room_type).length; - const numSpaces = filteredRoomSet.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 manageButtons; if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { @@ -582,7 +570,7 @@ const SpaceHierarchy = ({ content = <>
- { countsStr } +

{ query.trim() ? _t("Results") : _t("Rooms and spaces") }

{ additionalButtons } { manageButtons } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 64a3fe2951c..9f415420075 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2811,10 +2811,6 @@ "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", - "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", - "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces", - "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space", - "%(count)s rooms and 1 space|one": "%(count)s room and 1 space", "Select a room below first": "Select a room below first", "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Removing...": "Removing...", @@ -2822,6 +2818,8 @@ "Mark as suggested": "Mark as suggested", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", + "Results": "Results", + "Rooms and spaces": "Rooms and spaces", "Search names and descriptions": "Search names and descriptions", "Private space": "Private space", " invites you": " invites you", From 38645d90545d28f0726fde571b18e0b230a0f18d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Aug 2021 17:07:53 +0100 Subject: [PATCH 08/11] Fix loading state issues for spaces pagination --- src/components/structures/SpaceHierarchy.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index c55902cf702..cea6a568e7e 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -441,7 +441,7 @@ const SpaceHierarchy = ({ let content: JSX.Element; let loader: JSX.Element; - if (loading) { + if (loading && !rooms.length) { content = ; } else { let manageButtons; @@ -558,9 +558,11 @@ const SpaceHierarchy = ({ /> ; - loader =
- -
; + if (hierarchy.canLoadMore) { + loader =
+ +
; + } } else { results =

{ _t("No results found") }

From 8216a35a56d8962f1d172ff43fe2a467a7677b6e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Aug 2021 21:02:30 +0100 Subject: [PATCH 09/11] remove spurious mxclient stub --- test/test-utils.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test-utils.js b/test/test-utils.js index b337828b685..11a3400fbb6 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -84,9 +84,6 @@ export function createTestClient() { generateClientSecret: () => "t35tcl1Ent5ECr3T", isGuest: () => false, isCryptoEnabled: () => false, - getSpaceSummary: jest.fn().mockReturnValue({ - rooms: [], - }), getRoomHierarchy: jest.fn().mockReturnValue({ rooms: [], }), From 85b1f166e8fb122d7db8a69c9c659164de6e9823 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Aug 2021 12:03:14 +0100 Subject: [PATCH 10/11] post-merge tidy up --- res/css/structures/_SpaceHierarchy.scss | 8 +- src/components/structures/SpaceHierarchy.tsx | 264 ++++++++++--------- src/i18n/strings/en_EN.json | 8 +- 3 files changed, 154 insertions(+), 126 deletions(-) diff --git a/res/css/structures/_SpaceHierarchy.scss b/res/css/structures/_SpaceHierarchy.scss index 0cefaf252fb..74dfb5da6bc 100644 --- a/res/css/structures/_SpaceHierarchy.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -116,6 +116,12 @@ limitations under the License. } } + .mx_SpaceHierarchy_list { + list-style: none; + padding: 0; + margin: 0; + } + .mx_SpaceHierarchy_roomCount { > h3 { display: inline; @@ -264,7 +270,7 @@ limitations under the License. } } - li.mx_SpaceRoomDirectory_roomTileWrapper { + li.mx_SpaceHierarchy_roomTileWrapper { list-style: none; } diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 120f82f717f..a0d4d9c42ac 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -23,15 +23,18 @@ import React, { useState, KeyboardEvent, KeyboardEventHandler, + useContext, + SetStateAction, + Dispatch, } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { sortBy } from "lodash"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; @@ -53,12 +56,13 @@ import { Action } from "../../dispatcher/actions"; import { Key } from "../../Keyboard"; import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import { getDisplayAliasForRoom } from "./RoomDirectory"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; interface IProps { space: Room; initialText?: string; additionalButtons?: ReactNode; - showRoom(hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void; + showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void; } interface ITileProps { @@ -81,7 +85,7 @@ const Tile: React.FC = ({ numChildRooms, children, }) => { - const cli = MatrixClientPeg.get(); + const cli = useContext(MatrixClientContext); const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null; const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0] || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); @@ -238,7 +242,7 @@ const Tile: React.FC = ({ handled = true; if (showChildren) { const childSection = ref.current?.nextElementSibling; - childSection?.querySelector(".mx_SpaceRoomDirectory_roomTile")?.focus(); + childSection?.querySelector(".mx_SpaceHierarchy_roomTile")?.focus(); } else { toggleShowChildren(); } @@ -253,7 +257,7 @@ const Tile: React.FC = ({ } return
  • @@ -274,12 +278,12 @@ const Tile: React.FC = ({
  • ; }; -export const showRoom = (hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => { +export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => { const room = hierarchy.roomMap.get(roomId); // Don't let the user view a room they won't be able to either peek or join: // fail earlier so they don't have to click back to the directory. - if (MatrixClientPeg.get().isGuest()) { + if (cli.isGuest()) { if (!room.world_readable && !room.guest_can_join) { dis.dispatch({ action: "require_registration" }); return; @@ -322,7 +326,7 @@ export const HierarchyLevel = ({ onViewRoomClick, onToggleClick, }: IHierarchyLevelProps) => { - const cli = MatrixClientPeg.get(); + const cli = useContext(MatrixClientContext); const space = cli.getRoom(root.room_id); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); @@ -462,14 +466,107 @@ const useIntersectionObserver = (callback: () => void) => { }; }; +interface IManageButtonsProps { + hierarchy: RoomHierarchy; + selected: Map>; + setSelected: Dispatch>>>; + setError: Dispatch>; +} + +const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageButtonsProps) => { + const cli = useContext(MatrixClientContext); + + const [removing, setRemoving] = useState(false); + const [saving, setSaving] = useState(false); + + const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { + return [ + ...selected.get(parentId).values(), + ].map(childId => [parentId, childId]) as [string, string][]; + }); + + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return hierarchy.isSuggested(parentId, childId); + }); + + const disabled = !selectedRelations.length || removing || saving; + + let Button: React.ComponentType> = AccessibleButton; + let props = {}; + if (!selectedRelations.length) { + Button = AccessibleTooltipButton; + props = { + tooltip: _t("Select a room below first"), + yOffset: -40, + }; + } + + return <> + + + ; +}; + const SpaceHierarchy = ({ space, initialText = "", showRoom, additionalButtons, }: IProps) => { - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); + const cli = useContext(MatrixClientContext); const [query, setQuery] = useState(initialText); const [selected, setSelected] = useState(new Map>()); // Map> @@ -502,8 +599,6 @@ const SpaceHierarchy = ({ }, [rooms, hierarchy, query]); const [error, setError] = useState(""); - const [removing, setRemoving] = useState(false); - const [saving, setSaving] = useState(false); const loaderRef = useIntersectionObserver(loadMore); @@ -511,13 +606,29 @@ const SpaceHierarchy = ({ return

    { _t("Your server does not support showing space hierarchies.") }

    ; } - const onKeyDown = (ev: KeyboardEvent, state: IState) => { - if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) { + const onKeyDown = (ev: KeyboardEvent, state: IState): void => { + if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceHierarchy_search")) { state.refs[0]?.current?.focus(); } }; - // TODO loading state/error state + const onToggleClick = (parentId: string, childId: string): void => { + 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)))); + }; + return { ({ onKeyDownHandler }) => { let content: JSX.Element; @@ -526,93 +637,11 @@ const SpaceHierarchy = ({ if (loading && !rooms.length) { content = ; } else { - let manageButtons; - 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][]; - }); - - const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return hierarchy.isSuggested(parentId, childId); - }); - - const disabled = !selectedRelations.length || removing || saving; - - let Button: React.ComponentType> = AccessibleButton; - let props = {}; - if (!selectedRelations.length) { - Button = AccessibleTooltipButton; - props = { - tooltip: _t("Select a room below first"), - yOffset: -40, - }; - } - - manageButtons = <> - - - ; - } + const hasPermissions = space?.getMyMembership() === "join" && + space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); let results: JSX.Element; if (filteredRoomSet.size) { - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - 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)))); - } : undefined} + onToggleClick={hasPermissions ? onToggleClick : undefined} onViewRoomClick={(roomId, autoJoin) => { - showRoom(hierarchy, roomId, autoJoin); + showRoom(cli, hierarchy, roomId, autoJoin); }} /> ; @@ -659,13 +673,25 @@ const SpaceHierarchy = ({

    { query.trim() ? _t("Results") : _t("Rooms and spaces") }

    { additionalButtons } - { manageButtons } + { hasPermissions && ( + + ) }
    { error &&
    { error }
    } -
      +
        { results }
      { loader } @@ -674,7 +700,7 @@ const SpaceHierarchy = ({ return <> create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", - "Create room": "Create room", "Private space": "Private space", " invites you": " invites you", "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", From 8ddaa7faf1eaa705f8b8549d56a4ce114bffa4ff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Aug 2021 12:08:57 +0100 Subject: [PATCH 11/11] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5edcd932215..0a75cb9dce2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2843,12 +2843,12 @@ "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", - "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Select a room below first": "Select a room below first", "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Removing...": "Removing...", "Mark as not suggested": "Mark as not suggested", "Mark as suggested": "Mark as suggested", + "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "Results": "Results",